Skip to content

plan: remove !busy guard from composer arrow-key history #704

Description

@btipling

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

  1. 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.
  2. No alloc. The message buffer lives in bridge.RING_CAP stack space already held by ui.frame(). No heap allocation, no I/O.
  3. 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.
  4. The queue_editing_index guard stays. While editing a queued row, arrows still pass through to the text editor.
  5. 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:

  1. Start a long agent turn
  2. While busy (spinner visible), press ↑ on empty composer → newest user message loads
  3. Press ↑ again → older user message loads
  4. Press ↓ → newer user message loads
  5. Press ↓ past newest → saved draft restores
  6. Press New/Clear while busy and in history → draft restores, history resets
  7. Queue editing while busy → ↑/↓ still pass through to editor
  8. 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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions