mirror of
https://github.com/openai/codex.git
synced 2026-11-05 16:29:31 +00:00
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:
1 parent
74bfbda9b5
commit
854a82dbfd
10 files changed
+179
-109
No files matched your search
@@ -284,7 +284,7 @@ impl ChatWidget {
|
||||
.active_cell
|
||||
.as_mut()
|
||||
.and_then(|c| c.as_any_mut().downcast_mut::<ExecCell>())
|
||||
&& let Some(new_exec) = cell.with_added_call(
|
||||
&& cell.add_call(
|
||||
id.clone(),
|
||||
command.clone(),
|
||||
parsed_cmd.clone(),
|
||||
@@ -292,7 +292,6 @@ impl ChatWidget {
|
||||
/*interaction_input*/ None,
|
||||
)
|
||||
{
|
||||
*cell = new_exec;
|
||||
self.bump_active_cell_revision();
|
||||
} else {
|
||||
self.flush_active_cell();
|
||||
@@ -383,13 +382,11 @@ impl ChatWidget {
|
||||
let output = if is_unified_exec_interaction {
|
||||
CommandOutput {
|
||||
exit_code,
|
||||
formatted_output: String::new(),
|
||||
aggregated_output: String::new(),
|
||||
}
|
||||
} else {
|
||||
CommandOutput {
|
||||
exit_code,
|
||||
formatted_output: aggregated_output.clone(),
|
||||
aggregated_output,
|
||||
}
|
||||
};
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/app_server.rs
|
||||
expression: active
|
||||
---
|
||||
• Running printf 'stdout\nstderr\n'
|
||||
└ stdout
|
||||
stderr
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/app_server.rs
|
||||
expression: completed
|
||||
---
|
||||
$ printf 'stdout\nstderr\n'
|
||||
stdout
|
||||
stderr
|
||||
✗ (1) • <duration>
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/app_server.rs
|
||||
expression: lines_to_single_string(&transcript)
|
||||
---
|
||||
$ printf 'stdout\nstderr\n'
|
||||
stdout
|
||||
stderr
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user