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.
This commit is contained in:
Richard Feldman authored and GitHub committed 2026-01-08 22:39:06 -05:00
1 parent 29d2c12f3e
commit c47dd73349
4 files changed
+142 -2

No files matched your search

+1 -1
View File
@@ -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]
+137
View File
@@ -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<AcpThreadView>,
thread: Entity<AcpThread>,
message_editor: Entity<MessageEditor>,
}
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);
+1 -1
View File
@@ -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
+3
View File
@@ -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