Before submitting
Area
packages/contracts or packages/shared
Problem or use case
The keybindings system itself is in good shape — file-backed config, hot reload, when expressions, a settings UI. The limitation is that KeybindingCommand is a closed set (packages/contracts/src/keybindings.ts:50-71), and several everyday actions aren't in it. Each one is already implemented and dispatchable; it just has no command ID, so it stays mouse-only with no way to rebind it.
Six that I hit daily:
1. Interrupt a running turn — no keyboard path at all
interruptThreadTurn (packages/client-runtime/src/operations/commands.ts:245) is reachable only through the stop button's onInterrupt (ChatComposer.tsx:2737).
I grepped every Escape handler in apps/web/src — 13 of them, and all are dialogs, pickers, inline rename, or selection-clear. None interrupt a run. So stopping an agent requires the mouse, and there's no workaround.
This is the one that hurts most, and it's arguably a gap rather than a feature request. Escape-to-interrupt is close to universal in agent UIs, and reaching for the mouse to stop a run that's heading the wrong way is exactly the moment you don't want to.
2. Rename thread
{ id: "rename", label: "Rename thread" } (SidebarV2.tsx:2018). The inline editor already handles Enter and Escape (SidebarV2.tsx:579-594) — only the entry point is missing, and it's the right-click menu.
3. Settle / un-settle thread
SidebarV2.tsx:1996-2001, gated on the threadSettlement capability (state/entities.ts:200). Context menu only.
4. Focus terminal
There's no terminal.focus. Focus is coupled to visibility: opening the drawer bumps terminalFocusRequestId (ChatView.tsx:4218). If the drawer is already open and focus sits in the composer, there's no keyboard route into the terminal.
5. Focus composer
The mirror image. Composer focus fires on drawer close (ChatView.tsx:4222-4227) and on thread switch. Type-to-focus (ChatView.tsx:428) bails whenever focus is inside an input or textarea (ChatView.tsx:383-389) — xterm renders a textarea, so typing in the terminal never brings you back.
6. Environment / checkout picker
The "Current checkout" control (resolveEnvModeLabel, BranchToolbar.logic.ts:58) is a mouse-only menu (BranchToolbar.tsx:184-203). Neither BranchToolbar.tsx nor BranchToolbarBranchSelector.tsx has any keyboard handling. Choosing between current checkout and a new worktree is a decision made on nearly every new thread, and today it needs a pointer.
Proposed solution
Extend STATIC_KEYBINDING_COMMANDS and wire the dispatch, following existing naming:
| Command |
Action |
chat.interrupt |
interrupt the running turn |
thread.rename |
enter inline rename on the active thread |
thread.settle |
toggle settled, as the context menu does |
terminal.focus |
move focus into the terminal, opening it if closed |
chat.focusComposer |
move focus to the composer, leaving the terminal open |
env.togglePicker |
open the environment / checkout picker |
For chat.interrupt a default of escape with something like when: "!terminalFocus" seems right, since mod+K is already hardcoded as terminal-clear and commandPalette.toggle works around it the same way. Happy to ship it with no default binding if Escape feels too overloaded.
Why this matters
The keybinding system is already good enough that the closed command set is what's holding it back — the gap isn't infrastructure, it's coverage. Every action above is one an agent-driven workflow touches many times an hour, and each one currently breaks flow by forcing a reach for the mouse.
chat.interrupt in particular is a correctness-of-experience issue rather than a nicety: there is currently no way to stop a running agent from the keyboard.
Smallest useful scope
Just chat.interrupt. It's the only one with no workaround at all, and it's a small change — the command already exists, it needs a command ID and a dispatch branch.
Items 2, 3 and 6 are similarly mechanical: existing menu handlers, new command IDs.
Items 4 and 5 are the only ones with real design in them, since focus is currently coupled to panel visibility and would need decoupling. Fine to split those into a follow-up.
Alternatives considered
For focus terminal / focus composer there's a partial workaround: mod+J twice. terminal.toggle calls toggleTerminalVisibility() (ChatView.tsx:4267), which is visibility-only rather than lifecycle, so the terminal session survives the round trip. It works, but toggling a panel to move focus isn't a great story.
The other four have no workaround.
I also looked at whether any of this is reachable through the command palette instead — the thread actions are not; they live only in the sidebar context menu.
Risks or tradeoffs
- Escape is heavily overloaded. Binding
chat.interrupt to bare escape by default needs a when clause at minimum, and it may be safer to ship the command with no default and let users opt in.
thread.rename / thread.settle need a target. The context-menu versions act on the right-clicked thread; a keybinding would need to act on the active thread, which is a small semantic difference worth being deliberate about.
- Decoupling focus from visibility (items 4 and 5) touches existing effects in
ChatView.tsx that several other behaviors depend on. This is the main reason to treat it as a separate change.
thread.settle is capability-gated, so the command needs to no-op cleanly against pre-settlement servers.
Examples or references
Code references are against 23ea08d on main; the behavior was observed on the nightly build 0.0.29-nightly.20260727.915.
Adjacent open issues, none of which overlap with the actions above:
Contribution
Before submitting
Area
packages/contracts or packages/shared
Problem or use case
The keybindings system itself is in good shape — file-backed config, hot reload,
whenexpressions, a settings UI. The limitation is thatKeybindingCommandis a closed set (packages/contracts/src/keybindings.ts:50-71), and several everyday actions aren't in it. Each one is already implemented and dispatchable; it just has no command ID, so it stays mouse-only with no way to rebind it.Six that I hit daily:
1. Interrupt a running turn — no keyboard path at all
interruptThreadTurn(packages/client-runtime/src/operations/commands.ts:245) is reachable only through the stop button'sonInterrupt(ChatComposer.tsx:2737).I grepped every
Escapehandler inapps/web/src— 13 of them, and all are dialogs, pickers, inline rename, or selection-clear. None interrupt a run. So stopping an agent requires the mouse, and there's no workaround.This is the one that hurts most, and it's arguably a gap rather than a feature request. Escape-to-interrupt is close to universal in agent UIs, and reaching for the mouse to stop a run that's heading the wrong way is exactly the moment you don't want to.
2. Rename thread
{ id: "rename", label: "Rename thread" }(SidebarV2.tsx:2018). The inline editor already handles Enter and Escape (SidebarV2.tsx:579-594) — only the entry point is missing, and it's the right-click menu.3. Settle / un-settle thread
SidebarV2.tsx:1996-2001, gated on thethreadSettlementcapability (state/entities.ts:200). Context menu only.4. Focus terminal
There's no
terminal.focus. Focus is coupled to visibility: opening the drawer bumpsterminalFocusRequestId(ChatView.tsx:4218). If the drawer is already open and focus sits in the composer, there's no keyboard route into the terminal.5. Focus composer
The mirror image. Composer focus fires on drawer close (
ChatView.tsx:4222-4227) and on thread switch. Type-to-focus (ChatView.tsx:428) bails whenever focus is inside aninputortextarea(ChatView.tsx:383-389) — xterm renders a textarea, so typing in the terminal never brings you back.6. Environment / checkout picker
The "Current checkout" control (
resolveEnvModeLabel,BranchToolbar.logic.ts:58) is a mouse-only menu (BranchToolbar.tsx:184-203). NeitherBranchToolbar.tsxnorBranchToolbarBranchSelector.tsxhas any keyboard handling. Choosing between current checkout and a new worktree is a decision made on nearly every new thread, and today it needs a pointer.Proposed solution
Extend
STATIC_KEYBINDING_COMMANDSand wire the dispatch, following existing naming:chat.interruptthread.renamethread.settleterminal.focuschat.focusComposerenv.togglePickerFor
chat.interrupta default ofescapewith something likewhen: "!terminalFocus"seems right, sincemod+Kis already hardcoded as terminal-clear andcommandPalette.toggleworks around it the same way. Happy to ship it with no default binding if Escape feels too overloaded.Why this matters
The keybinding system is already good enough that the closed command set is what's holding it back — the gap isn't infrastructure, it's coverage. Every action above is one an agent-driven workflow touches many times an hour, and each one currently breaks flow by forcing a reach for the mouse.
chat.interruptin particular is a correctness-of-experience issue rather than a nicety: there is currently no way to stop a running agent from the keyboard.Smallest useful scope
Just
chat.interrupt. It's the only one with no workaround at all, and it's a small change — the command already exists, it needs a command ID and a dispatch branch.Items 2, 3 and 6 are similarly mechanical: existing menu handlers, new command IDs.
Items 4 and 5 are the only ones with real design in them, since focus is currently coupled to panel visibility and would need decoupling. Fine to split those into a follow-up.
Alternatives considered
For focus terminal / focus composer there's a partial workaround:
mod+Jtwice.terminal.togglecallstoggleTerminalVisibility()(ChatView.tsx:4267), which is visibility-only rather than lifecycle, so the terminal session survives the round trip. It works, but toggling a panel to move focus isn't a great story.The other four have no workaround.
I also looked at whether any of this is reachable through the command palette instead — the thread actions are not; they live only in the sidebar context menu.
Risks or tradeoffs
chat.interruptto bareescapeby default needs awhenclause at minimum, and it may be safer to ship the command with no default and let users opt in.thread.rename/thread.settleneed a target. The context-menu versions act on the right-clicked thread; a keybinding would need to act on the active thread, which is a small semantic difference worth being deliberate about.ChatView.tsxthat several other behaviors depend on. This is the main reason to treat it as a separate change.thread.settleis capability-gated, so the command needs to no-op cleanly against pre-settlement servers.Examples or references
Code references are against
23ea08donmain; the behavior was observed on the nightly build0.0.29-nightly.20260727.915.Adjacent open issues, none of which overlap with the actions above:
Contribution