diff --git a/crates/tui/src/tui/ui.rs b/crates/tui/src/tui/ui.rs index 64e35f66..a812e8fa 100644 --- a/crates/tui/src/tui/ui.rs +++ b/crates/tui/src/tui/ui.rs @@ -1940,12 +1940,8 @@ async fn run_event_loop( } else { let tool_input = input; - if tool_name == "apply_patch" { - maybe_add_patch_preview(app, &tool_input); - } - - // Create approval request and show overlay - let request = ApprovalRequest::new( + push_approval_request_view( + app, &id, &tool_name, &description, @@ -1961,8 +1957,6 @@ async fn run_event_loop( "mode": app.mode.label(), }), ); - app.view_stack - .push(ApprovalView::new_for_locale(request, app.ui_locale)); app.status_message = Some(format!( "Approval required for '{tool_name}': {description}" )); @@ -6399,6 +6393,23 @@ async fn handle_view_events( Ok(false) } +fn push_approval_request_view( + app: &mut App, + id: &str, + tool_name: &str, + description: &str, + tool_input: &serde_json::Value, + approval_key: &str, +) { + if tool_name == "apply_patch" { + maybe_add_patch_preview(app, tool_input); + } + + let request = ApprovalRequest::new(id, tool_name, description, tool_input, approval_key); + app.view_stack + .push(ApprovalView::new_for_locale(request, app.ui_locale)); +} + struct ApprovalDecisionEvent { tool_id: String, tool_name: String, diff --git a/crates/tui/src/tui/ui/tests.rs b/crates/tui/src/tui/ui/tests.rs index 4f0baa5b..987984ef 100644 --- a/crates/tui/src/tui/ui/tests.rs +++ b/crates/tui/src/tui/ui/tests.rs @@ -5202,6 +5202,49 @@ fn message_complete_drain_preserves_thinking_when_thinking_complete_lost() { ); } +#[test] +fn approval_prompt_uses_event_input_after_message_complete_drain() { + let mut app = create_test_app(); + app.pending_tool_uses.push(( + "tool-1".to_string(), + "exec_shell".to_string(), + serde_json::json!({"command": "stale value from drained list"}), + )); + + // Mirror the old race: MessageComplete drains pending tool uses before + // ApprovalRequired is handled. The approval modal must still show the + // non-empty input carried directly on the ApprovalRequired event. + app.pending_tool_uses.clear(); + + let event_input = serde_json::json!({ + "command": "cargo test -p codewhale-tui approval", + "workdir": "/repo", + }); + push_approval_request_view( + &mut app, + "tool-1", + "exec_shell", + "Run cargo tests", + &event_input, + "approval-key", + ); + + let mut view = app.view_stack.pop().expect("approval view"); + let approval = view + .as_any_mut() + .downcast_mut::() + .expect("approval view"); + let action = approval.handle_key(KeyEvent::new(KeyCode::Char('v'), KeyModifiers::NONE)); + let ViewAction::Emit(ViewEvent::OpenTextPager { content, .. }) = action else { + panic!("expected approval params pager"); + }; + + assert!(content.contains("cargo test -p codewhale-tui approval")); + assert!(content.contains("/repo")); + assert!(!content.contains("stale value from drained list")); + assert_ne!(content.trim(), "{}"); +} + #[test] fn second_thinking_block_appends_new_entry_in_same_active_cell() { // Real V4 turns can emit Thinking → Tool → Thinking → Tool before any