Skip to content

feat(web): stop the running turn from the keyboard via chat.interrupt - #5959

Open
achtan wants to merge 2 commits into
pingdotgg:mainfrom
achtan:feat/chat-interrupt-keybinding
Open

feat(web): stop the running turn from the keyboard via chat.interrupt#5959
achtan wants to merge 2 commits into
pingdotgg:mainfrom
achtan:feat/chat-interrupt-keybinding

Conversation

@achtan

@achtan achtan commented Aug 10, 2026

Copy link
Copy Markdown

Summary

There is currently no keyboard path to stop a running turn — interruptThreadTurn is 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.interrupt as a bindable keybinding command that does exactly what the stop button does.

Changes

  • Contracts: chat.interrupt added to STATIC_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 existing ForwardCompatibleArray decode, so nothing breaks across versions.
  • Dispatch (ChatView.tsx): a chat.interrupt branch in the existing keydown handler that calls the same onInterrupt the stop button uses (onInterrupt is now a useCallback so 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.
  • Settings UI: the command dropdown now includes all static commands, not just ones derived from default bindings — otherwise a command shipped without a default would be unbindable from the UI.
  • Docs: docs/user/keybindings.md documents the command.

No default shortcut, deliberately

The issue proposes escape with when: "!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 existing chat.new / chat.newLocal namespace 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 run on packages/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.interrupt decodes as a valid KeybindingRule command; the settings command options include it without any binding present.
  • tsgo --noEmit clean for @t3tools/contracts and @t3tools/web; vp lint clean on touched files.
  • Verified in a live client (isolated dev environment, real Claude provider turn): bound esc via keybindings.json, hot reload picked it up, pressing Escape during a running turn dispatched chat.interrupt and the turn stopped. Interrupt-to-stop latency matched the mouse stop button (both route through the same interruptThreadTurn).

Proof of work

Settings → Keybindings listing the esc → Chat: Interrupt binding sourced from keybindings.json (Custom):

keybindings rows

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.interrupt does not exist on main at all):

keybindings dropdown

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.interrupt command 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 while phase === "running"; when idle, the key behaves normally (e.g. Escape still closes dialogs). onInterrupt is a useCallback wired into the existing keydown effect.

Contracts export STATIC_KEYBINDING_COMMANDS and include chat.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.interrupt keybinding to stop a running thread turn from the keyboard

  • Adds a chat.interrupt command 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).
  • The command has no default binding but appears in the Keybindings settings UI so users can assign a shortcut.
  • STATIC_KEYBINDING_COMMANDS in keybindings.ts now exports chat.interrupt, and KeybindingsSettings.logic.ts seeds the options list from this constant so commands without default bindings are still shown.
  • Documents the new command in keybindings.md.

Macroscope summarized 1535189.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 886efaed-38cc-4fbd-8d44-6c4f002d2305

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Aug 10, 2026
Comment thread apps/web/src/components/ChatView.tsx
@macroscopeapp

macroscopeapp Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR introduces a new user-facing keyboard shortcut feature (chat.interrupt) with new event handling logic that modifies keyboard priority when a turn is running. New features with user-facing behavior changes warrant human review.

You can customize Macroscope's approvability policy. Learn more.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M 30-99 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant