From b4d69db5240c51c2c5c38b28ef0588508ab28463 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Thu, 8 Jan 2026 22:39:06 -0500 Subject: [PATCH] Allow Escape to interrupt agent thread from conversation focus (#46410) When focus is on the conversation part of the agent panel (not the message editor), pressing Escape now interrupts the running thread. Previously, Escape only worked when the message editor had focus. Release Notes: - Pressing Esc when the agent panel is focused now interrupts the running thread even if the text input box is not specifically focused. --- crates/agent_ui/Cargo.toml | 2 +- crates/agent_ui/src/acp/thread_view.rs | 137 +++++++++++++++++++++++++ crates/recent_projects/Cargo.toml | 2 +- crates/rules_library/Cargo.toml | 3 + 4 files changed, 142 insertions(+), 2 deletions(-) diff --git a/crates/agent_ui/Cargo.toml b/crates/agent_ui/Cargo.toml index 421488346d4b98505190664a531c90565e705ec2..5b2cbf58fcfb82452b1555702a2e33f95c5e082e 100644 --- a/crates/agent_ui/Cargo.toml +++ b/crates/agent_ui/Cargo.toml @@ -13,7 +13,7 @@ path = "src/agent_ui.rs" doctest = false [features] -test-support = ["assistant_text_thread/test-support", "acp_thread/test-support", "eval_utils", "gpui/test-support", "language/test-support", "reqwest_client", "workspace/test-support", "agent/test-support"] +test-support = ["assistant_text_thread/test-support", "acp_thread/test-support", "eval_utils", "gpui/test-support", "language/test-support", "reqwest_client", "workspace/test-support", "agent/test-support", "rules_library/test-support"] unit-eval = [] [dependencies] diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 1e17d79a0af7fe9465185068fa24e9944225852e..995a230e146288ba94cf991f7118f930ec9f55a1 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -6903,6 +6903,9 @@ impl Render for AcpThreadView { v_flex() .size_full() .key_context("AcpThread") + .on_action(cx.listener(|this, _: &menu::Cancel, _, cx| { + this.cancel_generation(cx); + })) .on_action(cx.listener(Self::toggle_burn_mode)) .on_action(cx.listener(Self::keep_all)) .on_action(cx.listener(Self::reject_all)) @@ -8224,6 +8227,140 @@ pub(crate) mod tests { }); } + struct GeneratingThreadSetup { + thread_view: Entity, + thread: Entity, + message_editor: Entity, + } + + async fn setup_generating_thread( + cx: &mut TestAppContext, + ) -> (GeneratingThreadSetup, &mut VisualTestContext) { + let connection = StubAgentConnection::new(); + + let (thread_view, cx) = + setup_thread_view(StubAgentServer::new(connection.clone()), cx).await; + add_to_workspace(thread_view.clone(), cx); + + let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone()); + message_editor.update_in(cx, |editor, window, cx| { + editor.set_text("Hello", window, cx); + }); + thread_view.update_in(cx, |thread_view, window, cx| { + thread_view.send(window, cx); + }); + + let (thread, session_id) = thread_view.read_with(cx, |view, cx| { + let thread = view.thread().unwrap(); + (thread.clone(), thread.read(cx).session_id().clone()) + }); + + cx.run_until_parked(); + + cx.update(|_, cx| { + connection.send_update( + session_id.clone(), + acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new( + "Response chunk".into(), + )), + cx, + ); + }); + + cx.run_until_parked(); + + thread.read_with(cx, |thread, _cx| { + assert_eq!(thread.status(), ThreadStatus::Generating); + }); + + ( + GeneratingThreadSetup { + thread_view, + thread, + message_editor, + }, + cx, + ) + } + + #[gpui::test] + async fn test_escape_cancels_generation_from_conversation_focus(cx: &mut TestAppContext) { + init_test(cx); + + let (setup, cx) = setup_generating_thread(cx).await; + + let focus_handle = setup + .thread_view + .read_with(cx, |view, _cx| view.focus_handle.clone()); + cx.update(|window, cx| { + window.focus(&focus_handle, cx); + }); + + setup.thread_view.update_in(cx, |_, window, cx| { + window.dispatch_action(menu::Cancel.boxed_clone(), cx); + }); + + cx.run_until_parked(); + + setup.thread.read_with(cx, |thread, _cx| { + assert_eq!(thread.status(), ThreadStatus::Idle); + }); + } + + #[gpui::test] + async fn test_escape_cancels_generation_from_editor_focus(cx: &mut TestAppContext) { + init_test(cx); + + let (setup, cx) = setup_generating_thread(cx).await; + + let editor_focus_handle = setup + .message_editor + .read_with(cx, |editor, cx| editor.focus_handle(cx)); + cx.update(|window, cx| { + window.focus(&editor_focus_handle, cx); + }); + + setup.message_editor.update_in(cx, |_, window, cx| { + window.dispatch_action(editor::actions::Cancel.boxed_clone(), cx); + }); + + cx.run_until_parked(); + + setup.thread.read_with(cx, |thread, _cx| { + assert_eq!(thread.status(), ThreadStatus::Idle); + }); + } + + #[gpui::test] + async fn test_escape_when_idle_is_noop(cx: &mut TestAppContext) { + init_test(cx); + + let (thread_view, cx) = + setup_thread_view(StubAgentServer::new(StubAgentConnection::new()), cx).await; + add_to_workspace(thread_view.clone(), cx); + + let thread = thread_view.read_with(cx, |view, _cx| view.thread().unwrap().clone()); + + thread.read_with(cx, |thread, _cx| { + assert_eq!(thread.status(), ThreadStatus::Idle); + }); + + let focus_handle = thread_view.read_with(cx, |view, _cx| view.focus_handle.clone()); + cx.update(|window, cx| { + window.focus(&focus_handle, cx); + }); + + thread_view.update_in(cx, |_, window, cx| { + window.dispatch_action(menu::Cancel.boxed_clone(), cx); + }); + + cx.run_until_parked(); + + thread.read_with(cx, |thread, _cx| { + assert_eq!(thread.status(), ThreadStatus::Idle); + }); + } + #[gpui::test] async fn test_interrupt(cx: &mut TestAppContext) { init_test(cx); diff --git a/crates/recent_projects/Cargo.toml b/crates/recent_projects/Cargo.toml index f79c84019500af21de23823af68073608e57d3e5..67d9594bbf48b7740a97fe18c32e92fba19ce5e5 100644 --- a/crates/recent_projects/Cargo.toml +++ b/crates/recent_projects/Cargo.toml @@ -14,7 +14,7 @@ doctest = false [features] default = [] -test-support = ["remote/test-support"] +test-support = ["remote/test-support", "project/test-support", "workspace/test-support"] [dependencies] anyhow.workspace = true diff --git a/crates/rules_library/Cargo.toml b/crates/rules_library/Cargo.toml index d2fdd765e044181ecb16535076fd31175ddb87c9..d9f1b2ced8026a50052cc84172733a498b088a17 100644 --- a/crates/rules_library/Cargo.toml +++ b/crates/rules_library/Cargo.toml @@ -11,6 +11,9 @@ workspace = true [lib] path = "src/rules_library.rs" +[features] +test-support = ["title_bar/test-support"] + [dependencies] anyhow.workspace = true collections.workspace = true