Update header from Working once batched commands are done (#2249)
Update commands from Working to Complete or Failed after they're done before: <img width="725" height="332" alt="image" src="https://github.com/user-attachments/assets/fb93d21f-5c4a-42bc-a154-14f4fe99d5f9" /> after: <img width="464" height="65" alt="image" src="https://github.com/user-attachments/assets/15ec7c3b-355f-473e-9a8e-eab359ec5f0d" />
This commit is contained in:
@@ -76,6 +76,7 @@ pub(crate) struct ChatWidget<'a> {
|
|||||||
// Track the most recently active stream kind in the current turn
|
// Track the most recently active stream kind in the current turn
|
||||||
last_stream_kind: Option<StreamKind>,
|
last_stream_kind: Option<StreamKind>,
|
||||||
running_commands: HashMap<String, RunningCommand>,
|
running_commands: HashMap<String, RunningCommand>,
|
||||||
|
pending_exec_completions: Vec<(Vec<String>, Vec<ParsedCommand>, CommandOutput)>,
|
||||||
task_complete_pending: bool,
|
task_complete_pending: bool,
|
||||||
// Queue of interruptive UI events deferred during an active write cycle
|
// Queue of interruptive UI events deferred during an active write cycle
|
||||||
interrupts: InterruptManager,
|
interrupts: InterruptManager,
|
||||||
@@ -112,6 +113,10 @@ impl ChatWidget<'_> {
|
|||||||
fn mark_needs_redraw(&mut self) {
|
fn mark_needs_redraw(&mut self) {
|
||||||
self.needs_redraw = true;
|
self.needs_redraw = true;
|
||||||
}
|
}
|
||||||
|
fn flush_answer_stream_with_separator(&mut self) {
|
||||||
|
let sink = AppEventHistorySink(self.app_event_tx.clone());
|
||||||
|
let _ = self.stream.finalize(StreamKind::Answer, true, &sink);
|
||||||
|
}
|
||||||
// --- Small event handlers ---
|
// --- Small event handlers ---
|
||||||
fn on_session_configured(&mut self, event: codex_core::protocol::SessionConfiguredEvent) {
|
fn on_session_configured(&mut self, event: codex_core::protocol::SessionConfiguredEvent) {
|
||||||
self.bottom_pane
|
self.bottom_pane
|
||||||
@@ -215,6 +220,7 @@ impl ChatWidget<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn on_exec_command_begin(&mut self, ev: ExecCommandBeginEvent) {
|
fn on_exec_command_begin(&mut self, ev: ExecCommandBeginEvent) {
|
||||||
|
self.flush_answer_stream_with_separator();
|
||||||
let ev2 = ev.clone();
|
let ev2 = ev.clone();
|
||||||
self.defer_or_handle(|q| q.push_exec_begin(ev), |s| s.handle_exec_begin_now(ev2));
|
self.defer_or_handle(|q| q.push_exec_begin(ev), |s| s.handle_exec_begin_now(ev2));
|
||||||
}
|
}
|
||||||
@@ -344,12 +350,11 @@ impl ChatWidget<'_> {
|
|||||||
|
|
||||||
pub(crate) fn handle_exec_end_now(&mut self, ev: ExecCommandEndEvent) {
|
pub(crate) fn handle_exec_end_now(&mut self, ev: ExecCommandEndEvent) {
|
||||||
let running = self.running_commands.remove(&ev.call_id);
|
let running = self.running_commands.remove(&ev.call_id);
|
||||||
self.active_exec_cell = None;
|
|
||||||
let (command, parsed) = match running {
|
let (command, parsed) = match running {
|
||||||
Some(rc) => (rc.command, rc.parsed_cmd),
|
Some(rc) => (rc.command, rc.parsed_cmd),
|
||||||
None => (vec![ev.call_id.clone()], Vec::new()),
|
None => (vec![ev.call_id.clone()], Vec::new()),
|
||||||
};
|
};
|
||||||
self.add_to_history(HistoryCell::new_completed_exec_command(
|
self.pending_exec_completions.push((
|
||||||
command,
|
command,
|
||||||
parsed,
|
parsed,
|
||||||
CommandOutput {
|
CommandOutput {
|
||||||
@@ -358,6 +363,16 @@ impl ChatWidget<'_> {
|
|||||||
stderr: ev.stderr.clone(),
|
stderr: ev.stderr.clone(),
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
|
|
||||||
|
if self.running_commands.is_empty() {
|
||||||
|
self.active_exec_cell = None;
|
||||||
|
let pending = std::mem::take(&mut self.pending_exec_completions);
|
||||||
|
for (command, parsed, output) in pending {
|
||||||
|
self.add_to_history(HistoryCell::new_completed_exec_command(
|
||||||
|
command, parsed, output,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn handle_patch_apply_end_now(
|
pub(crate) fn handle_patch_apply_end_now(
|
||||||
@@ -372,6 +387,7 @@ impl ChatWidget<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn handle_exec_approval_now(&mut self, id: String, ev: ExecApprovalRequestEvent) {
|
pub(crate) fn handle_exec_approval_now(&mut self, id: String, ev: ExecApprovalRequestEvent) {
|
||||||
|
self.flush_answer_stream_with_separator();
|
||||||
// Log a background summary immediately so the history is chronological.
|
// Log a background summary immediately so the history is chronological.
|
||||||
let cmdline = strip_bash_lc_and_escape(&ev.command);
|
let cmdline = strip_bash_lc_and_escape(&ev.command);
|
||||||
let text = format!(
|
let text = format!(
|
||||||
@@ -398,6 +414,7 @@ impl ChatWidget<'_> {
|
|||||||
id: String,
|
id: String,
|
||||||
ev: ApplyPatchApprovalRequestEvent,
|
ev: ApplyPatchApprovalRequestEvent,
|
||||||
) {
|
) {
|
||||||
|
self.flush_answer_stream_with_separator();
|
||||||
self.add_to_history(HistoryCell::new_patch_event(
|
self.add_to_history(HistoryCell::new_patch_event(
|
||||||
PatchEventType::ApprovalRequest,
|
PatchEventType::ApprovalRequest,
|
||||||
ev.changes.clone(),
|
ev.changes.clone(),
|
||||||
@@ -423,16 +440,29 @@ impl ChatWidget<'_> {
|
|||||||
parsed_cmd: ev.parsed_cmd.clone(),
|
parsed_cmd: ev.parsed_cmd.clone(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
self.active_exec_cell = Some(HistoryCell::new_active_exec_command(
|
// Accumulate parsed commands into a single active Exec cell so they stack
|
||||||
ev.command,
|
match self.active_exec_cell.as_mut() {
|
||||||
ev.parsed_cmd,
|
Some(HistoryCell::Exec(exec)) => {
|
||||||
));
|
exec.parsed.extend(ev.parsed_cmd);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
self.active_exec_cell = Some(HistoryCell::new_active_exec_command(
|
||||||
|
ev.command,
|
||||||
|
ev.parsed_cmd,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request a redraw so the working header and command list are visible immediately.
|
||||||
|
self.mark_needs_redraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn handle_mcp_begin_now(&mut self, ev: McpToolCallBeginEvent) {
|
pub(crate) fn handle_mcp_begin_now(&mut self, ev: McpToolCallBeginEvent) {
|
||||||
|
self.flush_answer_stream_with_separator();
|
||||||
self.add_to_history(HistoryCell::new_active_mcp_tool_call(ev.invocation));
|
self.add_to_history(HistoryCell::new_active_mcp_tool_call(ev.invocation));
|
||||||
}
|
}
|
||||||
pub(crate) fn handle_mcp_end_now(&mut self, ev: McpToolCallEndEvent) {
|
pub(crate) fn handle_mcp_end_now(&mut self, ev: McpToolCallEndEvent) {
|
||||||
|
self.flush_answer_stream_with_separator();
|
||||||
self.add_to_history(HistoryCell::new_completed_mcp_tool_call(
|
self.add_to_history(HistoryCell::new_completed_mcp_tool_call(
|
||||||
80,
|
80,
|
||||||
ev.invocation,
|
ev.invocation,
|
||||||
@@ -494,6 +524,7 @@ impl ChatWidget<'_> {
|
|||||||
stream: StreamController::new(config),
|
stream: StreamController::new(config),
|
||||||
last_stream_kind: None,
|
last_stream_kind: None,
|
||||||
running_commands: HashMap::new(),
|
running_commands: HashMap::new(),
|
||||||
|
pending_exec_completions: Vec::new(),
|
||||||
task_complete_pending: false,
|
task_complete_pending: false,
|
||||||
interrupts: InterruptManager::new(),
|
interrupts: InterruptManager::new(),
|
||||||
needs_redraw: false,
|
needs_redraw: false,
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ use codex_core::protocol::AgentReasoningDeltaEvent;
|
|||||||
use codex_core::protocol::ApplyPatchApprovalRequestEvent;
|
use codex_core::protocol::ApplyPatchApprovalRequestEvent;
|
||||||
use codex_core::protocol::Event;
|
use codex_core::protocol::Event;
|
||||||
use codex_core::protocol::EventMsg;
|
use codex_core::protocol::EventMsg;
|
||||||
|
use codex_core::protocol::ExecCommandBeginEvent;
|
||||||
|
use codex_core::protocol::ExecCommandEndEvent;
|
||||||
use codex_core::protocol::FileChange;
|
use codex_core::protocol::FileChange;
|
||||||
use codex_core::protocol::PatchApplyBeginEvent;
|
use codex_core::protocol::PatchApplyBeginEvent;
|
||||||
use codex_core::protocol::PatchApplyEndEvent;
|
use codex_core::protocol::PatchApplyEndEvent;
|
||||||
@@ -134,6 +136,7 @@ fn make_chatwidget_manual() -> (
|
|||||||
stream: StreamController::new(cfg),
|
stream: StreamController::new(cfg),
|
||||||
last_stream_kind: None,
|
last_stream_kind: None,
|
||||||
running_commands: HashMap::new(),
|
running_commands: HashMap::new(),
|
||||||
|
pending_exec_completions: Vec::new(),
|
||||||
task_complete_pending: false,
|
task_complete_pending: false,
|
||||||
interrupts: InterruptManager::new(),
|
interrupts: InterruptManager::new(),
|
||||||
needs_redraw: false,
|
needs_redraw: false,
|
||||||
@@ -188,6 +191,90 @@ fn open_fixture(name: &str) -> std::fs::File {
|
|||||||
File::open(name).expect("open fixture file")
|
File::open(name).expect("open fixture file")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exec_history_cell_shows_working_then_completed() {
|
||||||
|
let (mut chat, rx, _op_rx) = make_chatwidget_manual();
|
||||||
|
|
||||||
|
// Begin command
|
||||||
|
chat.handle_codex_event(Event {
|
||||||
|
id: "call-1".into(),
|
||||||
|
msg: EventMsg::ExecCommandBegin(ExecCommandBeginEvent {
|
||||||
|
call_id: "call-1".into(),
|
||||||
|
command: vec!["bash".into(), "-lc".into(), "echo done".into()],
|
||||||
|
cwd: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
|
||||||
|
parsed_cmd: vec![codex_core::parse_command::ParsedCommand::Unknown {
|
||||||
|
cmd: vec!["echo".into(), "done".into()],
|
||||||
|
}],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
// End command successfully
|
||||||
|
chat.handle_codex_event(Event {
|
||||||
|
id: "call-1".into(),
|
||||||
|
msg: EventMsg::ExecCommandEnd(ExecCommandEndEvent {
|
||||||
|
call_id: "call-1".into(),
|
||||||
|
stdout: "done".into(),
|
||||||
|
stderr: String::new(),
|
||||||
|
exit_code: 0,
|
||||||
|
duration: std::time::Duration::from_millis(5),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
let cells = drain_insert_history(&rx);
|
||||||
|
assert_eq!(
|
||||||
|
cells.len(),
|
||||||
|
1,
|
||||||
|
"expected only the completed exec cell to be inserted into history"
|
||||||
|
);
|
||||||
|
let blob = lines_to_single_string(&cells[0]);
|
||||||
|
assert!(
|
||||||
|
blob.contains("Completed"),
|
||||||
|
"expected completed exec cell to show Completed header: {blob:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exec_history_cell_shows_working_then_failed() {
|
||||||
|
let (mut chat, rx, _op_rx) = make_chatwidget_manual();
|
||||||
|
|
||||||
|
// Begin command
|
||||||
|
chat.handle_codex_event(Event {
|
||||||
|
id: "call-2".into(),
|
||||||
|
msg: EventMsg::ExecCommandBegin(ExecCommandBeginEvent {
|
||||||
|
call_id: "call-2".into(),
|
||||||
|
command: vec!["bash".into(), "-lc".into(), "false".into()],
|
||||||
|
cwd: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
|
||||||
|
parsed_cmd: vec![codex_core::parse_command::ParsedCommand::Unknown {
|
||||||
|
cmd: vec!["false".into()],
|
||||||
|
}],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
// End command with failure
|
||||||
|
chat.handle_codex_event(Event {
|
||||||
|
id: "call-2".into(),
|
||||||
|
msg: EventMsg::ExecCommandEnd(ExecCommandEndEvent {
|
||||||
|
call_id: "call-2".into(),
|
||||||
|
stdout: String::new(),
|
||||||
|
stderr: "error".into(),
|
||||||
|
exit_code: 2,
|
||||||
|
duration: std::time::Duration::from_millis(7),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
let cells = drain_insert_history(&rx);
|
||||||
|
assert_eq!(
|
||||||
|
cells.len(),
|
||||||
|
1,
|
||||||
|
"expected only the completed exec cell to be inserted into history"
|
||||||
|
);
|
||||||
|
let blob = lines_to_single_string(&cells[0]);
|
||||||
|
assert!(
|
||||||
|
blob.contains("Failed (exit 2)"),
|
||||||
|
"expected completed exec cell to show Failed header with exit code: {blob:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "current_thread")]
|
#[tokio::test(flavor = "current_thread")]
|
||||||
async fn binary_size_transcript_matches_ideal_fixture() {
|
async fn binary_size_transcript_matches_ideal_fixture() {
|
||||||
let (mut chat, rx, _op_rx) = make_chatwidget_manual();
|
let (mut chat, rx, _op_rx) = make_chatwidget_manual();
|
||||||
|
|||||||
@@ -342,7 +342,11 @@ impl HistoryCell {
|
|||||||
parsed_commands: &[ParsedCommand],
|
parsed_commands: &[ParsedCommand],
|
||||||
output: Option<&CommandOutput>,
|
output: Option<&CommandOutput>,
|
||||||
) -> Vec<Line<'static>> {
|
) -> Vec<Line<'static>> {
|
||||||
let mut lines: Vec<Line> = vec![Line::from("⚙︎ Working")];
|
let mut lines: Vec<Line> = vec![match output {
|
||||||
|
None => Line::from("⚙︎ Working".magenta().bold()),
|
||||||
|
Some(o) if o.exit_code == 0 => Line::from("✓ Completed".green().bold()),
|
||||||
|
Some(o) => Line::from(format!("✗ Failed (exit {})", o.exit_code).red().bold()),
|
||||||
|
}];
|
||||||
|
|
||||||
for (i, parsed) in parsed_commands.iter().enumerate() {
|
for (i, parsed) in parsed_commands.iter().enumerate() {
|
||||||
let text = match parsed {
|
let text = match parsed {
|
||||||
|
|||||||
Reference in New Issue
Block a user