Status: IMPLEMENTED
Source: #703
Type: Single issue — pure Zig (Wasm-only)
Summary
Bind Escape to bridge.queueCancelFromUi() in the harness event scan — the same cancel path as the ■ Stop icon. Today Stop is pointer-only; Escape does nothing to the in-flight round.
Where
One event-scan addition in ui.zig (the same for (0..es.len) loop at ~L617–664 that handles Ctrl+Enter and ↑/↓ history), plus a docs row.
Design
| Condition |
Behavior |
Busy + Escape .down + queue_editing_index == null |
bridge.queueCancelFromUi() + mark handled |
Busy + Escape .down + queue-row editor open |
skip — queue_band.handleEscape() already dismisses the editor (runs during queue_band.paint) |
| Idle + Escape |
no-op — do not clear composer, do not leave the page |
.repeat Escape events |
skip — only the initial .down fires cancel (same as ■ single-fire) |
Why this is safe
queueCancelFromUi() already has a no-op guard when lifecycle != .busy and !has_pending_submit (bridge.zig:288–289), so even a double-fire or a mis-placed Escape during idle is harmless. The queue-edit guard (queue_editing_index == null) matches the existing arrow-history guard at line 647 — the queue-row editor owns Esc.
Placement in the event scan
Insert the Escape handler after the Ctrl+Enter block (~L635) and before the arrow-history block (~L647). Escape has the highest priority after the send chord: if the operator presses Esc, don't also walk history or pass the key to textEntry.
Layers
| Layer |
Change |
| Harness (Wasm) |
ui.zig +4–6 lines in event scan |
| Vercel backend |
No change |
| DOM |
No change |
- No protocol bump — v9 cancel path reused as-is
- No bridge changes —
queueCancelFromUi already exposed
- No TS changes → no vitest/typecheck/di-gate/build
Docs
docs/harness-limits.md: Keyboard table → new row: **Escape** (harness focused, Busy) | Cancel the in-progress turn (same as ■ Stop). Idle: no-op. Queue-row editor: dismisses editor first (does not abort turn)
Ops & docs
- Cloud ops: N/A — no Production mutate
- Caps: No new caps, no existing cap changed
- Zig gates only:
zig fmt, zig build harness -Doptimize=Debug, zig build test-rich-invariants, build-harness CI
Testing
Manual smoke plan:
- Busy + Esc: Send prompt, press Esc while spinning → turn cancels (same as ■ Stop). Composer text preserved.
- Idle + Esc: Press Esc with no turn running → nothing happens.
- Queue edit + Esc: Edit a queued row, press Esc → editor closes (existing queue_band behavior). Press Esc again while still Busy → turn cancels.
- DOM nav overlay + Esc: Open NavMenu → Esc closes menu (host DOM, not canvas).
No host-testable unit test — frame() requires a running bridge + lifecycle state.
Suggested next step
/plan-review → review, then /implement-plan to ship.
Status:
IMPLEMENTEDSource: #703
Type: Single issue — pure Zig (Wasm-only)
Summary
Bind Escape to
bridge.queueCancelFromUi()in the harness event scan — the same cancel path as the ■ Stop icon. Today Stop is pointer-only; Escape does nothing to the in-flight round.Where
One event-scan addition in
ui.zig(the samefor (0..es.len)loop at ~L617–664 that handles Ctrl+Enter and ↑/↓ history), plus a docs row.Design
.down+queue_editing_index == nullbridge.queueCancelFromUi()+ mark handled.down+ queue-row editor openqueue_band.handleEscape()already dismisses the editor (runs duringqueue_band.paint).repeatEscape events.downfires cancel (same as ■ single-fire)Why this is safe
queueCancelFromUi()already has a no-op guard whenlifecycle != .busy and !has_pending_submit(bridge.zig:288–289), so even a double-fire or a mis-placed Escape during idle is harmless. The queue-edit guard (queue_editing_index == null) matches the existing arrow-history guard at line 647 — the queue-row editor owns Esc.Placement in the event scan
Insert the Escape handler after the Ctrl+Enter block (~L635) and before the arrow-history block (~L647). Escape has the highest priority after the send chord: if the operator presses Esc, don't also walk history or pass the key to textEntry.
Layers
ui.zig+4–6 lines in event scanqueueCancelFromUialready exposedDocs
docs/harness-limits.md: Keyboard table → new row:**Escape** (harness focused, Busy) | Cancel the in-progress turn (same as ■ Stop). Idle: no-op. Queue-row editor: dismisses editor first (does not abort turn)Ops & docs
zig fmt,zig build harness -Doptimize=Debug,zig build test-rich-invariants,build-harnessCITesting
Manual smoke plan:
No host-testable unit test —
frame()requires a running bridge + lifecycle state.Suggested next step
/plan-review→ review, then/implement-planto ship.