Status: IMPLEMENTED
Implements: #701
Summary
PR #686 shipped composer ↑/↓ arrow-key history, but pressing ↑ or ↓ while the agent is busy (thinking/inference running) does nothing. The guard in ui.zig:647 reads:
if (!busy and state.queue_editing_index == null) {
The !busy guard blocks history navigation during inference. This was a reviewer recommendation (#686 R1) that the implementer accepted — the original intent per the plan (#667) was that Busy should NOT block history. This plan restores that original intent.
Root cause
native/harness/src/ui.zig:647 — the !busy guard. Two lines below it, historyApply(.older) already gates on in_hist or buf_empty — the only scenario where history is entered is when the composer is empty or already in history, both of which are safe during inference (history is a pure in-memory read, no bridge write, no alloc).
Fix
One-line removal: delete !busy and from the guard at ui.zig:647.
// Before:
if (!busy and state.queue_editing_index == null) {
// After:
if (state.queue_editing_index == null) {
The doc comment on lines 644–646 ("Skip history when busy…") also needs to be removed or updated.
Behavior after fix
| Scenario |
Before |
After |
| Idle, empty composer, ↑ |
Loads newest user msg |
Same (unchanged) |
| Idle, in history, ↑/↓ |
Walks history |
Same (unchanged) |
| Idle, non-empty, not in history, ↑/↓ |
Passes through to textEntry |
Same (unchanged) |
| Busy, empty composer, ↑ |
Blocked (↓ does nothing) |
Loads newest user msg |
| Busy, in history, ↑/↓ |
Blocked |
Walks history |
| Queue editing (any state), ↑/↓ |
Passes through to editor |
Same (unchanged) |
Why this is safe
- No bridge write.
historyApply is a pure read: it scans the in-memory message buffer, finds user messages by kind, and copies text into prompt_buf. No inv_* call, no host comms.
- No alloc. The message buffer lives in
bridge.RING_CAP stack space already held by ui.frame(). No heap allocation, no I/O.
- No state mutation outside composer state. Only
prompt_buf, history_index, history_draft_buf, history_draft_len, history_newest_fingerprint, and history_newest_fp_len are touched — all composer-local state fields.
- The
queue_editing_index guard stays. While editing a queued row, arrows still pass through to the text editor.
- Hydrate drop still works during busy. The fingerprint-mismatch check (lines 539–563) runs unconditionally in
frame(), outside the !busy guard. If a session hydrate replaces all rows while the operator is in history during a busy turn, prompt_buf is correctly restored to the saved draft and history_index cleared.
Layers
| Layer |
Change |
| Harness (Wasm) |
ui.zig — remove !busy and from guard + update doc comment (~3 lines) |
| Vercel backend |
No change |
| DOM |
No change |
No protocol bump, no bridge changes, no TS gates.
Ops & docs
- Cloud ops: N/A — no Production mutate
- Docs:
docs/harness-limits.md keyboard table row updated: "Not active while Busy — arrows pass through to the text caret" → "Works while Busy (queue band visible, spinner active) — arrows navigate history during inference"
- Caps: No new caps. No existing cap changed.
- Zig gates only:
zig fmt, zig build harness -Doptimize=Debug, zig build test-rich-invariants, zig build test-rich (existing composer_history suite), build-harness CI
Testing
Existing composer_history.test.zig (30 tests) covers all data paths in composer_history.zig and historyApply. The !busy guard is in ui.frame() which has no host-testable path (requires a running bridge + lifecycle). Manual smoke:
- Start a long agent turn
- While busy (spinner visible), press ↑ on empty composer → newest user message loads
- Press ↑ again → older user message loads
- Press ↓ → newer user message loads
- Press ↓ past newest → saved draft restores
- Press New/Clear while busy and in history → draft restores, history resets
- Queue editing while busy → ↑/↓ still pass through to editor
- Hydrate (load a session) while busy and in history → fingerprint mismatch drops history, draft restored
Review notes (2026-08-21)
| Finding |
Sev |
Issue |
Fix |
| 1 |
Minor (L1) |
Plan didn't note that original plan #667 intended Busy to NOT block history — this is a bug-fix, not a design change |
Added to Summary and Design notes |
| 2 |
Minor (L8) |
Doc update text imprecise for keyboard table format |
Replaced with full sentence matching existing row style |
Reviewed: baseline verified against live main (4bc831b). Guard at L647 confirmed; historyApply confirmed pure read (zero inv_* calls); fingerprint hydrate drop at L539–563 confirmed runs unconditionally. No Blockers, no Majors.
Verdict: HANDOFF-READY — one-line guard removal, safe during inference.
Ship
Shipped in PR #707 (plan/composer-arrow-busy).
Status:
IMPLEMENTEDImplements: #701
Summary
PR #686 shipped composer ↑/↓ arrow-key history, but pressing ↑ or ↓ while the agent is busy (thinking/inference running) does nothing. The guard in
ui.zig:647reads:The
!busyguard blocks history navigation during inference. This was a reviewer recommendation (#686 R1) that the implementer accepted — the original intent per the plan (#667) was that Busy should NOT block history. This plan restores that original intent.Root cause
native/harness/src/ui.zig:647— the!busyguard. Two lines below it,historyApply(.older)already gates onin_hist or buf_empty— the only scenario where history is entered is when the composer is empty or already in history, both of which are safe during inference (history is a pure in-memory read, no bridge write, no alloc).Fix
One-line removal: delete
!busy andfrom the guard atui.zig:647.The doc comment on lines 644–646 ("Skip history when busy…") also needs to be removed or updated.
Behavior after fix
Why this is safe
historyApplyis a pure read: it scans the in-memory message buffer, finds user messages by kind, and copies text intoprompt_buf. Noinv_*call, no host comms.bridge.RING_CAPstack space already held byui.frame(). No heap allocation, no I/O.prompt_buf,history_index,history_draft_buf,history_draft_len,history_newest_fingerprint, andhistory_newest_fp_lenare touched — all composer-local state fields.queue_editing_indexguard stays. While editing a queued row, arrows still pass through to the text editor.frame(), outside the!busyguard. If a session hydrate replaces all rows while the operator is in history during a busy turn,prompt_bufis correctly restored to the saved draft andhistory_indexcleared.Layers
ui.zig— remove!busy andfrom guard + update doc comment (~3 lines)No protocol bump, no bridge changes, no TS gates.
Ops & docs
docs/harness-limits.mdkeyboard table row updated: "Not active while Busy — arrows pass through to the text caret" → "Works while Busy (queue band visible, spinner active) — arrows navigate history during inference"zig fmt,zig build harness -Doptimize=Debug,zig build test-rich-invariants,zig build test-rich(existingcomposer_historysuite),build-harnessCITesting
Existing
composer_history.test.zig(30 tests) covers all data paths incomposer_history.zigandhistoryApply. The!busyguard is inui.frame()which has no host-testable path (requires a running bridge + lifecycle). Manual smoke:Review notes (2026-08-21)
Reviewed: baseline verified against live
main(4bc831b). Guard at L647 confirmed;historyApplyconfirmed pure read (zeroinv_*calls); fingerprint hydrate drop at L539–563 confirmed runs unconditionally. No Blockers, no Majors.Verdict: HANDOFF-READY — one-line guard removal, safe during inference.
Ship
Shipped in PR #707 (
plan/composer-arrow-busy).