diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index c9621199e..e315f0d60 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -2938,6 +2938,12 @@ impl ChatWidget { fn finalize_turn(&mut self) { // Ensure any spinner is replaced by a red ✗ and flushed into history. self.finalize_active_cell_as_failed(); + // 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(); + } // Reset running state and clear streaming buffers. self.user_turn_pending_start = false; self.agent_turn_running = false; diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__interrupted_turn_clears_visible_running_hook.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__interrupted_turn_clears_visible_running_hook.snap new file mode 100644 index 000000000..22c70e335 --- /dev/null +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__interrupted_turn_clears_visible_running_hook.snap @@ -0,0 +1,8 @@ +--- +source: tui/src/chatwidget/tests/status_and_layout.rs +expression: "format!(\"before interrupt:\\n{before_interrupt}after interrupt:\\n{}\",\nactive_hook_blob(&chat))" +--- +before interrupt: +• Running PreToolUse hook: checking command policy +after interrupt: + 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 e91bb2885..73f0d3b7a 100644 --- a/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs +++ b/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs @@ -1331,6 +1331,32 @@ async fn status_line_branch_refreshes_after_interrupt() { assert!(chat.status_line_branch_pending); } +#[tokio::test] +async fn interrupted_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( + "pre-tool-use:0:/tmp/hooks.json", + codex_app_server_protocol::HookEventName::PreToolUse, + Some("checking command policy"), + ), + ); + reveal_running_hooks(&mut chat); + let before_interrupt = active_hook_blob(&chat); + + handle_turn_interrupted(&mut chat, "turn-1"); + + assert_chatwidget_snapshot!( + "interrupted_turn_clears_visible_running_hook", + format!( + "before interrupt:\n{before_interrupt}after interrupt:\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;