Skip to content

plan: harness help overlay — wide modal table, wheel stays in panel, no duplicate-id red outlines #781

Description

@btipling

Plan header

Field Value
Status HANDOFF-READY
Date 2026-08-22
Type single (not parent / phase)
Parent N/A
Source issue #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/metrics.zig 69–77: HELP_OVERLAY_W=460, HELP_OVERLAY_H=320, MIN_W=300,
    MIN_H=200, MARGIN_X/Y=16 ✓
  • ui/help_overlay.zig paint: 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.floatingWindow exists 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.zig KEY_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

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) Keymap owns the chord table; overlay IS the table
Tests harness native/harness/src/help_overlay.test.zig (+ build.zig wiring) 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.zig paint Centered absolute dvui.box over the transcript band; not a floatingWindow/subwindow. Contains its own ctx_scroll scrollArea 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.zig for (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
Row strings source native/harness/src/keymap.zig KEY_TABLE + help_overlay.rowChord rowChord is a hardcoded parallel switch (row.action) (leader family pinned by tests); help copy from row.help
Open/close ui/keymap_dispatch.zig (166–175), ui/state.zig help_overlay_open 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)
dvui primitive for subwindow vendored native/harness/zig-pkg/dvui-…/src/Subwindows.zig, src/dvui.zig:2449 dvui.floatingWindow exists; dvui window/event dispatch matches a subwindow before an earlier sibling scrollArea in the same region
Doc docs/harness-limits.md 55–67 (Keyboard), 69–75 (Touch), 77–84 (Layout) “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
README.md N/A — no visitor-facing entry change
SECURITY.md N/A — no secrets / trust-boundary change
.env.example N/A — no new env

Caps table

Cap / ceiling Value Wire & its ceiling Defense Verdict
HELP_OVERLAY_W / HELP_OVERLAY_H (fixed 460×320 max) 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

  1. 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).
  2. 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).
  3. 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).
  4. 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.
  5. 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 zero duplicate 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 and state.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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    harnessHarness / agent UIui

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions