Skip to content

feat(harness): single keymap + leader + help overlay for shortcuts (plan #741) - #746

Merged
btipling merged 6 commits into
mainfrom
plan/harness-keymap
Aug 21, 2026
Merged

feat(harness): single keymap + leader + help overlay for shortcuts (plan #741)#746
btipling merged 6 commits into
mainfrom
plan/harness-keymap

Conversation

@btipling

Copy link
Copy Markdown
Owner

Summary

Implements plan #741 — harness keyboard shortcuts (keymap, leader, help overlay). Ships a single keymap table in Wasm, a browser-safe reserved-browser deny-list, a Ctrl+Shift+Space leader with an in-canvas help overlay, and migrates all existing chords (Enter send/enqueue, ↑↓ history, Escape busy-cancel / queue-edit, Ctrl/Cmd+Enter queue-save) onto the table.

Fixes #741

What changed (all harness + docs; no DOM, no TS, no backend)

  • native/harness/src/keymap.zig (new) — the single chord table (ACTION rows with chord + when context), the reserved-browser deny-list (fail-closed: a reserved chord is never marked handled, even if a row overlaps), the match() pure dispatcher, and the leader machine. +26 unit tests covering every shipped row, reserved deny, and leader arm/swallow/timeout.
  • native/harness/src/ui/keymap_dispatch.zig (new) — the one per-frame walk of dvui.events() (before the composer textEntry is built). Marks handled, runs product seams via a Handlers{history} struct (avoids an import cycle with ui.zig), and drives the leader via dvui.timer expiry (no host I/O, no new exports).
  • native/harness/src/ui/help_overlay.zig (new) — TEAL in-canvas panel over the transcript band listing the live keymap; context-off rows grey (WARM-muted, not EMBER). No GPA/alloc, no host I/O.
  • native/harness/src/ui.zig — the inline composer event loop (composer_submit, busy-Escape, ↑↓ history) is replaced by the single keymap_dispatch.dispatch(...) call; submit is requested via state.request_submit and consumed after textEntry.deinit; help overlay painted over the transcript band; ring-clear (New/Clear/hydrate) closes the overlay + disarms the leader.
  • native/harness/src/ui/queue_band.zig — deleted handleEscape and submitChord (both inline for (dvui.events()) scans); queue_save/cancel_queue_edit now route through the dispatcher via a new pub cancelEditFromUi + existing saveEdit. Queue paint reads no keys.
  • native/harness/src/ui/state.zig — new flags help_overlay_open, leader_armed, request_submit (+ reset on clear/hydrate).
  • native/harness/src/ui/metrics.zigHELP_OVERLAY_* (NEW cap constants; helpful, generous, no wire).
  • native/harness/build.zig — registers keymap in test-rich.
  • Docs (living docs, same PR): docs/harness-limits.md (Keyboard & focus → live keymap table), docs/feature-divide.md + AGENTS.md (keymap + dispatcher + help overlay are harness-owned; DOM adds no shortcut UI).

Caps table

Cap Value Rationale Location
NEW KEYMAP_MAX 64 rows Static table; generous for follow-up chords (#742 leader+t); not a wire native/harness/src/keymap.zig
NEW LEADER_WINDOW_MS 800 Long enough for ?/a letter, short enough not to trap typing keymap.zig
NEW HELP_OVERLAY_* 460×320 px panel / 300×200 min band / 16 px margin Fixed in-canvas panel sized to the transcript band ui/metrics.zig

No existing cap is raised or lowered. No Function/Blob/Redis wire.

Verification

  • Zig-only (pure harness + docs) → Zig gates, no vitest/typecheck gate applies:
    • zig build test-richgreen (all host unit tests incl. new keymap suite)
    • zig build test-rich-invariants — green
    • zig build harness -Doptimize=Debuggreen (full wasm32-freestanding compile)
    • zig fmt --check on all touched .zig — clean
  • DoD met: no for (dvui.events()) key loop outside the dispatcher (verified via grep); ui.zig + queue_band.handleEscape + queue_band.submitChord inline scans all gone.
  • build-harness CI (release wasm32-freestanding on the self-hosted runner) will be the release gate — watching on this PR. Local Debug compile already proves the Wasm compiles.

Notes for review

  • composer context is proxied as !queue_editing (dvui focus is frame-global; the composer is the only text input). Matches the old inline gating exactly.
  • Leader window intentionally swallows the first post-prefix key (a recognized product chord does not fire during the window), so a stray key immediately disarms instead of both firing an action and arming the leader.
  • Mobile ~390px: overlay still paints (scrolls inside if taller than the band); leader/chords are desktop (non-goal: no on-screen cheatsheet button).

@btipling btipling self-assigned this Aug 21, 2026
@vercel

vercel Bot commented Aug 21, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
invincible Ignored Ignored Aug 21, 2026 7:01am

Request Review

@btipling

Copy link
Copy Markdown
Owner Author

Implemented — ready for adversarial review

Implementation of plan #741 is complete and pushed. Summary of the 4 commits on plan/harness-keymap:

  • keymap.zig — single chord table + reserved-browser deny-list + leader machine (+26 unit tests)
  • ui/keymap_dispatch.zig — the one per-frame walk of dvui.events() (handled-marking, leader dvui-timer expiry, Handlers{history} to avoid an import cycle)
  • ui/help_overlay.zig — TEAL in-canvas panel; ui.zig now calls the dispatcher (inline composer loop removed), queue_band.zig deleted handleEscape + submitChord, ui/state.zig + ui/metrics.zig new flags/caps
  • Living docsdocs/harness-limits.md, docs/feature-divide.md, AGENTS.md

Verified

  • zig build test-rich green (incl. new keymap suite)
  • zig build test-rich-invariants green
  • zig build harness -Doptimize=Debug green (full wasm32 compile)
  • build-harness CI green on this PR (wasm32-freestanding release; only infra Node-20 deprecation annotation)
  • zig fmt --check clean
  • DoD: no for (dvui.events()) key loop outside keymap_dispatch.zig

No TS/DOM/backend changes → no vitest/typecheck gate applies (Zig-only PR). Next step: adversarial-review on this head.

@btipling btipling left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adversarial review — PR #746

Verdict: REQUEST CHANGES
Repo: btipling/invincible
Scope: mainplan/harness-keymap · 11 files · single keymap table + leader + help overlay (#741)
Lenses run: L1, L3, L5, L6, L8, L9 (skip: L2 no secrets/API; L4 no workflow; L7 no clone bind)
AGENTS.md read: yes · docs/feature-divide.md read (Wasm owns chords; DOM adds none)

Architecture is the right shape: pure keymap.match, one dvui.events() walk before textEntry, reserved deny-list never marked handled, request_submit after te.deinit, queue handleEscape/submitChord gone. Caps are new (KEYMAP_MAX 64, LEADER_WINDOW_MS 800, HELP_OVERLAY_*) — none raised. dvui.timer(..., micros) × LEADER_WINDOW_MS*1000 is 800 ms. Palette TEAL + WARM-muted grey (not EMBER). Feature-divide holds.

The leader contract the last commit wrote (e780b8c: swallow everything except ? / Esc / re-arm) is broken at the dispatcher, and tests cannot see it.

Findings

Sev Lens Finding Break scenario Refutation attempt Confidence
Major L1 keymap_dispatch.fromDvui(...) orelse continue punches a hole in the leader swallow. Unmapped keys never reach keymap.match, are not marked handled, and do not disarm. Key only lists a subset of letters (c v x a z s f t n w r l p i j) plus a few specials — no b d e g h k m o q u y, digits, Backspace, = , , , etc. Pure tests only feed the enum (leader + unmatched printable uses .a, which is mapped). Ctrl+Shift+Space, then h (or 1, or Backspace). h inserts into the composer; leader stays armed. Next mapped letter (l in “help”) swallows and disarms — prompt contains he, overlay may still open on a later ? in the same 800 ms. The #741 “never lands in the prompt” promise is false for most of the keyboard. Defender: “unknown keys pass through (Goal 3).” Goal 3 is the reserved deny-list, which already runs inside match. Goal 2 / last-commit swallow is “unmatched key while leader pending → handled + disarm.” continue skips both. Fix: if fromDvui is null and state.leader_armed, mark handled + disarm; else pass through. high
Minor L1 Help overlay silently no-ops when the leftover transcript width is < HELP_OVERLAY_MIN_W (300). paint(pane_w, …, avail.w - pane_w, …) — open rail is 220 px. At ~390: leftover 170. help_overlay_open flips; nothing paints. PR body: “Mobile ~390px: overlay still paints.” Open the left rail on a ~390 canvas, hit Ctrl+/. State is open, Esc still closes, operator sees no panel. Closed rail (40) leaves 350 ≥ 300, so the happy path works. Lower MIN_W or size against avail.w (full band) instead of leftover. high
Minor L8 Overlay is not the table. rowLabel switches on action, so history_older + history_older_in both render “Older message / ↑” (duplicate row). row.help is unused. KEY_TABLE precedence (help_close before leader_cancel) is dead while leader_pending because match() short-circuits to Esc → leader_cancel before the table — overlay-open + leader armed needs two Escapes; docs say help wins. Operator opens help, then hits the leader, then Esc. Overlay stays. A later agent “fixes” overlay copy from row.help and still ships the duplicate ↑ row. Last commit made the short-circuit explicit; queue-edit Esc also loses to leader (safer). Duplicate is paint-only. high
Nit L9 Overlay chord labels / use addText on .theme(.mono). Harness-limits already say Noto has no Arrows block (chip jump hint uses addTextMixed). May tofu in the cheatsheet of all places. Open help — arrows are .notdef. ASCII Ctrl/Cmd+/ rows are fine. Same class as #732. medium

Residual risk

  • LEADER_TIMER_ID = 0x7410_0000_0000_0001 vs widget Id.extendId — assumed collision-safe, not proven.
  • Overlay ctx_scroll is a module-level var; reopen keeps the old scroll offset.
  • Dispatcher has no host test (only pure keymap.zig). The Major above is exactly the seam test-rich does not compile.
  • Zig test-rich / wasm32 build-harness were not re-run in this review environment.

Merge guidance

  • Do not merge until leader-pending unmapped keys are swallowed (or the dispatcher tests a non-enum key and the overlay is visible at ~390 with the rail open).
  • Do not add a DOM window keydown / React cheatsheet to paper over the overlay min-width.
  • Do not silently change LEADER_WINDOW_MS / KEYMAP_MAX.

What was not attacked

Host zig build test-rich / wasm32 build-harness, live canvas (Ctrl+/ , leader+?, Esc vs menus), dvui timer clock vs rAF, Production harness artifact deploy, #742 leader+t (not in this PR).

@btipling btipling left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adversarial review — PR #746 (verdict correction)

Verdict: CONCERNS
Repo: btipling/invincible
Scope: mainplan/harness-keymap · 11 files · single keymap table + leader + help overlay (#741)
Lenses run: L1, L3, L5, L6, L8, L9 (skip: L2 no secrets/API; L4 no workflow; L7 no clone bind)
AGENTS.md read: yes · docs/feature-divide.md read (Wasm owns chords; DOM adds none)

Correction: the previous comment used REQUEST CHANGES, which is not a skill verdict. Allowed: BLOCK | CONCERNS | PASS WITH NOTES | INCOMPLETE. One Major + no Blocker → CONCERNS. Findings unchanged from the attack @ e780b8c.

Architecture is the right shape: pure keymap.match, one dvui.events() walk before textEntry, reserved deny-list never marked handled, request_submit after te.deinit, queue handleEscape/submitChord gone. Caps are new (KEYMAP_MAX 64, LEADER_WINDOW_MS 800, HELP_OVERLAY_*) — none raised. dvui.timer(..., micros) × LEADER_WINDOW_MS*1000 is 800 ms. Palette TEAL + WARM-muted grey (not EMBER). Feature-divide holds.

The leader contract the last commit wrote (e780b8c: swallow everything except ? / Esc / re-arm) is broken at the dispatcher, and tests cannot see it.

Findings

Sev Lens Finding Break scenario Refutation attempt Confidence
Major L1 keymap_dispatch.fromDvui(...) orelse continue punches a hole in the leader swallow. Unmapped keys never reach keymap.match, are not marked handled, and do not disarm. Key only lists a subset of letters (c v x a z s f t n w r l p i j) plus a few specials — no b d e g h k m o q u y, digits, Backspace, = , , , etc. Pure tests only feed the enum (leader + unmatched printable uses .a, which is mapped). Ctrl+Shift+Space, then h (or 1, or Backspace). h inserts into the composer; leader stays armed. Next mapped letter (l in “help”) swallows and disarms — prompt contains he, overlay may still open on a later ? in the same 800 ms. The #741 “never lands in the prompt” promise is false for most of the keyboard. Defender: “unknown keys pass through (Goal 3).” Goal 3 is the reserved deny-list, which already runs inside match. Goal 2 / last-commit swallow is “unmatched key while leader pending → handled + disarm.” continue skips both. Fix: if fromDvui is null and state.leader_armed, mark handled + disarm; else pass through. high
Minor L1 Help overlay silently no-ops when the leftover transcript width is < HELP_OVERLAY_MIN_W (300). paint(pane_w, …, avail.w - pane_w, …) — open rail is 220 px. At ~390: leftover 170. help_overlay_open flips; nothing paints. PR body: “Mobile ~390px: overlay still paints.” Open the left rail on a ~390 canvas, hit Ctrl+/. State is open, Esc still closes, operator sees no panel. Closed rail (40) leaves 350 ≥ 300, so the happy path works. Lower MIN_W or size against avail.w (full band) instead of leftover. high
Minor L8 Overlay is not the table. rowLabel switches on action, so history_older + history_older_in both render “Older message / ↑” (duplicate row). row.help is unused. KEY_TABLE precedence (help_close before leader_cancel) is dead while leader_pending because match() short-circuits to Esc → leader_cancel before the table — overlay-open + leader armed needs two Escapes; docs say help wins. Operator opens help, then hits the leader, then Esc. Overlay stays. A later agent “fixes” overlay copy from row.help and still ships the duplicate ↑ row. Last commit made the short-circuit explicit; queue-edit Esc also loses to leader (safer). Duplicate is paint-only. high
Nit L9 Overlay chord labels / use addText on .theme(.mono). Harness-limits already say Noto has no Arrows block (chip jump hint uses addTextMixed). May tofu in the cheatsheet of all places. Open help — arrows are .notdef. ASCII Ctrl/Cmd+/ rows are fine. Same class as #732. medium

Residual risk

  • LEADER_TIMER_ID = 0x7410_0000_0000_0001 vs widget Id.extendId — assumed collision-safe, not proven.
  • Overlay ctx_scroll is a module-level var; reopen keeps the old scroll offset.
  • Dispatcher has no host test (only pure keymap.zig). The Major above is exactly the seam test-rich does not compile.
  • Zig test-rich / wasm32 build-harness were not re-run in this review environment.

Merge guidance

  • CONCERNS: merge only with explicit accept of the Major (leader-pending unmapped keys insert). Prefer fixing fromDvui null + leader_armed → swallow/disarm before merge.
  • Do not add a DOM window keydown / React cheatsheet to paper over the overlay min-width.
  • Do not silently change LEADER_WINDOW_MS / KEYMAP_MAX.

What was not attacked

Host zig build test-rich / wasm32 build-harness, live canvas (Ctrl+/ , leader+?, Esc vs menus), dvui timer clock vs rAF, Production harness artifact deploy, #742 leader+t (not in this PR).

@btipling

btipling commented Aug 21, 2026

Copy link
Copy Markdown
Owner Author

Plan to address feedback on PR #746 (Status: DONE)

Last updated: 2026-08-21

Feedback items (most complex → least)

# Severity Review item Status
1 Major L1 fromDvui(...) orelse continue punches a hole in the leader swallow for unmapped (non-enum) keys 🟢 done
2 Minor L1 help overlay silently no-ops when leftover transcript width < HELP_OVERLAY_MIN_W (300) 🟢 done
3 Minor L8 overlay is not the table: history_older+history_older_in duplicate row; row.help unused; help-vs-leader Esc precedence 🟢 done
4 Nit L9 overlay / chord labels use addText on .theme(.mono) — Noto has no Arrows block (tofu class #732) 🟢 done

#1 — Major L1: leader window lets unmapped keys insert + stay armed (🟢 DONE)

  • Review item: keymap_dispatch.fromDvui(...) orelse continue — unmapped keys never reach keymap.match, are not marked handled, and do not disarm. Key lists only a subset of letters (c v x a z s f t n w r l p i j) + specials; no b d e g h k m o q u y, digits, Backspace, =, ,. Pure tests only feed the enum (leader + unmatched printable uses .a, which is mapped). Ctrl+Shift+Space then h → inserts into composer, leader stays armed.
  • Grounded problem: src/ui/keymap_dispatch.zig:187 const kcode = fromDvui(ke.code) orelse continue;. fromDvui returned null for any dvui enums.Key not in the mapping → continue skipped both e.handled = true and disarmLeader(). The leader-swallow contract only exists inside keymap.match which unmapped keys never reached. Confirmed: dvui enums.Key has b d e g h k m o q u y, digits, punctuation, backspace, delete, etc. all unmapped.
  • Severity assessment: Major — breaks the plan's plan: harness keyboard shortcuts (keymap, leader, help overlay) #741 "never lands in the prompt" promise for most of the keyboard while a leader is pending.
  • Strategies considered:
    • A) Extend fromDvui to map every unrecognized non-modifier dvui Key → a new .unknown keymap Key; keep null only for modifier/lock keys. Then keymap.match(.unknown, ...) returns .swallow_leader while leader pending (handled + disarmed) and .none (pass-through) otherwise — no dispatcher-specific disarm logic, and the pure module host-tests the exact seam test-rich compiles. (→ CHOSEN)
    • B) In the dispatcher, if (state.leader_armed and kcode == null) { e.handled = true; disarmLeader(); } — the reviewer's literal suggestion. Rejected: it disarms on modifier .up/release right after arming (releasing Ctrl/Shift after Ctrl+Shift+Space or re-pressing Shift for ? would kill the window), and is not host-testable (dispatcher imports dvui).
    • C) Pass-through (continue) — the current buggy behavior — rejected.
  • Open questions: none.
  • Fix (locked): Added unknown to keymap.Key. Rewrote fromDvui: known subset → those keys; every other alphabetic/digit/punct/nav/function/kp key → .unknown; pure modifier/lock/system keys (left_shift/right_shift/left_control/right_control/left_alt/right_alt/left_command/right_command, menu, num_lock, caps_lock, print, scroll_lock, pause, dvui .unknown) → null (still orelse continue; never disarm). No change needed in dispatch.unknown flows into match which already returns .swallow_leader for unmatched keys while ctx.leader_pending.
  • Tests: keymap.zigmatch(.unknown,.down,... leader_pending=true).swallow_leader (handled, no action); match(.unknown,.down,... idle).none (pass-through); match(.unknown,.repeat,... leader_pending=true).swallow_leader; match(.unknown,.up,... leader_pending=true).none (release never disarms/swallows). keymap suite 27 → 36 tests, all pass.

#2 — Minor L1: overlay no-ops on narrow canvas with rail open (🟢 DONE)

  • Review item: Help overlay silently no-ops when the leftover transcript width < HELP_OVERLAY_MIN_W (300). paint(pane_w, …, avail.w - pane_w, …) — open rail is 220 px. At ~390: leftover 170. help_overlay_open flips; nothing paints.
  • Grounded problem: src/ui.zig:592 passed w = @max(0, avail.w - pane_w) (leftover transcript width) to help_overlay.paint; src/ui/help_overlay.zig:67 if (w < metrics.HELP_OVERLAY_MIN_W ...) return;. With the rail open (220) on a 390px canvas, leftover = 170 < 300 → early return.
  • Severity assessment: Minor — narrow-viewport + left-rail-open case silently drops the cheatsheet.
  • Strategies considered:
    • A) Size against the full band avail.w (x=0, w=avail.w) instead of the leftover — the overlay is a modal in-canvas panel; centering across the whole window is correct. (→ CHOSEN)
    • B) Lower HELP_OVERLAY_MIN_W — rejected: changing an existing cap needs a human decision, and wouldn't help when the rail consumes most width.
  • Fix: src/ui.zig:592help_overlay.paint(0, scroll_y, avail.w, scroll_h, ...).
  • Tests: not host-testable (dvui paint/geometry). Compile-forced by build-harness; math recorded above (avail.w=390 ≥ 300 → paints).

#3 — Minor L8: overlay is not the table (🟢 DONE)

  • Review item: rowLabel switches on action, so history_older + history_older_in both render "Older message / ↑" (duplicate row). row.help is unused. KEY_TABLE precedence (help_close before leader_cancel) is dead while leader_pending because match() short-circuits to Esc → leader_cancel before the table — overlay-open + leader armed needs two Escapes; docs say help wins.
  • Grounded problem: src/ui/help_overlay.zig:23-37 rowLabel keyed on row.action; both history_older rows share action = .history_older → identical rendered row. row.help (keymap.zig Row.help) never read. src/keymap.zig:370-381 while ctx.leader_pending Escape returned leader_cancel before scanning KEY_TABLE's help_close row.
  • Severity assessment: Minor — duplicate paint row + help-vs-leader Esc precedence contradicts docs (docs/harness-limits.md:61 "Esc ... closes the help overlay (wins over busy cancel); disarms the leader").
  • Strategies considered:
    • A) Overlay: iterate KEY_TABLE, dedupe by action (each distinct action once — history_older_in no longer repaints), and use row.help for the help copy (the overlay IS the table). (→ CHOSEN)
    • B) match: inside the leader-pending branch, if help_open and Escape → return help_close before leader_cancel (help wins, one Esc). (→ CHOSEN together with A)
  • Fix: src/ui/help_overlay.zigrowChord() replaces rowLabel() (help now from row.help, chord from action); paint loop dedupes by action. src/keymap.zig leader-pending Escape → help_open check first.
  • Tests: keymap.zigmatch(.escape,.down,... leader_pending+help_open).help_close; leader_pending without help → .leader_cancel.

