Track TUI command completion separately from output (#34218)

## Why

App-server command output deltas populate an exec cell before the command
finishes. Treating the presence of output as completion can therefore mark a
streaming command inactive too early and mishandle it when the turn is
interrupted.

## What changed

- Use the recorded duration, which is set when a command ends, as the exec
  call's completion marker.
- Preserve streamed output when marking unfinished calls as failed and render
  status lines only after completion.
- Keep a single aggregated output representation and render truncated output
  without collecting every line first.
- Append grouped exploration calls in place and coalesce adjacent reads while
  rendering without cloning the calls.

## Testing

- Add snapshots covering active streamed output, transcript output, and the
  resulting history entry after interruption.
- Add coverage for truncating newline-dense output.

GitOrigin-RevId: 420ac1e5fe1b9fcdbdaa5a8998f3e43a5b720254
This commit is contained in:
Charlie Marsh authored and copyberry committed 2026-07-19 19:44:10 +00:00
1 parent 74bfbda9b5
commit 854a82dbfd
10 files changed
+179 -109

No files matched your search

@@ -850,6 +850,57 @@ async fn live_app_server_command_execution_strips_shell_wrapper() {
);
}
#[tokio::test]
async fn live_app_server_command_output_delta_transcript_snapshot() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
chat.on_task_started();
begin_exec(&mut chat, "cmd-1", "printf 'stdout\\nstderr\\n'");
for delta in ["stdout\n", "stderr\n"] {
chat.handle_server_notification(
ServerNotification::CommandExecutionOutputDelta(
codex_app_server_protocol::CommandExecutionOutputDeltaNotification {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
item_id: "cmd-1".to_string(),
delta: delta.to_string(),
},
),
/*replay_kind*/ None,
);
}
let active = active_blob(&chat);
assert_chatwidget_snapshot!("live_app_server_command_output_delta_active", active);
let transcript = chat
.active_cell_transcript_lines(/*width*/ 80)
.expect("active exec transcript lines");
assert_chatwidget_snapshot!(
"live_app_server_command_output_delta_transcript",
lines_to_single_string(&transcript)
);
handle_turn_interrupted(&mut chat, "turn-1");
let mut completed = None;
while let Ok(event) = rx.try_recv() {
if let AppEvent::InsertHistoryCell(cell) = event {
let transcript = lines_to_single_string(&cell.transcript_lines(/*width*/ 80));
if transcript.contains("printf 'stdout\\nstderr\\n'") {
completed = Some(transcript);
}
}
}
let completed = completed.expect("expected the interrupted command in history");
let completed = regex_lite::Regex::new(r"(?m) • (?:\d+ms|\d+\.\d+s|\d+m \d+s)$")
.expect("valid duration regex")
.replace(&completed, " • <duration>");
assert_chatwidget_snapshot!(
"live_app_server_command_output_delta_interrupted",
completed
);
}
#[tokio::test]
async fn live_app_server_collab_wait_items_render_history() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;