Problem
Beyond plain send, the composer offers three turn-control actions while a Turn is running: queue a message for the next Turn, take a queued message back to edit it, and interrupt. Reaching them is harder than it should be.
They are hard to discover. Nothing in the composer suggests they exist. The pending bar appears only after a successful queue, so a user has to already know the chord to find out the feature is there. The one place the chords are written down is /help, whose list lives in tui-primary-guidance.ts and is maintained separately from the 21 hardcoded matchesKey call sites in pi-tui-runner.ts — two hand-kept copies with nothing holding them in agreement.
They are awkward to press. Both live on Alt chords:
// packages/cli/src/pi-tui-runner.ts
if (matchesKey(data, Key.alt('enter')) && !isKeyRepeat(data)) { handleAltEnter(); } // queue
if (matchesKey(data, Key.alt('up')) && !isKeyRepeat(data)) { retractQueuedMessages(); } // retract
Alt chords interrupt typing more than a single key does, and they are the modifier terminals are most likely to claim for themselves. Each action has exactly one binding, so when a terminal does claim one there is no second way in and the action is not merely awkward — it is unavailable. Windows Terminal takes Alt+Enter for its fullscreen toggle; the same Alt+↑ used for retract is reported as never reaching the application on macOS Terminal and iTerm in a comparable CLI (codex#4490, codex#14213). The runtime behind these is complete and idle: turn.message.submit with placement: 'next_turn', the Host holding the entry to the terminal transition, queue.retract, and the Queued: bar pi-transcript.ts already renders.
One action is missing entirely. A user who types the correction that should stop the current work cannot apply it. Esc stops the Turn and anything pending is folded into the next one, so the sequence is stop, wait, retype. There is no way to say "stop now and use this".
Desired outcome
The three actions are reachable with single keys the user is already pressing, the keys are discoverable from the interface rather than from documentation, and interrupting can apply a pending message instead of deferring it.
Delivery semantics do not change. This is about how the actions are reached, not what Steer or Queue mean.
Where the proposed defaults come from
Two comparable CLIs solve the same problem, and the defaults below are taken from whichever already ships a shape that survives contact with real terminals rather than invented here.
|
Maka today |
Codex |
Claude Code |
| Inject into the running Turn |
Enter |
Enter |
Enter |
| Queue for the next Turn |
Alt+Enter |
Tab |
no separate concept |
| Take a pending message back |
Alt+↑ |
Option+↑ |
↑ from the first line |
| Interrupt |
Esc Esc / Ctrl+C |
Esc |
Esc / Ctrl+C |
| Interrupt and apply now |
— |
Esc while pending |
Esc, pending sent next |
| Pending indicator |
Steering: / Queued: bar |
"press esc to interrupt and send immediately" |
listed above the input |
| Bindings user-overridable |
no |
not investigated |
yes, keybindings.json — though queueing itself is not a bindable action |
Two rows are easy to read backwards. All three deliver a mid-Turn Enter inside the current Turn at a step boundary — the products differ in vocabulary, not in when the message lands. And only Maka and Codex have a distinct next-Turn queue at all.
Proposed defaults
| Action |
Proposed default |
Alias kept |
| Steer (inject into the running Turn) |
Enter |
— (unchanged) |
| Queue (open the next Turn) |
Tab, when no completion popup is open |
Alt+Enter |
| Retract queued text |
↑, when something is queued and the cursor is on the first line |
Alt+↑ |
| Interrupt |
Esc Esc / Ctrl+C |
— (unchanged) |
| Interrupt and apply now |
Esc, while a message is pending |
— (new) |
Every default is a single key, and all three are keys a user's hands are already on. The existing chords stay as aliases so current habits keep working. Both new defaults are reused conditionally rather than claimed outright:
Tab is not unconditionally free — pi-tui reserves it as "tui.input.tab": { defaultKeys: "tab", description: "Tab / autocomplete" }. It is free only while no completion popup is open, so the binding has to be conditional on that, not global.
↑ must keep working as history recall. It should be intercepted only when something is queued and the cursor is on the first line, falling through to history otherwise — the rule Claude Code uses for the same key.
The interface should state its own keys
Better defaults do not help a user who never learns the action exists. The surfaces that already appear at exactly the right moment should name the keys that apply to them, so the feature is discovered in the composer rather than in /help.
The pending bar pi-transcript.ts renders is the natural place, and it is currently silent about what can be done to it:
Steering: fix the retry logic
Queued: then run the linter
It could carry its own actions the way Codex's hint does — Codex advertises the escape hatch inline rather than documenting it elsewhere:
Steering: fix the retry logic esc apply now · ↑ edit
Queued: then run the linter ↑ edit
The same applies to the composer while a Turn is running: nothing currently suggests a message can be queued at all, and a short tab queue affordance next to the send hint would close the gap that documentation cannot.
This only stays truthful if the hint text and the key dispatcher read from one definition — a bar that advertises esc while a user has rebound it, or after a default changes, is worse than a silent one. That makes the registry below a prerequisite for this rather than an unrelated cleanup.
One definition for bindings
pi-tui already ships a named registry with user overrides and conflict detection, and Maka uses none of it:
export class KeybindingsManager {
constructor(definitions, userBindings = {}) { … } // conflict detection included
}
// keybindings.d.ts: "Downstream packages can add keybindings via declaration merging."
Declaring Maka's TUI actions there (maka.turn.steer, maka.turn.queue, maka.turn.retract, maka.turn.interrupt, maka.transcript.expand, …) and dispatching through the manager would replace the 21 hardcoded sites, let /help and the pending bar print what is actually bound instead of a parallel hand-written list, and give a user whose terminal eats a default a way to rebind it.
Of the three parts this is the least urgent on its own, but it is what makes the inline hints above safe to ship: they have to render the live binding, not a string that happens to match today's default.
Alternatives or workarounds
Learn the chords and use a terminal that passes them through. What users do today, and it amounts to telling them their terminal is wrong.
Document the chords better without changing them. Cheaper, and it addresses discoverability only — the chords are still two-handed, and still unavailable where the terminal claims them.
Pick different chords. The space is genuinely narrow: Shift+Enter and Ctrl+J are newline (tui.input.newLine), Ctrl+C and Esc are interrupt, Ctrl+O and Ctrl+T are transcript toggles, Ctrl+D quits. That is why this reuses Tab and ↑ conditionally instead of claiming new chords — and a single new chord would inherit the same single-binding shape.
Do nothing and rely on Desktop. Desktop is separately gaining queue controls (#2262). Leaving the TUI's bindings hardcoded while that lands is how two surfaces start disagreeing about what Queue means.
Sequencing
"Interrupt and apply now" depends on the unpulled-steering path being sound, because interrupting is precisely when a message has not been pulled yet. That path is being repaired in #3530 (an unpulled steer poisons the Host) and #3529 (a tool-free Turn never injects a steer); this item should land after them.
Researched with AI assistance (Claude Code). Maka behaviour verified against source at f19eede03; comparable-CLI behaviour cited inline.
Problem
Beyond plain send, the composer offers three turn-control actions while a Turn is running: queue a message for the next Turn, take a queued message back to edit it, and interrupt. Reaching them is harder than it should be.
They are hard to discover. Nothing in the composer suggests they exist. The pending bar appears only after a successful queue, so a user has to already know the chord to find out the feature is there. The one place the chords are written down is
/help, whose list lives intui-primary-guidance.tsand is maintained separately from the 21 hardcodedmatchesKeycall sites inpi-tui-runner.ts— two hand-kept copies with nothing holding them in agreement.They are awkward to press. Both live on Alt chords:
Alt chords interrupt typing more than a single key does, and they are the modifier terminals are most likely to claim for themselves. Each action has exactly one binding, so when a terminal does claim one there is no second way in and the action is not merely awkward — it is unavailable. Windows Terminal takes
Alt+Enterfor its fullscreen toggle; the sameAlt+↑used for retract is reported as never reaching the application on macOS Terminal and iTerm in a comparable CLI (codex#4490, codex#14213). The runtime behind these is complete and idle:turn.message.submitwithplacement: 'next_turn', the Host holding the entry to the terminal transition,queue.retract, and theQueued:barpi-transcript.tsalready renders.One action is missing entirely. A user who types the correction that should stop the current work cannot apply it.
Escstops the Turn and anything pending is folded into the next one, so the sequence is stop, wait, retype. There is no way to say "stop now and use this".Desired outcome
The three actions are reachable with single keys the user is already pressing, the keys are discoverable from the interface rather than from documentation, and interrupting can apply a pending message instead of deferring it.
Delivery semantics do not change. This is about how the actions are reached, not what Steer or Queue mean.
Where the proposed defaults come from
Two comparable CLIs solve the same problem, and the defaults below are taken from whichever already ships a shape that survives contact with real terminals rather than invented here.
EnterEnterEnterAlt+EnterTabAlt+↑Option+↑↑from the first lineEscEsc/Ctrl+CEscEsc/Ctrl+CEscwhile pendingEsc, pending sent nextSteering:/Queued:barkeybindings.json— though queueing itself is not a bindable actionTwo rows are easy to read backwards. All three deliver a mid-Turn
Enterinside the current Turn at a step boundary — the products differ in vocabulary, not in when the message lands. And only Maka and Codex have a distinct next-Turn queue at all.Proposed defaults
EnterTab, when no completion popup is openAlt+Enter↑, when something is queued and the cursor is on the first lineAlt+↑EscEsc/Ctrl+CEsc, while a message is pendingEvery default is a single key, and all three are keys a user's hands are already on. The existing chords stay as aliases so current habits keep working. Both new defaults are reused conditionally rather than claimed outright:
Tabis not unconditionally free — pi-tui reserves it as"tui.input.tab": { defaultKeys: "tab", description: "Tab / autocomplete" }. It is free only while no completion popup is open, so the binding has to be conditional on that, not global.↑must keep working as history recall. It should be intercepted only when something is queued and the cursor is on the first line, falling through to history otherwise — the rule Claude Code uses for the same key.The interface should state its own keys
Better defaults do not help a user who never learns the action exists. The surfaces that already appear at exactly the right moment should name the keys that apply to them, so the feature is discovered in the composer rather than in
/help.The pending bar
pi-transcript.tsrenders is the natural place, and it is currently silent about what can be done to it:It could carry its own actions the way Codex's hint does — Codex advertises the escape hatch inline rather than documenting it elsewhere:
The same applies to the composer while a Turn is running: nothing currently suggests a message can be queued at all, and a short
tab queueaffordance next to the send hint would close the gap that documentation cannot.This only stays truthful if the hint text and the key dispatcher read from one definition — a bar that advertises
escwhile a user has rebound it, or after a default changes, is worse than a silent one. That makes the registry below a prerequisite for this rather than an unrelated cleanup.One definition for bindings
pi-tui already ships a named registry with user overrides and conflict detection, and Maka uses none of it:
Declaring Maka's TUI actions there (
maka.turn.steer,maka.turn.queue,maka.turn.retract,maka.turn.interrupt,maka.transcript.expand, …) and dispatching through the manager would replace the 21 hardcoded sites, let/helpand the pending bar print what is actually bound instead of a parallel hand-written list, and give a user whose terminal eats a default a way to rebind it.Of the three parts this is the least urgent on its own, but it is what makes the inline hints above safe to ship: they have to render the live binding, not a string that happens to match today's default.
Alternatives or workarounds
Learn the chords and use a terminal that passes them through. What users do today, and it amounts to telling them their terminal is wrong.
Document the chords better without changing them. Cheaper, and it addresses discoverability only — the chords are still two-handed, and still unavailable where the terminal claims them.
Pick different chords. The space is genuinely narrow:
Shift+EnterandCtrl+Jare newline (tui.input.newLine),Ctrl+CandEscare interrupt,Ctrl+OandCtrl+Tare transcript toggles,Ctrl+Dquits. That is why this reusesTaband↑conditionally instead of claiming new chords — and a single new chord would inherit the same single-binding shape.Do nothing and rely on Desktop. Desktop is separately gaining queue controls (#2262). Leaving the TUI's bindings hardcoded while that lands is how two surfaces start disagreeing about what Queue means.
Sequencing
"Interrupt and apply now" depends on the unpulled-steering path being sound, because interrupting is precisely when a message has not been pulled yet. That path is being repaired in #3530 (an unpulled steer poisons the Host) and #3529 (a tool-free Turn never injects a steer); this item should land after them.
Researched with AI assistance (Claude Code). Maka behaviour verified against source at
f19eede03; comparable-CLI behaviour cited inline.