#4 — Nit L9: overlay arrow glyphs may tofu (🟢 DONE)

  • Review item: Overlay / chord labels use addText on .theme(.mono). Noto/Vera have no Arrows block; the chip jump hint uses addTextMixed. May tofu in the cheatsheet.
  • Grounded problem: src/ui/help_overlay.zig:119-124 chord textLayout .theme(.mono).addText(...); / (U+2191/U+2193) lack glyphs in Noto + Vera (mono) — see docs/harness-limits.md:235,246 (DejaVu symbols carry arrows); existing paint paths use rich/mixed_text.zig addTextMixed.
  • Severity assessment: Nit — cosmetic; would tofu in the cheatsheet that documents the arrows.
  • Strategies considered:
    • A) Paint the chord via addTextMixed — mixed face split routes / to DejaVu symbols, ASCII chords keep the mono face. (→ CHOSEN)
    • B) Substitute ASCII ("Up"/"Down") — rejected: loses the arrow affordance.
  • Fix: src/ui/help_overlay.zig chord textLayout → mixed_text.addTextMixed(chord, chord_str, .theme(.mono), {...}).
  • Tests: paint path (not host-testable); compile-forced. Same pattern already pinned by paint_diff.test.zig (addTextMixed is the face-split seam).

Round 2 — mergeable-state reconciliation (single-file conflict vs #744)

Not adversarial review feedback — the merge fast-fail. The 4 review items above were DONE and PASS'd at head 95d956e (PASS WITH NOTES, safe to merge). The blocker was a git conflict vs main: after this PR forked, #744 merged and independently rewrote the inline composer block in ui.zig — extracting the field + ▶/■ icon pack into ui/composer_chrome.zig (reserved trailing-icon width, plan #737/#734). Both #744 and #746 rewrote the same loop: #746 replaced it with keymap_dispatch.dispatch(...) + state.request_submit; #744 replaced it with composer_chrome.paintComposerChrome(...) + an inline composer_submit chord scan.

Progress log (append-only)

  • 2026-08-21 — created plan for round 1 (adversarial-review CONCERNS @ e780b8c); 1 Major + 2 Minor + 1 Nit; locked .unknown keymap variant + modifier-null split for the Major.
  • 2026-08-21 — implemented all 4 items (Major + overlay width + overlay-dedup/help-wins-Esc + addTextMixed); commit 95d956e on plan/harness-keymap.
  • 2026-08-21 — gates: zig build test-rich 88/88 steps · 1283/1283 tests (keymap 27→36); test-rich-invariants exit 0; zig build harness -Doptimize=Debug exit 0 (full wasm compile); zig fmt --check clean. No bridge exports changed → shipped artifact stays valid.
  • 2026-08-21 — build-harness CI GREEN on head 95d956e (run 32452015493, 2m17s; only a non-blocking Node-20 deprecation annotation).
  • 2026-08-21 — adversarial-review re-review @ 95d956ePASS WITH NOTES ("safe to merge from this attack"); both prior Minimors fixed on head.
  • 2026-08-21 — merge attempt fast-failed: CONFLICTING vs main (this PR + composer: reserve trailing icon pack, bound field to leftover (#734 / #737) #744 both rewrote the inline composer loop in ui.zig).
  • 2026-08-21 — Round 2 reconcile: merged origin/main into plan/harness-keymap (merge commit 160c8d3), resolved ui.zig only; kept composer: reserve trailing icon pack, bound field to leftover (#734 / #737) #744's chrome extraction and layered feat(harness): single keymap + leader + help overlay for shortcuts (plan #741) #746's dispatcher + request_submit on top. Gates green on head 160c8d3: test-rich 92/92 · 1290/1290 · invariants 221/221 · harness Debug compile clean · build-harness CI GREEN (run 32456843608). PR now MERGEABLE/CLEAN vs main.

Merge decision

PASS WITH NOTES: the 4 round-1 items are done and the merge-state conflict is reconciled. Head changed (95d956e160c8d3) in the reconcile, so the prior PASS is stale on the new head — the current head 160c8d3 needs a fresh adversarial-review before merge-pr can run. On a literal PASS (or PASS WITH NOTES) on 160c8d3 → invoke merge-pr.

…erlay fixes (review #746)

Address PR #746 adversarial-review CONCERNS (1 Major + 2 Minor + 1 Nit):

- Major L1: fromDvui no longer drops unrecognized non-modifier keys via
  `orelse continue`. New keymap.Key.unknown receives every unmapped printable
  (letters outside the subset, digits, punctuation, Backspace, f1-f4/f6+, nav),
  so keymap.match swallows+disarms it while a leader is pending (never lands in
  the prompt). Pure modifier/lock keys (shift/control/alt/command, menu, locks,
  dvui unknown) still return null -> pass through, so releasing/re-pressing
  Shift for the `?` command never disarms the leader. Host-tested in the pure
  keymap module (the seam the review said test-rich does not cover).
- Minor L1: help overlay sized against the FULL band (avail.w) instead of the
  leftover transcript width, so an open rail on a ~390px canvas still paints
  (avail.w=390 >= HELP_OVERLAY_MIN_W 300).
- Minor L8: overlay now iterates KEY_TABLE and presents each distinct action
  once (history_older + history_older_in no longer duplicate the Old-up row),
  help copy comes from row.help (overlay IS the table), and leader+Esc closes
  help first (help wins, one Esc) per docs/harness-limits L61.
- Nit L9: chord labels (↑/↓ arrows) painted via addTextMixed so the DejaVu
  symbols face renders them (Noto/Vera mono have no Arrows block; same tofu
  class as #732).

Zig-only: test-rich 88/88 (keymap 27->36) + invariants + harness Debug wasm +
fmt clean. No bridge exports changed (no new inv_*).

@btipling btipling left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adversarial review — PR #746 (re-review @ 95d956e)

Verdict: PASS WITH NOTES
Repo: btipling/invincible
Scope: mainplan/harness-keymap · 11 files · single keymap table + leader + help overlay (#741)
Lenses run: L1, L3, L5, L6, L8, L9 (skip: L2 no secrets/API; L4 no workflow; L7 no clone bind)
AGENTS.md read: yes · docs/feature-divide.md read (Wasm owns chords; DOM adds none)

Prior findings are closed on this HEAD:

Prior Status @ 95d956e
L1 Major fromDvui(...) orelse continue drops unmapped keys fixed — unmapped non-modifiers map to keymap.Key.unknown; match(.unknown) while leader pending is .swallow_leader; tests lock .unknown down/repeat/up
L1 Minor overlay no-op at ~390 with rail open fixedhelp_overlay.paint(0, scroll_y, avail.w, scroll_h, …) sizes against the full band (390 ≥ MIN_W 300)
L8 Minor overlay ≠ table / duplicate ↑ / two Esc fixedrow.help is the copy; consecutive same-action rows collapsed; leader+Esc with help open → help_close (and runAction disarms)
L9 Nit / tofu fixedmixed_text.addTextMixed on chord labels

Architecture still holds: one dvui.events() walk before textEntry, reserved deny-list never marked handled, request_submit after te.deinit. Caps unchanged. Pure modifiers still null so Shift for ? does not kill the window.

Findings

None remaining that survive self-refute.

Residual risk

  • fromDvui still lives in keymap_dispatch.zig (dvui-backed, not in test-rich). A later else => null would reopen the Major without a red host test; only keymap.match(.unknown) is locked.
  • dvui’s own .unknown key still returns null (pass through, never disarms). Exotic scancodes dvui does not name can still insert during the window.
  • Overlay dedup is consecutive row.action only. Inserting a row between history_older / history_older_in would paint ↑ twice again.
  • LEADER_TIMER_ID = 0x7410_0000_0000_0001 vs widget Id.extendId — assumed collision-safe.
  • Overlay ctx_scroll is a module-level var; reopen keeps the old offset.
  • Zig test-rich / wasm32 build-harness were not re-run in this review environment.

Merge guidance

  • PASS WITH NOTES: safe to merge from this attack; nits optional
  • Do not add a DOM window keydown / React cheatsheet
  • Do not silently change LEADER_WINDOW_MS / KEYMAP_MAX

What was not attacked

Host zig build test-rich / wasm32 build-harness, live canvas (Ctrl+/ , leader+?/h/1, Esc vs menus), dvui timer clock vs rAF, Production harness artifact deploy, #742 leader+t (not in this PR).

@btipling btipling left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adversarial review — PR #746 (re-review @ 160c8d3, merge of main)

Verdict: PASS WITH NOTES
Repo: btipling/invincible
Scope: mainplan/harness-keymap · 11 files (+ merge of #744 chrome + later main) · keymap + leader + help overlay (#741)
Lenses run: L1, L3, L4, L5, L6, L8, L9 (skip: L2 no secrets/API; L7 no clone bind)
AGENTS.md read: yes · docs/feature-divide.md read (Wasm owns chords; DOM adds none)

This HEAD is 95d956e (prior PASS WITH NOTES) merged with origin/main (ff9a1e8, #744 composer chrome extraction). Attack focused on the ui.zig conflict resolution.

Merge wiring is the right shape:

  • keymap_dispatch.dispatch still runs before composer_chrome.paintComposerChrome (the extracted textEntry).
  • request_submit is consumed after chrome returns res.typed.
  • No leftover for (dvui.events()) in ui.zig / queue_band / composer_chrome.
  • #734 reservation (field_w, wrap min=max) is intact in composer_chrome.zig; composer_layout and keymap both still register in test-rich.
  • Prior CONCERNS stay closed: Key.unknown swallow, overlay avail.w, help-wins Esc, addTextMixed arrows.
  • Docs still have both the Keyboard & focus table and the width-vs-height pad / reserved-strip rows.

Findings

None remaining that survive self-refute.

Candidate “dispatch is after queue_band.paint” does not survive: the queue editor is multiline = false, and the removed submitChord already ran after that textEntry. Composer (the widget that inserts \n on Enter, modifier ignored) is still built after dispatch.

Residual risk

  • fromDvui still lives in keymap_dispatch.zig (not in test-rich). A later else => null reopens the leader-swallow Major without a red host test.
  • dvui’s own .unknown key still returns null (pass through during the leader window).
  • Overlay dedup is consecutive row.action only.
  • LEADER_TIMER_ID vs widget Id.extendId — assumed collision-safe.
  • Overlay ctx_scroll persists across close/reopen.
  • Queue Ctrl+Enter save still depends on the single-line textEntry not marking the chord handled before dispatch (same order as the old inline scan).
  • Zig test-rich / wasm32 build-harness were not re-run in this review environment (merge is the kind of change that can fail those).

Merge guidance

  • PASS WITH NOTES: safe to merge from this attack; nits optional
  • Do not add a DOM window keydown / React cheatsheet
  • Do not silently change LEADER_WINDOW_MS / KEYMAP_MAX
  • Do not re-inline the composer textEntry in ui.zig (breaks #744 and this dispatch-before-TE contract)

What was not attacked

Host zig build test-rich / wasm32 build-harness on the merge commit, live canvas (Ctrl+/ , leader+h, queue Ctrl+Enter save), dvui timer clock vs rAF, Production harness artifact deploy, #742 leader+t.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

plan: harness keyboard shortcuts (keymap, leader, help overlay)

1 participant