You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Thread control is mouse-only today. There's no keyboard way to stop a running turn, edit a queued follow-up, or rewind/edit a sent message. Worse, the Stop button is hidden whenever you've typed a draft (the submit slot shows Send instead), so you can't even stop-by-mouse without clearing what you typed.
We want a "terminal-style" feel, but the underlying agents (Codex, Claude Code) don't behave identically, so we shouldn't blindly bind keys to whatever each harness does natively. This issue proposes which gestures are worth a key now and which aren't, and asks for agreement on one shared decision before anyone wires bindings.
The test each gesture has to pass
A gesture only deserves a key if it's:
Consistent across agents — the same keypress produces the same user-visible state transition on Codex and Claude Code.
Coherent in bb's state machine — there's a clear target and the action is legal in the state you'd press it.
Per-gesture verdict
Interrupt — ship it. Both providers stop a running turn with the same observable result (turn ends, partial output kept, next turn resumes); only cost differs (Codex keeps the session warm, Claude Code respawns). Fills the real "can't stop with a draft typed" gap. → bare ESC while a turn is running = interrupt.
Edit a queued message — ship it. Queued messages are a bb server concept with zero provider branching — editing one never touches the agent, behaves identically everywhere, and is available mid-run (which is exactly when queued messages exist). Do it in-place (reuse the existing inline editor + the expectedUpdatedAt-guarded update, which is safe if the message flushes mid-edit). Give it its own keyboard entry — queue-row focus + Enter — not ESC.
Rewind / edit a sent message — defer. This one fails both tests today: on Claude Code a turn with no recorded checkpoint hard-errors (409), while Codex never does — so an identical gesture would be dark on some turns and not others. And bb has no timeline keyboard-focus model, so there's no message for a key to target, plus editing a sent message requires the thread to be idle. Making it real is a mini-project (a focus/navigation model, "why unavailable" feedback, and a decision on live-vs-idle semantics), not a binding. Out of scope here.
The one decision to ratify
Bare ESC = interrupt. Queue-editing gets its own entry (row focus + Enter). We do not overload ESC across all three gestures.
The trap we're avoiding: if ESC means "interrupt" when the queue is empty but "edit last queued" when it isn't, its meaning silently flips on invisible state. Pick one meaning for bare ESC and give the others distinct entries.
Non-goals
No double-ESC / key-sequence infrastructure (the binding system is single-key + modifiers only).
No timeline focus/navigation model.
Rewind/edit-sent keyboard support — deferred to its own issue.
Implementation notes (for the interrupt PR)
Binding registry is single base-key + flat modifiers, no sequences: packages/domain/src/app-keybindings.ts (appShortcutSchema, APP_COMMAND_IDS). Dispatch: apps/app/src/components/commands/AppCommandProvider.tsx. There's no command id for stop/steer/edit today — all three are mouse-only.
Interrupt path already exists end to end: stopThread → POST /v1/threads/:id/stop → daemon thread.stop → provider-native turn/interrupt (Codex) / session abort (Claude Code). UI entry: ThreadDetailPromptArea.tsxhandleStopThread. Add a thread.interrupt command id + a threadRunning context key + a window-level ESC handler gated on running & not-editable-focus. Note the emergent double-ESC-to-stop: first ESC blurs the composer, second arrives unfocused → interrupts, with no sequence infra.
ESC is currently ad-hoc in component handlers (dismiss typeahead → blur composer → cancel voice: PromptBoxInternal.tsx), not in the registry — precedence must be specified so interrupt slots in after those.
Queued edit: reuse beginEditQueuedMessage (useInlineQueuedMessageEditing.ts) + updateQueuedMessage (thread-runtime-mutations.ts); target queuedMessages[len-1]. Current ESC in the queue collapses the drawer only when the drag handle is focused (QueuedMessagesList.tsx), so composer-focused ESC is free.
Capability-unavailable states are surfaced today by absence (control not rendered), no tooltip — keyboard triggers will need explicit "why unavailable" feedback.
The gap
Thread control is mouse-only today. There's no keyboard way to stop a running turn, edit a queued follow-up, or rewind/edit a sent message. Worse, the Stop button is hidden whenever you've typed a draft (the submit slot shows Send instead), so you can't even stop-by-mouse without clearing what you typed.
We want a "terminal-style" feel, but the underlying agents (Codex, Claude Code) don't behave identically, so we shouldn't blindly bind keys to whatever each harness does natively. This issue proposes which gestures are worth a key now and which aren't, and asks for agreement on one shared decision before anyone wires bindings.
The test each gesture has to pass
A gesture only deserves a key if it's:
Per-gesture verdict
Interrupt — ship it. Both providers stop a running turn with the same observable result (turn ends, partial output kept, next turn resumes); only cost differs (Codex keeps the session warm, Claude Code respawns). Fills the real "can't stop with a draft typed" gap. → bare ESC while a turn is running = interrupt.
Edit a queued message — ship it. Queued messages are a bb server concept with zero provider branching — editing one never touches the agent, behaves identically everywhere, and is available mid-run (which is exactly when queued messages exist). Do it in-place (reuse the existing inline editor + the
expectedUpdatedAt-guarded update, which is safe if the message flushes mid-edit). Give it its own keyboard entry — queue-row focus + Enter — not ESC.Rewind / edit a sent message — defer. This one fails both tests today: on Claude Code a turn with no recorded checkpoint hard-errors (409), while Codex never does — so an identical gesture would be dark on some turns and not others. And bb has no timeline keyboard-focus model, so there's no message for a key to target, plus editing a sent message requires the thread to be idle. Making it real is a mini-project (a focus/navigation model, "why unavailable" feedback, and a decision on live-vs-idle semantics), not a binding. Out of scope here.
The one decision to ratify
Bare ESC = interrupt. Queue-editing gets its own entry (row focus + Enter). We do not overload ESC across all three gestures.
The trap we're avoiding: if ESC means "interrupt" when the queue is empty but "edit last queued" when it isn't, its meaning silently flips on invisible state. Pick one meaning for bare ESC and give the others distinct entries.
Non-goals
Implementation notes (for the interrupt PR)
packages/domain/src/app-keybindings.ts(appShortcutSchema,APP_COMMAND_IDS). Dispatch:apps/app/src/components/commands/AppCommandProvider.tsx. There's no command id for stop/steer/edit today — all three are mouse-only.stopThread→POST /v1/threads/:id/stop→ daemonthread.stop→ provider-nativeturn/interrupt(Codex) / session abort (Claude Code). UI entry:ThreadDetailPromptArea.tsxhandleStopThread. Add athread.interruptcommand id + athreadRunningcontext key + a window-level ESC handler gated on running & not-editable-focus. Note the emergent double-ESC-to-stop: first ESC blurs the composer, second arrives unfocused → interrupts, with no sequence infra.PromptBoxInternal.tsx), not in the registry — precedence must be specified so interrupt slots in after those.beginEditQueuedMessage(useInlineQueuedMessageEditing.ts) +updateQueuedMessage(thread-runtime-mutations.ts); targetqueuedMessages[len-1]. Current ESC in the queue collapses the drawer only when the drag handle is focused (QueuedMessagesList.tsx), so composer-focused ESC is free.jpham/modsis behind Default-enable sent-message editing #1351/Improve experimental message editing #1378 (the edit-message hardening). Interrupt is independent of that, but rebase before picking up rewind.