83. Busy-Input Disposition: Enter Queues, Ctrl+Enter Forks¶
- Status: accepted
- Date: 2026-10-26
Context¶
While an agent turn runs, typed input had one escape hatch: Ctrl+Enter/Ctrl+J
a background agent (ADR-0043) — great for a self-contained side
question, wrong for conversation continuation (corrections, "also do X",
next steps), which belongs to the live agent's next turn. Enter was a dead
end: a hint pointing at Ctrl+Enter. The design work (tech + UI, kept in
docs/internal/design/busy-queue-or-fork-*.md) gave both dispositions
first-class, one-keystroke reach.
Decision¶
- Enter = queue it for this agent's next turn.
queuePromptcaptures the text with its paste provenance and staged attachments (they were staged for that typed input) intointeractiveState.queuedPrompts(in-memory, cap 20 — refused with a hint past it, text kept in the box) and prints a receipt naming the review surface:⋯ queued for next turn — "<preview>" · /queue to review, with⋯ N lines/+ N attachmentsvariants. - Drain: one per turn, first.
handleAgentDone→drainQueuedPromptflushes the oldest entry before the background-followup and remote-input chain, delegating tosubmitInput(q.text, dim("[queued]")+" ")— the[telegram]label precedent — so history, the loop-detector feed and the issue-#151 paste gate replay correctly. The flushed turn is the next turn (drainBackgroundJobs rides along inside it), so both follow-up flags are cleared at flush and re-armed by any job that finishes later. FIFO order; survives Ctrl+C; dropped on session switch with a notice (like staged attachments). - Ctrl+Enter/Ctrl+J = fork, unchanged (ADR-0043). Slash commands are
never queued (non-busy-safe ones keep their own hint);
!bang runs immediately as before. - Surface:
/queue list|show <n>|drop <n|last|all>|clear(TUI-only, busy-safe), a» N queuedstatus-bar chip, and the flush echo's[queued]label so no turn answers an invisible prompt. - Escape hatch:
queue_while_busy: falserestores Enter's hint-only behavior.
Alternatives rejected (design §12)¶
A Ctrl+Enter fork/queue chooser (modal friction), Shift+Enter (newline binding + terminal mangling), flipping Ctrl+Enter's meaning (established/documented/tested), merging queued prompts into one turn, persisting the queue, queueing slash commands.
Consequences¶
- Enter-while-busy is now lossless by default: the receipt line is the proof
the input was not dropped,
/queue drop lastis a full undo until the turn ends — queueing is the cheap, reversible action and owns the default key. - ACP parity is a non-goal:
session/promptalready queues behind the session's turn mutex. - obs counters (
milk.prompt_queued) were listed as optional in the design and are not in this cut. - Historical note: ADR-0043's "Enter shows a hint" contract is amended by this record.
Amendment¶
- 2026-10-26 (two-step confirm + Ctrl+C undo). Queuing on the first
Enter made the fork alternative invisible — the user saw their input
vanish with only a receipt after the fact. Enter is now two-step: the
first press announces both choices in the busy hint (
Enter again to queue it for the next turn · Ctrl+Enter to spawn a background agent with this), the second press on the same gesture queues. Any other key disarms the confirm (it chains across consecutive Enters only), and a confirm never survives a turn boundary. Ctrl+C gains an undo stage first: with a non-empty queue it drops the last queued entry (one press per entry, notice +/queuepointer) before falling through to its usual chain (clear input → interrupt the turn → quit) — the receipt's counterpart, so queueing is reversible by keystroke alone.