From c47dd73349a326c1e39df8a71e5aa3dc8e32686d 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 421488346d..5b2cbf58fc 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 1e17d79a0a..995a230e14 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 f79c840195..67d9594bbf 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 d2fdd765e0..d9f1b2ced8 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