diff --git a/codex-rs/tui/src/chatwidget/hook_lifecycle.rs b/codex-rs/tui/src/chatwidget/hook_lifecycle.rs index 6545d839a..04fa87f7d 100644 --- a/codex-rs/tui/src/chatwidget/hook_lifecycle.rs +++ b/codex-rs/tui/src/chatwidget/hook_lifecycle.rs @@ -6,6 +6,13 @@ use super::*; impl ChatWidget { + /// Drop transient live hook status without flushing it into history. + pub(super) fn clear_active_hook_cell(&mut self) { + if self.active_hook_cell.take().is_some() { + self.bump_active_cell_revision(); + } + } + pub(super) fn on_hook_started(&mut self, run: codex_app_server_protocol::HookRunSummary) { self.record_visible_turn_activity(); self.flush_answer_stream_with_separator(); diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__completed_turn_clears_visible_running_hook.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__completed_turn_clears_visible_running_hook.snap new file mode 100644 index 000000000..e41954e41 --- /dev/null +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__completed_turn_clears_visible_running_hook.snap @@ -0,0 +1,8 @@ +--- +source: tui/src/chatwidget/tests/status_and_layout.rs +expression: "format!(\"before completion:\\n{before_completion}after completion:\\n{}\",\nactive_hook_blob(&chat))" +--- +before completion: +• Running PostToolUse hook +after completion: + diff --git a/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs b/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs index b7cd47683..62aede458 100644 --- a/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs +++ b/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs @@ -2173,6 +2173,32 @@ async fn interrupted_turn_clears_visible_running_hook() { ); } +#[tokio::test] +async fn completed_turn_clears_visible_running_hook() { + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + + handle_hook_started( + &mut chat, + hook_started_run( + "post-tool-use:0:/tmp/hooks.json", + codex_app_server_protocol::HookEventName::PostToolUse, + /*status_message*/ None, + ), + ); + reveal_running_hooks(&mut chat); + let before_completion = active_hook_blob(&chat); + + handle_turn_completed(&mut chat, "turn-1", /*duration_ms*/ None); + + assert_chatwidget_snapshot!( + "completed_turn_clears_visible_running_hook", + format!( + "before completion:\n{before_completion}after completion:\n{}", + active_hook_blob(&chat) + ) + ); +} + #[tokio::test] async fn status_line_fast_mode_renders_on_and_off() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; diff --git a/codex-rs/tui/src/chatwidget/turn_runtime.rs b/codex-rs/tui/src/chatwidget/turn_runtime.rs index 24674d710..3e413c82a 100644 --- a/codex-rs/tui/src/chatwidget/turn_runtime.rs +++ b/codex-rs/tui/src/chatwidget/turn_runtime.rs @@ -59,9 +59,7 @@ impl ChatWidget { self.quit_shortcut_key = None; self.update_task_running_state(); self.status_state.retry_status_header = None; - if self.active_hook_cell.take().is_some() { - self.bump_active_cell_revision(); - } + self.clear_active_hook_cell(); self.status_state.pending_status_indicator_restore = false; self.bottom_pane .set_interrupt_hint_visible(/*visible*/ true); @@ -162,6 +160,7 @@ impl ChatWidget { // Mark task stopped and request redraw now that all content is in history. self.status_state.pending_status_indicator_restore = false; self.input_queue.user_turn_pending_start = false; + self.clear_active_hook_cell(); self.turn_lifecycle.finish(); self.update_task_running_state(); self.running_commands.clear(); @@ -302,9 +301,7 @@ impl ChatWidget { // Turn-scoped hook rows are transient live state; once the turn is over, // do not leave an orphaned running row behind if no matching completion // event arrived before cancellation. - if self.active_hook_cell.take().is_some() { - self.bump_active_cell_revision(); - } + self.clear_active_hook_cell(); // Reset running state and clear streaming buffers. self.input_queue.user_turn_pending_start = false; self.turn_lifecycle.finish();