You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
#780 — harness: help overlay is tiny, not a table, leaks scroll, and paints red duplicate-id outlines
Branch
plan/help-overlay (implementation branch if used)
Layers
harness (Wasm / Zig + dvui)
Reusability impact
none — in-canvas chrome, no config seam or tenant binding
Production mutate?
no
Cloud ops path
N/A — no Production mutate
Living docs
docs/harness-limits.md; AGENTS.md considered (update only if metrics/caps change)
Human decision (2026-08-22)
Cap retirement approved — "460×320 is too small, cap change approved".
The fixed HELP_OVERLAY_W/H (460×320) metrics are retired in favor of band
fractions (HELP_OVERLAY_W_FRACTION/HELP_OVERLAY_H_FRACTION + min floors).
This is the governed-cap human sign-off; plan-review may now proceed. Status →
HANDOFF-READY.
Review notes (2026-08-22)
Reviewed by plan-review (plan-review skill) against main. All baseline claims
verified live:
ui/help_overlay.zigpaint: centered absolute dvui.box, own ctx_scroll
scrollArea but no subwindow, no id_extra (loop emits the same @src()
id), no fixed chord column ✓
ui.zig paint site 634–651: transcript scrollArea built first; overlay
painted after over the same region ✓
help_overlay.test.zig: string pins only, never paints → CI cannot catch
wires (1)–(4) ✓
build.zig 233–252: help_overlay test wired into test_rich with dvui_testing import ✓
dvui.floatingWindowexists in the vendored 0.5.0 snapshot at src/dvui.zig:2449 (subwindow event ordering primitives + src/Subwindows.zig),
so architectural decision B is on solid ground ✓
keymap.zigKEY_TABLE (line 187) is the single row+help source ✓
docs/harness-limits.md Keyboard section (55–67) describes the overlay as a
"TEAL panel over the transcript band" — no size/table/scroll rule today ✓
Sev
Axis
Finding
Resolution
RESOLVED — human approved
Cap governance
Retiring HELP_OVERLAY_W/H (460×320) — a raise of effective panel width / change to existing size caps — is a governed cap change. plan-review may suggest but never approve a cap raise/lower; only a human approves (AGENTS cap governance).
APPROVED (2026-08-22): "460×320 is too small, cap change approved". The fixed HELP_OVERLAY_W/H overlay caps are retired in favor of HELP_OVERLAY_W_FRACTION/HELP_OVERLAY_H_FRACTION + min floors. Pure visual-paint metric; no wire/transport ceiling (defense in the Caps table below). Block cleared → Status HANDOFF-READY.
Minor
Correctness
Goal 2 asserts "Click-outside … still close" as existing behavior. Live code closes the overlay only via Esc (help_close) / Ctrl+//leader toggle / New-Clear reset — there is no backdrop click-outside handler today.
Reword Goal 2 + test #2/#7: once the overlay becomes a modal floatingWindow, backdrop click-outside-to-close is a NEW behavior, not preserved — specify the backdrop wiring and add a deterministic test.
Minor
Harness UX / modal dispatch
A modal floatingWindow may consume pointer/scroll events at the subwindow wall; confirm backdrop click + Esc/Ctrl+//leader still route to the overlay's own closures (vs the keymap dispatcher) and the transcript state.transcript_scroll is untouched.
Covered partly by the existing risk row + test #2; add an explicit backdrop-click test row and an assertion that the leader/Esc chords still reach keymap_dispatch while the modal is open.
Verdict:HANDOFF-READY — the single surgical human decision (cap
retirement) was APPROVED (2026-08-22); the engineering plan is sound.
Summary
Fix the in-canvas help overlay (Ctrl/Cmd+/ · leader ? / Ctrl+I, then ? — #741, #761) so it is usable: (1) no red duplicate-id outlines — every looping widget gets a unique .id_extra; (2) pointer + wheel stay in the panel while open — the overlay
scrolls its own help list, the transcript does not move; (3) wide and tall
— it fills most of the transcript band (width-first), not a 460×320 postage
stamp; (4) a real two-column table — every chord and every help string
starts on a fixed x (chord column + remaining-width help column), with ordered
rows de-duplicated per distinct action and grey (WARM-muted) for context-off
rows. TEAL only; EMBER never.
Goals
#
Goal
Success signal
1
No red outlines
Every loop-created widget (row box, chord text, help text) carries .id_extra = i (or equivalent loop-unique id). Console shows zero duplicate widget id for the overlay; dvui errorOutline never fires on help rows
2
Pointer + wheel stay in the overlay while open
Wheel / trackpad over the panel scrolls the overlay's help list (ctx_scroll offset changes). The transcript's user_scroll / state.transcript_scroll does not change. Esc and bottom/non-modal backdrop click-outside close and are new explicit caps/behaviors once the overlay is a modal floatingWindow (not "preserved" — live code today only closes on Esc / Ctrl+//leader / New-Clear); Esc wins over busy cancel
3
Wide panel, most of the transcript band
On a desktop-width canvas (~1280) the panel uses the band minus the margin (≫ 460 wide). It is also tall, not a fixed 320-high square. No hard 460×320 cap; size is a named fraction of the band with a modest min floor. ~390 px remains on-canvas with no horizontal overflow and the list still scrolls internally
4
Two-column table layout
Every chord starts at the same x; every help string starts at the same x (chord column = fixed/min width, help column = remaining width). Rows de-duplicated per distinct action, KEY_TABLE order preserved (today's de-dupe). Grey (WARM-muted) marks context-off rows
5
Rows stay in KEY_TABLE order, one row per distinct action
No scrambling from id collision; no second sort unless a later plan asks
6
TEAL only (surface / accent / muted); EMBER never
Palette-only colors (palette.zig). Red appears only as dvui's debug errorOutline, which this plan removes
Non-goals / out of scope
Do not add a DOM / React cheatsheet (feature-divide: keymap is in-canvas only).
Do not raise KEYMAP_MAX (64) or LEADER_WINDOW_MS (800).
Forbidden wiring: dual DOM chat · secrets in Wasm · laptop-only Production ops.
Architectural decisions
Decision
Options considered
Choice
Why
Overlay host primitive
(A) absolute dvui.box painted after the transcript (today) / (B) modal floatingWindow subwindow
B — floatingWindow
A subwindow / floating window is a dvui first-class primitive (dvui.floatingWindow, verified in the vendored 0.5.0 snapshot at src/dvui.zig:2449 + src/Subwindows.zig). It participates in dvui's window/event dispatch before the earlier-built transcript scrollArea in the same screen region, so wheel/trackpad user_scroll in the panel hits the overlay's own ctx_scroll — this is the only robust way to satisfy Goal 2 without fragile manual hit-testing. New: specify backdrop click-outside-to-close explicitly (a modal window consumes its wall's events) — see Tests #2/#7
Panel size
(A) keep fixed HELP_OVERLAY_W/H = 460×320 and the @min paint cap / (B) fill the band minus a margin, width-first, with a named fraction + small min floor
B — fill band minus margin
Drops the postage-stamp defect. Panel = min(fraction·band, band − 2·margin) width and a tall height; a modest min floor keeps ~390 px usable and prevents degenerate paint. No GPA/heap in the frame path (pure rect math, frame-budget-safe). Cap-governed change: replaces existing HELP_OVERLAY_W/H — human approval granted (2026-08-22)
Column alignment
(A) keep two free textLayouts in a horizontal box (today) / (B) fixed chord column width + remaining-width help column
B — chord column width + help column
Every chord and every help string then paints at a stable x per row (table, Goal 4). Chord is mono / mixed-text (↑/↓ via mixed_text.addTextMixed, existing #732 arrows face); help takes the remaining width
Per-row unique ids
(A) reuse @src() each loop (today — the duplicate-id red outlines) / (B) .id_extra = i (or equivalent) on every looping widget
B — loop-unique id_extra
dvui uniqueness is by widget id; same source location + loop index ⇒ collision. Adding the loop index (or an explicit row counter) to box + chord text + help text removes Goal 1's red outlines
Layer placement
Concern
Layer
Path(s)
Rationale
Overlay event capture (wheel stays in panel)
harness
native/harness/src/ui/help_overlay.zig
Wasm chrome; dvui owns event dispatch
Size policy / metrics
harness
native/harness/src/ui/metrics.zig
Replace/supersede HELP_OVERLAY_W/H
Paint + open state
harness
native/harness/src/ui/help_overlay.zig, native/harness/src/ui.zig (paint site ~636), ui/keymap_dispatch.zig (open/close unchanged)
In-canvas chrome; DOM adds no keyboard UI / cheatsheet
Row strings
harness
native/harness/src/keymap.zig (KEY_TABLE stays the source of row+help)
Extend the existing dvui_testing host suite beyond string pins to paint / id_extra / column-x / wheel / backdrop-click locks
Living doc
docs
docs/harness-limits.md
Durable operator/behavior truth
Current baseline (live code)
Claim
Path / symbol
Notes
Panel size caps
native/harness/src/ui/metrics.zig 69–77
HELP_OVERLAY_W=460, HELP_OVERLAY_H=320, MIN_W=300, MIN_H=200, MARGIN_X/Y=16. paint does @min(HELP_OVERLAY_W, w−2·margin) — never grows with a wide canvas
Overlay paint
native/harness/src/ui/help_overlay.zigpaint
Centered absolute dvui.box over the transcript band; not a floatingWindow/subwindow. Contains its own ctx_scrollscrollArea but no window isolation
Paint site order
native/harness/src/ui.zig 634–651
Transcript scrollArea is built first (rect = leftover band); overlay paints after over the same screen region but is not a subwindow → wheel/trackpad user_scroll still hits state.transcript_scroll
Row loop / duplicate ids
help_overlay.zigfor (keymap.KEY_TABLE)
One horizontal box + chord textLayout + help textLayout per distinct action. No id_extra — every iteration emits the same @src() id → duplicate widget id … highlighted in red (dvui errorOutline, 1 px red stroke)
Alignment / columns
help_overlay.zig rows
Free textLayouts in a horizontal box — no chord column width → each help string starts at a different x
Ctrl/Cmd+/ · leader ? toggles; Esc (help_close) closes (wins over busy cancel); New/Clear/hydrate resets at ui.zig 520. No backdrop click-outside handler exists today — outside-click close is a NEW behavior when the overlay becomes a modal
Tests
native/harness/src/help_overlay.test.zig
Pins rowChord strings only (plan #761 Nit L6). Does not paint, so CI cannot catch (1)–(4)
Host test wiring
native/harness/build.zig 233–252
help_overlay test module wired into test_rich with dvui_testing import (no frame today; the module imports stop at mixed_text/unicode_face)
“Toggle the in-canvas help overlay (TEAL panel over the transcript band)” — no size, no table, no scroll-stays rule
Design
Treat the overlay as a modal in-canvas subwindow (dvui.floatingWindow, verified
in the vendored dvui 0.5.0 snapshot; if it proves unsuitable, the closest
equivalent that makes event matching hit it before the transcript scrollArea).
Behavior:
Size (width-first, no 460×320 cap): panel width = clamp of band-minus-margin; target a high fraction of w (named metric, e.g. HELP_OVERLAY_W_FRACTION) with a small absolute floor so ~390 px is still a usable on-canvas panel with internal scroll. Height likewise fills most of the band (HELP_OVERLAY_H_FRACTION) with a small floor. All as named constants in ui/metrics.zig (with the fixed HELP_OVERLAY_W/H retired from use or removed) — no magic numbers in paint. Cap-governed change — APPROVED by human (2026-08-22).
Rows: one row per distinct action in KEY_TABLE order (today's prev_action == row.action de-dupe preserved). Each row = chord column (fixed/min width; mono / mixed_text.addTextMixed for ↑/↓) + help column (remaining width). Every looping widget — row box, chord text, help text — gets .id_extra = i (loop index; or an explicit row counter that survives the de-dupe). Context-off rows stay grey WARM-muted, never EMBER.
Scroll isolation (Goal 2): because the overlay is now a subwindow painted with its own scroll area, wheel/trackpad over the panel scrolls ctx_scroll only; the transcript state.transcript_scroll is unaffected. Esc / Ctrl+/ / leader ? open/close behavior is preserved. Backdrop click-outside to close is a NEW explicit behavior — wire it on the modal wall and add a deterministic test (the modal window consumes the wall's events, so this is not free). The central-deinit / id-local rules must not be violated (unique .id_extra for tx and fl same-source loops).
Frame budget: pure dvui layout/paint over the static KEY_TABLE, exact same classes as the current paint — no GPA alloc, no host I/O, no per-frame parsing.
Edge cases: ~390 px canvas (panel on-canvas, no horizontal overflow, internal scroll — matches the no-h-scroll policy already applied to composer chrome); wide 1280 canvas (panel width ≫ 460); left rail open or closed (overlay already sizes against avail.w full band, plan plan: split ui.zig into src/ui/* facade (source #652) #656 / review L1); empty KEY_TABLE (defensive), keymap context changes mid-open (rows grey live via rowActive).
Cloud ops path
N/A — no Production mutate. In-canvas harness chrome only; docs are behavior-only updates. No GHA workflow added/extended (Zig build gates run in the agent workspace / CI as always — zig build test-rich, zig build harness -Doptimize=Debug).
Living docs plan
Surface
Change
Notes
docs/…docs/harness-limits.md
Keyboard section: rewrite the Ctrl/Cmd+/ row to describe the overlay as a wide two-column table over the transcript band; add a short "Help overlay" note that wheel/trackpad scrolls the help list inside the panel (does not scroll the transcript) and that it is a modal in-canvas subwindow filling most of the band
timeless; no phase/issue process artifacts
AGENTS.md
~line 299 ownership table row for Keyboard/help overlay: mention wide table + wheel-stays-in-panel + loop-unique id_extra. Only if metrics/caps land (name the fraction constants)
Update only if the metric cap names change; otherwise N/A
retired → replaced by HELP_OVERLAY_W_FRACTION / HELP_OVERLAY_H_FRACTION of the transcript band (plus small min floors)
No wire / transport carrier — these are pure dvui paint metrics (Wasm canvas geometry), not a Function body, Blob, Redis, or session meta budget
Removal (not a raise/lower of a transport ceiling): the overlay is a visual panel; the only ceilings that matter are the canvas rect (band w/h), which are the exact thing being filled. Budget accounting is trivial — panel ≤ band, internal scroll handles overflow. No residual transport risk
✅ APPROVED by human (2026-08-22). Cap retirement confirmed — "460×320 is too small, cap change approved". Pure visual-paint metric, no transport ceiling (defense above). No residual risk
No new transport caps are introduced by this plan beyond the two UI fraction
constants (named paint metrics on the canvas, not transport budgets). KEYMAP_MAX
(64) and LEADER_WINDOW_MS (800) are unchanged.
Implementation order
Rewrite native/harness/src/ui/help_overlay.zig:
Wrap rows in a floatingWindow/subwindow with its own ctx_scroll so wheel/events land in the panel; wire an explicit backdrop click-outside-to-close on the modal wall.
Add .id_extra = i (loop-unique) to row box + chord text + help text.
Add a chord column min/fixed width; help text takes remaining width.
Size = fill band minus margin (width-first fractions from a NEW named metrics table).
Update native/harness/src/ui/metrics.zig: add HELP_OVERLAY_W_FRACTION / HELP_OVERLAY_H_FRACTION + min-floor constants; retire/stop using HELP_OVERLAY_W/H (or repurpose explicitly) — human-approved cap change (2026-08-22).
Confirm ui.zig paint site (634–651) still passes the full band rect and passes the right open/close state unchanged; keymap_dispatch.zig chords unchanged (Esc / Ctrl+//leader still reach the dispatcher while the modal is open).
Extend native/harness/src/help_overlay.test.zig to the new paint/id_extra/column-x/wheel/backdrop-click locks (see Testing) and wire any new needed dvui_testing import in build.zig.
Update docs/harness-limits.md (+ AGENTS.md if the metric names land).
Testing
#
Case
Layer
Type
Command / method
1
Open overlay; console
harness
operator / host
Open via Ctrl/Cmd+/; browser console must show zeroduplicate widget id for help_overlay widgets; no red outlines
2
Wheel over the panel
harness
integration (dvui_testing)
With overlay open, inject a wheel event over the panel rect; assert ctx_scroll.offset moves andstate.transcript_scroll (or user_scroll) does not. Uses the same testing-backend pattern as the existing composer/layout host tests
2b
Backdrop click-outside closes (NEW)
harness
integration (dvui_testing)
With the modal overlay open, inject a click on the modal wall outside the panel; assert the overlay closes (state flips) and the underlying transcript/composer is not activated/steered. Explicit behavior once it is a modal — the modal consumes wall events
2c
Leader/Esc chord still reaches the keymap (modal dispatch)
harness
integration (dvui_testing)
While the modal is open, assert Esc (and Ctrl/Cmd+/ toggle) still land on keymap_dispatch and close the overlay — the modal wall must not swallow them
3
Wide canvas (~1280)
metrics / harness
unit / integration
Assert panel width ≫ 460 and uses most of the band (fraction constant); tall, not 320
4
Table columns
harness
integration (paint)
After painting several rows, assert all chord textLayout left edges are equal and all help textLayout left edges are equal
5
~390 px
metrics / harness
integration
Panel on-canvas; no horizontal overflow; internal scroll still works; help_overlay.paint returns early correctly when the band is below the min floor
6
Row order / de-dupe (~390 px context)
harness
unit
One row per distinct action in KEY_TABLE order (existing prev_action de-dupe holds); ordering stable
7
Esc / Ctrl+/ / leader ? close
existing keymap
harness unit / operator
Unchanged (Esc wins over busy cancel; overlay is modal, backdrop click-outside closes)
8
Operator
browser
operator checklist
Overlay is a wide table, chord/help columns align, wheel stays inside, no red boxes, grey context-off rows
Gates (agent workspace or CI, per AGENTS.md): zig fmt --check on the touched .zig files; zig build test-rich (fresh/extended help_overlay paint/id_extra/column-x/wheel/backdrop locks); zig build test-rich-invariants; zig build harness -Doptimize=Debug (full Wasm compile — catches real compile errors like optional-unwrap / use-after-deinit). Wheel-over-overlay is also operator-gated (test #2 is the best harness proxy).
Definition of done
Goals 1–6 met (no duplicate-id outlines; wheel stays in panel; wide fill-band panel; aligned two-column table; KEY_TABLE order + de-dupe preserved; TEAL only, no EMBER/red); backdrop click-outside-to-close and leader/Esc-through-modal wired and tested (2b/2c)
Human decision: HELP_OVERLAY_W/H retirement approved (cap governance) — APPROVED (2026-08-22); plan is HANDOFF-READY
zig fmt --check passes on touched .zig
zig build test-rich green (help_overlay tests extended beyond string pins — at least paint/id_extra + column-x + wheel + backdrop locks)
zig build test-rich-invariants green
zig build harness -Doptimize=Debug green (full Wasm compile)
No dual-chat regression (keymap stays in-canvas; feature-divide intact)
Cloud ops: explicit N/A — no Production mutate
Living docs:docs/harness-limits.md updated (timeless; no phase/issue theater); AGENTS.md updated only if metric/cap names change
README / SECURITY / .env.example considered → N/A with one-line why
Risks & mitigations
Risk
Mitigation
dvui floatingWindow event/no-window semantics differ across the vendored 0.5.0 snapshot (subwindow ordering)
Verify against the vendored dvui source + widgetpedia example during implementation; if floatingWindow is unsuitable, fall back to the closest subwindow/window-ordering primitive that captures wheel before the transcript scrollArea. Test #2 is the executable lock
Modal window wall swallows backdrop/Esc chords
Explicit backdrop click-outside-to-close wiring (NEW behavior, test 2b) + assert Esc/Ctrl+//leader still reach the keymap dispatcher (test 2c)
Changing the size metric (a cap retirement) surprises reviewers
Caps table above is explicit it is a pure visual metric with no transport ceiling; human approval obtained (2026-08-22) — no open risk
id_extra collisions across widget types in the same loop (box vs text share an index)
Use distinct id bases per widget kind (e.g. i for the box, i + KEYMAP_MAX for chord, i + 2·KEYMAP_MAX for help) or a single per-row counter namespace; verified by console test #1 and integration test #4
Wheel test flakiness in dvui_testing
Use the same deterministic event-injection pattern the existing composer/layout host tests use; assert offsets (not pixel equality)
~390 px still looks cramped
Panel stays on-canvas with internal scroll (no horizontal overflow) — matches the existing no-h-scroll policy; rows wrap/scrolling is acceptable at phone widths
Open questions
CAP RETIREMENT — RESOLVED/APPROVED. The retirement of the fixed HELP_OVERLAY_W/H (460×320) metrics in favor of band fractions
(HELP_OVERLAY_W_FRACTION/HELP_OVERLAY_H_FRACTION + min floors) was approved by the human (2026-08-22) — "460×320 is too small, cap change
approved". No open items; Status HANDOFF-READY.
(Team note, not blocking) Backdrop click-outside-to-close and leader/Esc-through-modal are NEW explicit behaviors once the overlay is a modal — confirm scope at plan-review/implement time (tests 2b/2c lock them).
References
Source issue:#780 — the bug this plan ships against.
Prior plans: #741 (keymap + overlay origin) · #761 (leader chord copy — the existing help_overlay.test.zig pin its strings).
Plan header
plan/help-overlay(implementation branch if used)docs/harness-limits.md; AGENTS.md considered (update only if metrics/caps change)Human decision (2026-08-22)
Cap retirement approved — "460×320 is too small, cap change approved".
The fixed
HELP_OVERLAY_W/H(460×320) metrics are retired in favor of bandfractions (
HELP_OVERLAY_W_FRACTION/HELP_OVERLAY_H_FRACTION+ min floors).This is the governed-cap human sign-off; plan-review may now proceed. Status →
HANDOFF-READY.
Review notes (2026-08-22)
Reviewed by plan-review (plan-review skill) against
main. All baseline claimsverified live:
ui/metrics.zig69–77:HELP_OVERLAY_W=460,HELP_OVERLAY_H=320,MIN_W=300,MIN_H=200,MARGIN_X/Y=16 ✓ui/help_overlay.zigpaint: centered absolutedvui.box, ownctx_scrollscrollArea but no subwindow, no
id_extra(loop emits the same@src()id), no fixed chord column ✓
ui.zigpaint site 634–651: transcriptscrollAreabuilt first; overlaypainted after over the same region ✓
help_overlay.test.zig: string pins only, never paints → CI cannot catchwires (1)–(4) ✓
build.zig233–252:help_overlaytest wired intotest_richwithdvui_testingimport ✓dvui.floatingWindowexists in the vendored 0.5.0 snapshot atsrc/dvui.zig:2449(subwindow event ordering primitives +src/Subwindows.zig),so architectural decision B is on solid ground ✓
keymap.zigKEY_TABLE(line 187) is the single row+help source ✓docs/harness-limits.mdKeyboard section (55–67) describes the overlay as a"TEAL panel over the transcript band" — no size/table/scroll rule today ✓
HELP_OVERLAY_W/H(460×320) — a raise of effective panel width / change to existing size caps — is a governed cap change. plan-review may suggest but never approve a cap raise/lower; only a human approves (AGENTS cap governance).HELP_OVERLAY_W/Hoverlay caps are retired in favor ofHELP_OVERLAY_W_FRACTION/HELP_OVERLAY_H_FRACTION+ min floors. Pure visual-paint metric; no wire/transport ceiling (defense in the Caps table below). Block cleared → Status HANDOFF-READY.help_close) / Ctrl+//leader toggle / New-Clear reset — there is no backdrop click-outside handler today.floatingWindow, backdrop click-outside-to-close is a NEW behavior, not preserved — specify the backdrop wiring and add a deterministic test.floatingWindowmay consume pointer/scroll events at the subwindow wall; confirm backdrop click + Esc/Ctrl+//leader still route to the overlay's own closures (vs the keymap dispatcher) and the transcriptstate.transcript_scrollis untouched.keymap_dispatchwhile the modal is open.Verdict: HANDOFF-READY — the single surgical human decision (cap
retirement) was APPROVED (2026-08-22); the engineering plan is sound.
Summary
Fix the in-canvas help overlay (Ctrl/Cmd+/ · leader
?/Ctrl+I, then ?—#741,
#761) so it is usable: (1)
no red duplicate-id outlines — every looping widget gets a unique
.id_extra; (2) pointer + wheel stay in the panel while open — the overlayscrolls its own help list, the transcript does not move; (3) wide and tall
— it fills most of the transcript band (width-first), not a 460×320 postage
stamp; (4) a real two-column table — every chord and every help string
starts on a fixed x (chord column + remaining-width help column), with ordered
rows de-duplicated per distinct action and grey (WARM-muted) for context-off
rows. TEAL only; EMBER never.
Goals
.id_extra = i(or equivalent loop-unique id). Console shows zeroduplicate widget idfor the overlay; dvuierrorOutlinenever fires on help rowsctx_scrolloffset changes). The transcript'suser_scroll/state.transcript_scrolldoes not change. Esc and bottom/non-modal backdrop click-outside close and are new explicit caps/behaviors once the overlay is a modal floatingWindow (not "preserved" — live code today only closes on Esc / Ctrl+//leader / New-Clear); Esc wins over busy cancelerrorOutline, which this plan removesNon-goals / out of scope
KEYMAP_MAX(64) orLEADER_WINDOW_MS(800).Architectural decisions
dvui.boxpainted after the transcript (today) / (B) modalfloatingWindowsubwindowfloatingWindowdvui.floatingWindow, verified in the vendored 0.5.0 snapshot atsrc/dvui.zig:2449+src/Subwindows.zig). It participates in dvui's window/event dispatch before the earlier-built transcriptscrollAreain the same screen region, so wheel/trackpaduser_scrollin the panel hits the overlay's ownctx_scroll— this is the only robust way to satisfy Goal 2 without fragile manual hit-testing. New: specify backdrop click-outside-to-close explicitly (a modal window consumes its wall's events) — see Tests #2/#7HELP_OVERLAY_W/H= 460×320 and the@minpaint cap / (B) fill the band minus a margin, width-first, with a named fraction + small min floormin(fraction·band, band − 2·margin)width and a tall height; a modest min floor keeps ~390 px usable and prevents degenerate paint. No GPA/heap in the frame path (pure rect math, frame-budget-safe). Cap-governed change: replaces existingHELP_OVERLAY_W/H— human approval granted (2026-08-22)textLayouts in a horizontal box (today) / (B) fixed chord column width + remaining-width help column↑/↓viamixed_text.addTextMixed, existing #732 arrows face); help takes the remaining width@src()each loop (today — the duplicate-id red outlines) / (B).id_extra = i(or equivalent) on every looping widgetid_extraLayer placement
native/harness/src/ui/help_overlay.zignative/harness/src/ui/metrics.zigHELP_OVERLAY_W/Hnative/harness/src/ui/help_overlay.zig,native/harness/src/ui.zig(paint site ~636),ui/keymap_dispatch.zig(open/close unchanged)native/harness/src/keymap.zig(KEY_TABLEstays the source of row+help)native/harness/src/help_overlay.test.zig(+build.zigwiring)docs/harness-limits.mdCurrent baseline (live code)
native/harness/src/ui/metrics.zig69–77HELP_OVERLAY_W=460,HELP_OVERLAY_H=320,MIN_W=300,MIN_H=200,MARGIN_X/Y=16.paintdoes@min(HELP_OVERLAY_W, w−2·margin)— never grows with a wide canvasnative/harness/src/ui/help_overlay.zigpaintdvui.boxover the transcript band; not afloatingWindow/subwindow. Contains its ownctx_scrollscrollAreabut no window isolationnative/harness/src/ui.zig634–651scrollAreais built first (rect = leftover band); overlay paints after over the same screen region but is not a subwindow → wheel/trackpaduser_scrollstill hitsstate.transcript_scrollhelp_overlay.zigfor (keymap.KEY_TABLE)box+ chordtextLayout+ helptextLayoutper distinct action. Noid_extra— every iteration emits the same@src()id →duplicate widget id … highlighted in red(dvuierrorOutline, 1 px red stroke)help_overlay.zigrowsnative/harness/src/keymap.zigKEY_TABLE+help_overlay.rowChordrowChordis a hardcoded parallelswitch (row.action)(leader family pinned by tests); help copy fromrow.helpui/keymap_dispatch.zig(166–175),ui/state.zighelp_overlay_open?toggles; Esc (help_close) closes (wins over busy cancel); New/Clear/hydrate resets atui.zig520. No backdrop click-outside handler exists today — outside-click close is a NEW behavior when the overlay becomes a modalnative/harness/src/help_overlay.test.zigrowChordstrings only (plan #761 Nit L6). Does not paint, so CI cannot catch (1)–(4)native/harness/build.zig233–252help_overlaytest module wired intotest_richwithdvui_testingimport (no frame today; the module imports stop at mixed_text/unicode_face)native/harness/zig-pkg/dvui-…/src/Subwindows.zig,src/dvui.zig:2449dvui.floatingWindowexists; dvui window/event dispatch matches a subwindow before an earlier sibling scrollArea in the same regiondocs/harness-limits.md55–67 (Keyboard), 69–75 (Touch), 77–84 (Layout)Design
Treat the overlay as a modal in-canvas subwindow (
dvui.floatingWindow, verifiedin the vendored dvui 0.5.0 snapshot; if it proves unsuitable, the closest
equivalent that makes event matching hit it before the transcript
scrollArea).Behavior:
w(named metric, e.g.HELP_OVERLAY_W_FRACTION) with a small absolute floor so ~390 px is still a usable on-canvas panel with internal scroll. Height likewise fills most of the band (HELP_OVERLAY_H_FRACTION) with a small floor. All as named constants inui/metrics.zig(with the fixedHELP_OVERLAY_W/Hretired from use or removed) — no magic numbers in paint. Cap-governed change — APPROVED by human (2026-08-22).KEY_TABLEorder (today'sprev_action == row.actionde-dupe preserved). Each row = chord column (fixed/min width; mono /mixed_text.addTextMixedfor↑/↓) + help column (remaining width). Every looping widget — row box, chord text, help text — gets.id_extra = i(loop index; or an explicit row counter that survives the de-dupe). Context-off rows stay grey WARM-muted, never EMBER.ctx_scrollonly; the transcriptstate.transcript_scrollis unaffected. Esc / Ctrl+/ / leader?open/close behavior is preserved. Backdrop click-outside to close is a NEW explicit behavior — wire it on the modal wall and add a deterministic test (the modal window consumes the wall's events, so this is not free). The central-deinit / id-local rules must not be violated (unique.id_extrafortxandflsame-source loops).KEY_TABLE, exact same classes as the current paint — no GPA alloc, no host I/O, no per-frame parsing.avail.wfull band, plan plan: split ui.zig into src/ui/* facade (source #652) #656 / review L1); emptyKEY_TABLE(defensive), keymap context changes mid-open (rows grey live viarowActive).Cloud ops path
N/A — no Production mutate. In-canvas harness chrome only; docs are behavior-only updates. No GHA workflow added/extended (Zig build gates run in the agent workspace / CI as always —
zig build test-rich,zig build harness -Doptimize=Debug).Living docs plan
docs/…docs/harness-limits.mdAGENTS.mdid_extra. Only if metrics/caps land (name the fraction constants)README.mdSECURITY.md.env.exampleCaps table
HELP_OVERLAY_W/HELP_OVERLAY_H(fixed 460×320 max)HELP_OVERLAY_W_FRACTION/HELP_OVERLAY_H_FRACTIONof the transcript band (plus small min floors)No new transport caps are introduced by this plan beyond the two UI fraction
constants (named paint metrics on the canvas, not transport budgets).
KEYMAP_MAX(64) and
LEADER_WINDOW_MS(800) are unchanged.Implementation order
native/harness/src/ui/help_overlay.zig:floatingWindow/subwindow with its ownctx_scrollso wheel/events land in the panel; wire an explicit backdrop click-outside-to-close on the modal wall..id_extra = i(loop-unique) to row box + chord text + help text.native/harness/src/ui/metrics.zig: addHELP_OVERLAY_W_FRACTION/HELP_OVERLAY_H_FRACTION+ min-floor constants; retire/stop usingHELP_OVERLAY_W/H(or repurpose explicitly) — human-approved cap change (2026-08-22).ui.zigpaint site (634–651) still passes the full band rect and passes the right open/close state unchanged;keymap_dispatch.zigchords unchanged (Esc / Ctrl+//leader still reach the dispatcher while the modal is open).native/harness/src/help_overlay.test.zigto the new paint/id_extra/column-x/wheel/backdrop-click locks (see Testing) and wire any new needed dvui_testing import inbuild.zig.docs/harness-limits.md(+ AGENTS.md if the metric names land).Testing
duplicate widget idforhelp_overlaywidgets; no red outlinesctx_scroll.offsetmoves andstate.transcript_scroll(oruser_scroll) does not. Uses the same testing-backend pattern as the existing composer/layout host testskeymap_dispatchand close the overlay — the modal wall must not swallow themhelp_overlay.paintreturns early correctly when the band is below the min floorKEY_TABLEorder (existingprev_actionde-dupe holds); ordering stable?closeGates (agent workspace or CI, per AGENTS.md):
zig fmt --checkon the touched.zigfiles;zig build test-rich(fresh/extended help_overlay paint/id_extra/column-x/wheel/backdrop locks);zig build test-rich-invariants;zig build harness -Doptimize=Debug(full Wasm compile — catches real compile errors like optional-unwrap / use-after-deinit). Wheel-over-overlay is also operator-gated (test #2 is the best harness proxy).Definition of done
zig fmt --checkpasses on touched.zigzig build test-richgreen (help_overlay tests extended beyond string pins — at least paint/id_extra + column-x + wheel + backdrop locks)zig build test-rich-invariantsgreenzig build harness -Doptimize=Debuggreen (full Wasm compile)docs/harness-limits.mdupdated (timeless; no phase/issue theater); AGENTS.md updated only if metric/cap names change.env.exampleconsidered → N/A with one-line whyRisks & mitigations
floatingWindowevent/no-window semantics differ across the vendored 0.5.0 snapshot (subwindow ordering)widgetpediaexample during implementation; iffloatingWindowis unsuitable, fall back to the closest subwindow/window-ordering primitive that captures wheel before the transcript scrollArea. Test #2 is the executable lockid_extracollisions across widget types in the same loop (box vs text share an index)ifor the box,i + KEYMAP_MAXfor chord,i + 2·KEYMAP_MAXfor help) or a single per-row counter namespace; verified by console test #1 and integration test #4Open questions
HELP_OVERLAY_W/H(460×320) metrics in favor of band fractions(
HELP_OVERLAY_W_FRACTION/HELP_OVERLAY_H_FRACTION+ min floors) wasapproved by the human (2026-08-22) — "460×320 is too small, cap change
approved". No open items; Status HANDOFF-READY.
References
help_overlay.test.zigpin its strings).