Skip to content

Keyboard control for running threads: interrupt + edit-queued now, rewind later #1585

Description

@jshph

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:

  1. Consistent across agents — the same keypress produces the same user-visible state transition on Codex and Claude Code.
  2. 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: stopThreadPOST /v1/threads/:id/stop → daemon thread.stop → provider-native turn/interrupt (Codex) / session abort (Claude Code). UI entry: ThreadDetailPromptArea.tsx handleStopThread. 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.
  • Branch note: jpham/mods is 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.

AGENT GENERATED: by Claude Opus 4.8

Metadata

Metadata

Assignees

No one assigned

    Labels

    threadsTurns, timeline, messaging, forks

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions