Handle orphan exec ends without clobbering active exploring cell (#12313)

Summary
- distinguish exec end handling targets (active tracking, active orphan
history, new cell) so unified exec responses don’t clobber unrelated
exploring cells
- ensure orphan ends flush existing exploring history when complete,
insert standalone history entries, and keep active cells correct
- add regression tests plus a snapshot covering the new behavior and
expose the ExecCell completion result for verification

Fix for https://github.com/openai/codex/issues/12278

---------

Co-authored-by: Josh McKinney <joshka@openai.com>
This commit is contained in:
jif-oai
2026-02-22 14:26:58 +00:00
committed by GitHub
parent 4666a6e631
commit 0a0caa9df2
4 changed files with 266 additions and 45 deletions

View File

@@ -1,3 +1,10 @@
//! Data model for grouped exec-call history cells in the TUI transcript.
//!
//! An `ExecCell` can represent either a single command or an "exploring" group of related read/
//! list/search commands. The chat widget relies on stable `call_id` matching to route progress and
//! end events into the right cell, and it treats "call id not found" as a real signal (for
//! example, an orphan end that should render as a separate history entry).
use std::time::Duration;
use std::time::Instant;
@@ -67,17 +74,24 @@ impl ExecCell {
}
}
/// Marks the most recently matching call as finished and returns whether a call was found.
///
/// Callers should treat `false` as a routing mismatch rather than silently ignoring it. The
/// chat widget uses that signal to avoid attaching an orphan `exec_end` event to an unrelated
/// active exploring cell, which would incorrectly collapse two transcript entries together.
pub(crate) fn complete_call(
&mut self,
call_id: &str,
output: CommandOutput,
duration: Duration,
) {
if let Some(call) = self.calls.iter_mut().rev().find(|c| c.call_id == call_id) {
call.output = Some(output);
call.duration = Some(duration);
call.start_time = None;
}
) -> bool {
let Some(call) = self.calls.iter_mut().rev().find(|c| c.call_id == call_id) else {
return false;
};
call.output = Some(output);
call.duration = Some(duration);
call.start_time = None;
true
}
pub(crate) fn should_flush(&self) -> bool {