feat(web): stop the running turn from the keyboard via chat.interrupt - #5959
feat(web): stop the running turn from the keyboard via chat.interrupt#5959achtan wants to merge 2 commits into
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
ApprovabilityVerdict: Needs human review This PR introduces a new user-facing keyboard shortcut feature ( You can customize Macroscope's approvability policy. Learn more. |
Summary
There is currently no keyboard path to stop a running turn —
interruptThreadTurnis reachable only through the composer's stop button, so interrupting an agent that's heading the wrong way requires the mouse. This is item 1 ("smallest useful scope") of #4641.This PR adds
chat.interruptas a bindable keybinding command that does exactly what the stop button does.Changes
chat.interruptadded toSTATIC_KEYBINDING_COMMANDS(packages/contracts/src/keybindings.ts). The list is now exported so the settings UI can enumerate bindable commands. Old clients drop the unknown command via the existingForwardCompatibleArraydecode, so nothing breaks across versions.ChatView.tsx): achat.interruptbranch in the existing keydown handler that calls the sameonInterruptthe stop button uses (onInterruptis now auseCallbackso the effect can depend on it). The command claims the key only while a turn is running (phase === "running", mirroring the stop button); otherwise the event falls through untouched. While a turn is running, the command is resolved ahead of the type-to-focus fallback so a printable-key binding interrupts instead of being typed into the composer; type-to-focus precedence for every other command is unchanged.docs/user/keybindings.mddocuments the command.No default shortcut, deliberately
The issue proposes
escapewithwhen: "!terminalFocus"but also offers shipping unbound if Escape feels too overloaded — and it is: the dispatch handler runs in window capture phase, ahead of every dialog, picker, and composer menu that closes on Escape, so a default Escape binding would swallow those during a running turn. Shipping the command unbound lets users opt in ({ "key": "esc", "command": "chat.interrupt" }works fine for those who want it), and the running-turn gate means even a bare-Escape binding keeps its normal meaning while idle. A default can be added later as a one-line change if the maintainers want one.Relationship to other open PRs
#5669 (
thread.interrupt, default Escape, stacked on #5668) and #4308 (thread.stop, unbound) address the same gap. This PR implements item 1 of the tracking issue #4641 under the command name specced there (chat.interrupt, matching the existingchat.new/chat.newLocalnamespace for composer-scoped actions). Beyond naming, it differs in resolving the command ahead of the type-to-focus fallback while a turn runs, and in the no-default rationale above being grounded in the capture-phase handler analysis. No objection from my side if the maintainers prefer to land one of the others instead — the gap getting closed matters more than whose diff does it.Testing
vp test runonpackages/contracts/src/keybindings.test.ts,apps/web/src/components/settings/KeybindingsSettings.logic.test.ts,apps/web/src/keybindings.test.ts,apps/server/src/keybindings.test.ts— 86 passed. New assertions:chat.interruptdecodes as a validKeybindingRulecommand; the settings command options include it without any binding present.tsgo --noEmitclean for@t3tools/contractsand@t3tools/web;vp lintclean on touched files.escviakeybindings.json, hot reload picked it up, pressing Escape during a running turn dispatchedchat.interruptand the turn stopped. Interrupt-to-stop latency matched the mouse stop button (both route through the sameinterruptThreadTurn).Proof of work
Settings → Keybindings listing the
esc→ Chat: Interrupt binding sourced fromkeybindings.json(Custom):The add-keybinding command dropdown offering Chat: Interrupt (before this PR the dropdown only listed commands that have a default binding, so an unbound command could not be selected;
chat.interruptdoes not exist on main at all):Video of Escape stopping a live running turn (badge overlay marks the key press; the stop takes a few seconds — that is provider interrupt latency, identical for the mouse stop button): esc-interrupts-running-turn.webm
Related to #4641 (item 1 of the tracking list — does not close it).
Work done by Claude Fable 5 via Claude Agent SDK.
Note
Low Risk
User-triggered interrupt reuses the existing stop-button path and only overrides keys during an active turn; no auth or data-layer changes.
Overview
Adds a bindable
chat.interruptcommand so users can stop an in-flight agent turn from the keyboard, matching the composer stop button. There is no default shortcut; users assign one in **Settings → Keybindings.In
ChatView, shortcut resolution runs before type-to-focus so a bound printable key stops the turn instead of typing into the composer. The binding only claims the key whilephase === "running"; when idle, the key behaves normally (e.g. Escape still closes dialogs).onInterruptis auseCallbackwired into the existing keydown effect.Contracts export
STATIC_KEYBINDING_COMMANDSand includechat.interrupt. The settings command picker seeds from that list so commands without default bindings stay bindable. User docs describe the command and running-turn behavior.Reviewed by Cursor Bugbot for commit 1535189. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add
chat.interruptkeybinding to stop a running thread turn from the keyboardchat.interruptcommand that interrupts the active thread turn when pressed while a turn is running; when idle, the key is not claimed and falls through to other handlers (including type-to-focus).STATIC_KEYBINDING_COMMANDSin keybindings.ts now exportschat.interrupt, and KeybindingsSettings.logic.ts seeds the options list from this constant so commands without default bindings are still shown.Macroscope summarized 1535189.