From f0afec38126fb645e1cb707462ac1a46293be0c5 Mon Sep 17 00:00:00 2001 From: "claude@clouddev1" Date: Thu, 11 Jun 2026 09:59:51 +0000 Subject: [PATCH] docs: session handoff 64 + ADR-0047 implemented (#22/#24) Flip ADR-0047 Status -> implemented (commits f879d54..2d0f4b2, phased A->B->C + flat-rectangle restyle); update the README index entry to match (implemented, flat black-on-yellow rectangles, final 2290-green tally, website-cast follow-up noted). Add session handoff 64 covering #24 (vi load-picker nav) and #22 (ADR-0047 demo overlay layer). --- docs/adr/0047-demonstration-overlay-layer.md | 27 +++- docs/adr/README.md | 2 +- docs/handoff/20260611-handoff-64.md | 140 +++++++++++++++++++ 3 files changed, 167 insertions(+), 2 deletions(-) create mode 100644 docs/handoff/20260611-handoff-64.md diff --git a/docs/adr/0047-demonstration-overlay-layer.md b/docs/adr/0047-demonstration-overlay-layer.md index 3d7eba9..4258e95 100644 --- a/docs/adr/0047-demonstration-overlay-layer.md +++ b/docs/adr/0047-demonstration-overlay-layer.md @@ -2,13 +2,38 @@ ## Status -Accepted (2026-06-10). Addresses Gitea **#22**. Builds the in-app +Accepted (2026-06-10); **implemented 2026-06-11**, phased A→B→C (closes +Gitea **#22**). Addresses Gitea **#22**. Builds the in-app overlay/annotation primitive that screencast recording (ADR-website-001 §2, the `autocast` pipeline) and a future guided-lesson system both need. Adjacent to ADR-0046 (the nav-mode sidebar overlay it must coexist with) and unblocks the polished version of the assistive-editor and projects (`#24`) casts. +**Implementation (commits `f879d54` → `2d0f4b2`).** Phase A +(`f879d54`): `--demo` flag + `RDBMS_PLAYGROUND_DEMO` env → +`App.demo_mode`, mirroring the `--no-undo` plumbing; help text mentions +only the visible badges (the `Ctrl+]` caption trigger stays +low-profile, D6). Phase B (`2584e76`): automatic keystroke badges — pure +`demo_badge_label`, set in `App::update` before the modal gate, expired +by a ~1.5 s runtime timer via the new `nearest_deadline` helper that +extends the time-boxed-`recv` arm condition **without** regressing the +ADR-0027 indicator debounce (the rewrite tracks `Instant` deadlines; +verified equivalent). Phase C (`241f60c`): the stealth `Ctrl+]` +caption buffer in `App::update`, intercepted before the modal gate so +captions work over the load picker. Post-build (`2d0f4b2`, user +decision): the overlays render as **flat filled yellow rectangles** (no +border glyphs, one-cell text margin) to read as a distinct callout. A +whole-implementation `/runda` pass returned **PASS** with no blockers; +the only untested wiring is the `run_loop` badge timer (not unit-testable +in isolation — same posture as the existing `IndicatorDebounce`; the +pure pieces are all tested). One intentional, user-acknowledged +behaviour: `Ctrl-C` is inert while capturing (every non-`Ctrl+]` key is, +by spec; exit capture with `Ctrl+]`). Tests: 2290 passing / 0 failing / +0 skipped (Tier-1 label fn + caption FSM + `nearest_deadline`, Tier-2 +dark/light/stacked/wrapped/clamp snapshots + black-on-yellow style, +CLI parse/env); clippy clean. + All primary forks and the visual placement were **user-confirmed** — including the two follow-ups settled after the first draft: the trigger key (**`Ctrl+]`**, the maximally-obscure valid single-byte code, over diff --git a/docs/adr/README.md b/docs/adr/README.md index 9157c3f..f73b802 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -52,4 +52,4 @@ This directory contains the project's ADRs, recorded per - [ADR-0044 — Relationship visualization (two-table connector diagrams)](0044-relationship-visualization.md) — **Accepted 2026-06-09; implemented 2026-06-10** (closes `requirements.md` V1; second `/runda` pass over the implementation; §3 last-resort helper line considered and rejected). Resolves **ADR-0016 OOS-1** and closes the open half of `requirements.md` **V1** ("a selected relationship as two tables joined by a line"). Renders a relationship as **Style A** (two structure boxes + connector). **Reach = "relationship-relevant"** (user-chosen over global / show-only): diagrams on the surfaces where the relationship is the *subject* — `show relationship ` (one full diagram), `show table ` (T's structure box then a **Relationships** section of **stacked compact** per-relationship diagrams — chosen over a focal-centred subgraph: no crossing lines, scales via scroll, two-boxes-wide fits any terminal), and relationship DDL echoes (`add`/`drop`/`modify relationship`); incidental DDL echoes (`add column`, `drop index`, `change column`, plain `create table`) keep the terse prose, via a `Diagram`|`Prose` render mode on `render_structure`. Reading convention **child(FK)-left / parent-right, arrow →, `n`…`1` cardinality**, applied uniformly; every box gets a **bold title row + rule** so the name can't read as a column. **Compound FKs** (ADR-0043) route one connector per positional pair + an explicit pairing line. **Width-aware** (first in the codebase) but **App-side**: `render_structure`/diagram rendering runs in `app.rs` (the worker only returns `TableDescription`s), a new `App::last_output_width` (set from `ui.rs`) drives side-by-side vs a **vertical-stack** fallback + last-resort "run `show relationship`" pointer; rendered once at command time, **no live reflow** (V4). `show relationship`'s worker path (`do_show_one`, prose-only) is restructured to return both endpoint `TableDescription`s. Styling reuses **ADR-0028** App-side styled runs (new classes: table-name/key/connector/cardinality/action) — no worker→UI contract change. **Partially supersedes ADR-0016 §5** (prose block replaced on relationship-subject surfaces, retained on incidental ones); extends §4 (layout width-awareness, still no cell truncation) and §6 (per-span theming). Tests: insta snapshots (single, compound, vertical fallback, helper line, self-referential, multi-rel `show table`) + width-threshold/routing unit tests + Tier-3 wiring; enumerated prose-fallout updates (`output_render.rs:121/135/793`, the relationships snapshot, `walking_skeleton.rs:477/530`). A `/runda` DA pass corrected three inverted-architecture claims (App-side rendering, untracked width, prose-in-worker show-relationship) before acceptance. OOS: user-configurable display setting (OOS-7), live reflow (V4), whole-DB ER export (V3), m:n (C4), ASCII fallback (ADR-0016 OOS-5) - [ADR-0045 — `create m:n relationship` convenience command (C4)](0045-mn-convenience.md) — **Accepted + implemented 2026-06-10** (closes `requirements.md` **C4**; all forks user-confirmed + a `/runda` DA pass that verified the `do_create_table` reuse against code and corrected the "no PK-less tables" assumption — advanced SQL `create table t (a int)` has none, so a parent-PK guard is retained). Implementation corrected a second ADR premise: "the walker already dispatches multiple nodes per entry word" held only in *advanced* mode — two simple-mode spots (dispatcher `decide`, completion continuation-merge) assumed ≤1 DSL form per entry word and were generalized **behaviour-preservingly** (dispatch reduces to the old single-candidate commit; completion merge gated on `simple_count > 1`). Junction echo wired (`render_create_m2n`, round-trips as SQL). `create m:n relationship from to [as ]` generates a junction table with one FK column per parent PK column, a **compound PK over all the FK columns** (the textbook junction — the pair is unique, no duplicate links), and **two 1:n relationships**, all in **one transaction = one undo step** (built by reusing `do_create_table`, which already takes `foreign_keys` + writes relationship metadata — no batch bracketing). Forks all user-chosen: junction PK = compound-over-FKs (vs surrogate serial / no PK); referential actions = **`CASCADE`** on delete+update (vs NO ACTION / RESTRICT); naming = auto `{T1}_{T2}` + optional `as` (vs auto-only); available in **both modes** (Simple-category DSL, like the sibling relationship commands). FK columns named `{parent_table}_{pk_column}` (disambiguates shared `id`; generalises to compound parents via ADR-0043), typed via `fk_target_type` (ADR-0011). A distinct `Command::CreateM2nRelationship` (not lowered to `CreateTable`) preserves command identity (X5) and lets the teaching echo speak in m:n terms. Cross-cutting wiring enumerated: separate `CREATE_M2N` `CommandNode` (own `help_id`/`usage_ids`), `("m","m:n")` completion composite, `HintMode`s, grammar-driven highlighting, `help`/`help create`, `parse_error_pedagogy` near-miss matrix, teaching echo. OOS: **self-referential m:n** (`from T to T`) refused outright (user-confirmed "full stop" — directional column-naming is more than this beginner convenience warrants); per-relationship action overrides; extra junction payload columns; m:n diagram echo; renaming the auto-generated relationships - [ADR-0046 — Schema sidebar focus/navigation mode and responsive input & hint layout (UI #20/#21/#23)](0046-sidebar-navigation-and-responsive-input-hint.md) — **Accepted + implemented 2026-06-10, phased A→B→C** (8 commits `9f5f76b`…`22bec61`; closes Gitea **#20** hint jumpiness, **#21** left-column improvements, **#23** long input — all forks user-confirmed, including the persistent show/hide toggle which is **deferred**: the Ctrl-O peek covers #21's "keystroke to show and hide"). Two decisions landed differently from the draft (recorded inline): relationship data on **`App`** not `SchemaCache` (DB2); the nav overlay clears **only the sidebar strip + a one-column gutter**, panels staying visible behind (DC2). Treats the three UI issues as one coupled decision because they share the terminal's width/height budget. **Phase A (input & hint):** the hint panel's height becomes a function of **terminal geometry, fixed between resizes** (not of hint content), eliminating the #20 jump at its source — measured catalog shows ≥ ~54-col right-column width never needs > 2 hint lines, so 3 lines is a rare narrow-terminal-only case; height buckets `H<40` compact (input 1 row + horizontal scroll / hint 2) vs `H≥40` comfortable (input 2 rows soft-wrap / hint 2), output `Min(5)` honoured first under degradation; input gains horizontal scroll (`input_scroll_offset`, single logical `String` — **not** I1 multi-line) and 2-row soft-wrap display when tall, preserving ADR-0027's 6-col indicator reserve. **Phase B (sidebar):** the 26-col Tables column is **kept but made optional and richer** (not deleted — pedagogy wins ties) — **width-derived session-only** visibility (visible iff width > 90 or a Ctrl-O peek is active — no stored field; hides at width ≤ 90 so the 90-col screencasts drop it; ADR-0015 format untouched), plus a **relationships panel** rendered narrow with endpoints broken at the arrow, ellipsized — a **separate sibling panel** that **overrides S2**'s nested-list extension model (relationships are cross-table). the full records live on a new **`App.relationships`** field (revised from the ADR's original `SchemaCache.relationship_details` at implementation — `SchemaCache` is walker-facing and needs only the names, kept in `relationships: Vec`; details are UI-only, so `App` mirrors `app.tables` and avoids ~23 fixture edits), delivered by `Database::read_all_relationships` + an `AppEvent::RelationshipsRefreshed`; the two left panels split vertically with the relationships panel floored at 5 rows ("(none)" when empty) and capped at 50 % of the column (DB4). **Phase C (navigation mode):** **`Ctrl-O`** enters a focus cycle (Input → Tables → Relationships → Input; `Esc` exits) orthogonal to the ADR-0003 input mode — **`Ctrl-B` was rejected on review as the default tmux prefix** (unreachable inside tmux); the focused panel **expands to ~40–50 cols as a `Clear` overlay** (right panels stay unchanging underneath) and scrolls via **Up/Down (line) + PageUp/PageDown (page)** (context-rebind, reusing the output-scroll viewport mechanism), with an accent focus border; all non-nav keys inert in nav mode (and nav keys inert while a modal is open). Forks all user-chosen: keep-optional-richer (vs remove/narrow); navigation-mode (vs modeless modifier scroll); `Ctrl-O` (Ctrl-B rejected = tmux prefix); overlay (vs layout re-split); inert-non-nav-keys; geometry-fixed hint height; `H<40/≥40` thresholds; session-only persistence; Up/Down line-scroll; **separate relationships panel overriding S2**; **no hint-area toggle** (S4's stale "keyboard-toggleable" claim struck — never implemented, unwanted). A pre-build `/runda` DA pass drove these corrections: caught the `Ctrl-B`/tmux collision, the `SchemaCache` retype that would have broken completion, the 2-row-input/indicator placement, the missing nav-mode key disposition + modal gate, and three unreferenced requirements (S1 evolved, S2 overridden, S4 corrected); also cross-checked open issue **#22** (overlay/annotation layer — separate ADR, adjacent). OOS: true multi-line input (I1); readline shortcuts (I1b); cross-session sidebar persistence; output as a third nav focus; relationship search/edit from the panel; hint-area toggle; #22's annotation layer. Accepted consequence: the 90-col visibility threshold makes a terminal's output *narrower* when widened across the boundary (sidebar appears) -- [ADR-0047 — Demonstration overlay layer (keystroke badges + step captions)](0047-demonstration-overlay-layer.md) — **Accepted 2026-06-10** (Gitea **#22**; no `requirements.md` item — tracked by issue + ADR per convention; all forks user-confirmed + a `/runda` pass that produced 10 tightening findings). An in-app **demonstration mode** (`--demo` flag / `RDBMS_PLAYGROUND_DEMO` env, **off by default, zero footprint when off**) that renders two transient overlays so `autocast` screencasts — and live teaching, and a future guided-lesson system — can show otherwise-invisible interactions. **Keystroke badges** (`[TAB]`, `[ENTER]`, `[UP]`, …): **automatic, app-detected** over a fixed set of glyph-less keys (the app already sees every key, so it re-records for free), label via a pure `demo_badge_label(&KeyEvent)`; the badge **auto-expires on a ~1.5 s timer** that extends the runtime's existing time-boxed-`recv` arm condition (`debounce.is_armed() || badge_pending`; expiry `Instant` in the runtime, `App.demo_badge` the render mirror — mirroring the `input` vs `input_indicator` split). **Step captions**: a **stealth, control-code-delimited input buffer** toggled by **`Ctrl+]`** (byte `0x1D` → arrives as `Char('5')+CONTROL`, verified against crossterm 0.29 `parse.rs:110-113`; chosen over `Ctrl+!`, which is **not a single ASCII byte so autocast cannot send it** — the same wall as arrow keys, R4) — typed characters accumulate **invisibly** (prompt untouched, no echo/history), `Backspace` edits, other keys inert, a second `Ctrl+]` **commits** to the caption box (empty commit dismisses); lives in pure-sync `App::update()`, **intercepted before the modal gate** so captions/badges work **over the load picker** (the `#24` projects cast). Both render as **floating black-on-yellow boxes at the output panel's inner bottom-right**, drawn **last over modals**, badge **stacked above** the caption, **no layout reflow**; caption **word-wraps to ≤ 3 lines** (3–5 rows), badge fixed 3 rows; clamp/skip guard for tiny terminals; a new **`App.last_output_area: Rect`** (set in `render_output_panel`) gives the top-level draw the anchor. Caption persists **until the next keystroke**; badge suppressed while capturing. Forks all user-chosen: `--demo` activation (vs hidden command / chord); automatic badges (vs scripted); stealth buffer (vs typed-command / preloaded-file); floating bottom-right boxes (vs HUD / banner / subtitle); `Ctrl+]` trigger; wrap-to-3-line captions; ~1.5 s badge / next-keystroke caption timing. Tested test-first across Tier 1 (label fn, capture state machine incl. over-modal + demo-off gate, nearest-deadline helper), Tier 2 (insta snapshots: badge/caption/both-stacked at 90×26 light+dark, short-terminal clamp), Tier 3 (`--demo` plumbing, badge set/suppressed, caption-without-input wiring), CLI (`--demo` parse + env fallback) — with an **honest limit** noted: the `tokio` timer wiring inside `run_loop` is exercised via the pure pieces + Tier-3 plumbing, not a standalone integration test of the timeout. OOS: scripted/manual badge push; badges for glyph keys; configurable styling/placement; the guided-lesson system itself (own ADR); cross-session/-switch persistence; localised caption content; arrow-only cast interactions (output-pane scroll). Implementation phased **A** (`--demo` plumbing) → **B** (badges) → **C** (captions) +- [ADR-0047 — Demonstration overlay layer (keystroke badges + step captions)](0047-demonstration-overlay-layer.md) — **Accepted 2026-06-10; implemented 2026-06-11, phased A→B→C (closes Gitea #22)** (commits `f879d54`→`2d0f4b2`; no `requirements.md` item — tracked by issue + ADR per convention; all forks user-confirmed + a pre-build `/runda` pass that produced 10 tightening findings and a whole-implementation `/runda` pass that returned PASS, no blockers). An in-app **demonstration mode** (`--demo` flag / `RDBMS_PLAYGROUND_DEMO` env, **off by default, zero footprint when off**) that renders two transient overlays so `autocast` screencasts — and live teaching, and a future guided-lesson system — can show otherwise-invisible interactions. **Keystroke badges** (`[TAB]`, `[ENTER]`, `[UP]`, …): **automatic, app-detected** over a fixed set of glyph-less keys (the app already sees every key, so it re-records for free), label via a pure `demo_badge_label(&KeyEvent)`; the badge **auto-expires on a ~1.5 s timer** that extends the runtime's existing time-boxed-`recv` arm condition (`debounce.is_armed() || badge_pending`; expiry `Instant` in the runtime, `App.demo_badge` the render mirror — mirroring the `input` vs `input_indicator` split). **Step captions**: a **stealth, control-code-delimited input buffer** toggled by **`Ctrl+]`** (byte `0x1D` → arrives as `Char('5')+CONTROL`, verified against crossterm 0.29 `parse.rs:110-113`; chosen over `Ctrl+!`, which is **not a single ASCII byte so autocast cannot send it** — the same wall as arrow keys, R4) — typed characters accumulate **invisibly** (prompt untouched, no echo/history), `Backspace` edits, other keys inert, a second `Ctrl+]` **commits** to the caption box (empty commit dismisses); lives in pure-sync `App::update()`, **intercepted before the modal gate** so captions/badges work **over the load picker** (the `#24` projects cast). Both render as **floating flat black-on-yellow rectangles** (solid fill, **no border glyphs** — a one-cell text margin, deliberately unlike the app's bordered panels; user decision post-build, `2d0f4b2`) **at the output panel's inner bottom-right**, drawn **last over modals**, badge **stacked above** the caption, **no layout reflow**; caption **word-wraps to ≤ 3 lines** (3–5 rows), badge fixed 3 rows; clamp/skip guard for tiny terminals; a new **`App.last_output_area: Rect`** (set in `render_output_panel`) gives the top-level draw the anchor. Caption persists **until the next keystroke**; badge suppressed while capturing. Forks all user-chosen: `--demo` activation (vs hidden command / chord); automatic badges (vs scripted); stealth buffer (vs typed-command / preloaded-file); floating bottom-right boxes (vs HUD / banner / subtitle); `Ctrl+]` trigger; wrap-to-3-line captions; ~1.5 s badge / next-keystroke caption timing. Tested test-first across Tier 1 (label fn, capture state machine incl. over-modal + demo-off gate, nearest-deadline helper), Tier 2 (insta snapshots: badge/caption/both-stacked at 90×26 light+dark, short-terminal clamp), Tier 3 (`--demo` plumbing, badge set/suppressed, caption-without-input wiring), CLI (`--demo` parse + env fallback) — with an **honest limit** noted: the `tokio` timer wiring inside `run_loop` is exercised via the pure pieces + Tier-3 plumbing, not a standalone integration test of the timeout (same posture as the existing `IndicatorDebounce`). One intentional, user-acknowledged behaviour: `Ctrl-C` is inert while capturing (every non-`Ctrl+]` key is, by spec). Final tally **2290 passing / 0 failing / 0 skipped** (1 long-standing ignored doctest), clippy clean. OOS: scripted/manual badge push; badges for glyph keys; configurable styling/placement; the guided-lesson system itself (own ADR); cross-session/-switch persistence; localised caption content; arrow-only cast interactions (output-pane scroll); wiring the overlays into the website `casts.mjs` scripts (website-branch follow-up). Implementation phased **A** (`--demo` plumbing) → **B** (badges) → **C** (captions) + a flat-rectangle restyle diff --git a/docs/handoff/20260611-handoff-64.md b/docs/handoff/20260611-handoff-64.md new file mode 100644 index 0000000..e5dbbe2 --- /dev/null +++ b/docs/handoff/20260611-handoff-64.md @@ -0,0 +1,140 @@ +# Session handoff — 2026-06-11 (64) + +Sixty-fourth handover. Continues from handoff-63 (ADR-0046 sidebar/nav). +This session closed **two unrelated, website-screencast-enabling Gitea +issues**: **#24** (vi-style load-picker navigation) and **#22** +(in-app demonstration overlay layer — its own **ADR-0047**, built end +to end across three phases + a restyle). + +## §1. State at handoff + +**Branch:** `main`. **HEAD `2d0f4b2`** plus an **uncommitted docs +finalization** (ADR-0047 status → implemented, README index, this +handoff — see §6). Push is the user's step. + +**Tests: 2290 passing / 0 failing / 0 skipped / 1 ignored** (the 1 +ignored is the long-standing `friendly` doctest). **Clippy clean** +(nursery, all targets). +27 over the handoff-63 baseline of 2263. + +**This session's commits:** +``` +2d0f4b2 feat(ui): flat filled rectangles for demo overlays (#22, ADR-0047 D4) +241f60c feat(ui): demo-mode step-caption stealth buffer (#22, ADR-0047 D3/D4) +2584e76 feat(ui): demo-mode keystroke badges (#22, ADR-0047 D2/D4/D5) +f879d54 feat(cli): --demo demonstration mode flag + app plumbing (#22, ADR-0047 D1) +e9eb1b1 docs: ADR-0047 — demonstration overlay layer for casts/teaching (#22) +638b4c9 feat(app): vi-style j/k/g/G navigation in the load picker (#24) +``` + +**Issues closed:** **#24** (vi nav) and **#22** (demo overlays) — close +#22 once the docs finalization commit lands. + +## §2. #24 — vi-style load-picker navigation (commit `638b4c9`) + +Purely additive to the ADR-0015 load picker (`handle_load_picker_key`, +`LoadPickerSubMode::List`): **`j`/`k`** mirror Down/Up (bounds- +respecting, no wrap), **`g`/`G`** jump to first/last. Existing keys +(`↑↓`/`Enter`/`Esc`/`b`) unchanged; the footer hint is **left as-is** at +the user's request (the new keys are not advertised). No ADR (additive). +Motivation: `autocast` (the website cast driver) can only send typeable +characters — not arrow keys — so the projects demo needs `j`/`k` to +drive the picker. Tests: `load_picker_jk_navigates_like_arrows`, +`load_picker_g_jumps_to_first_and_last` (test-first). + +## §3. #22 — ADR-0047 demonstration overlay layer (read the ADR) + +An in-app **demonstration mode** (`--demo` flag / `RDBMS_PLAYGROUND_DEMO` +env, **off by default, zero footprint when off**) that renders two +transient overlays so `autocast` screencasts — and live teaching, and a +future guided-lesson system — can show otherwise-invisible interactions. + +- **Phase A (`f879d54`):** `--demo` + env → `App.demo_mode`, threaded + through `run_loop` like `--no-undo`. `--help` line mentions **only the + visible badges**; the `Ctrl+]` caption trigger is kept low-profile + (user decision, D6). +- **Phase B (`2584e76`):** **automatic keystroke badges** + (`[TAB]`/`[ENTER]`/`[UP]`/…) over a fixed set of glyph-less keys — + pure `demo_badge_label(&KeyEvent)`, set in `App::update` **before the + modal gate** (so they fire over the load picker), expired by a **~1.5 s + runtime timer**. The timer extends the event loop's time-boxed-`recv` + via a new pure `nearest_deadline` helper; the rewrite tracks `Instant` + deadlines and was **verified not to regress the ADR-0027 indicator + debounce**. New `App.last_output_area: Rect` (set in + `render_output_panel`) anchors the overlays. +- **Phase C (`241f60c`):** the **stealth `Ctrl+]` caption buffer** — + `Ctrl+]` (byte `0x1D` → `Char('5')+CONTROL`, verified vs crossterm + 0.29) toggles an invisible buffer; typed chars accumulate without + touching input/output, `Backspace` edits, other keys inert, a second + `Ctrl+]` commits (empty commit dismisses). In pure-sync `App::update`, + intercepted **before the modal gate**; an ordinary keystroke clears a + visible caption. +- **Restyle (`2d0f4b2`):** the overlays render as **flat filled yellow + rectangles** (no border glyphs, one-cell text margin) — user decision, + deliberately unlike the bordered panels so they pop. Shared + `fill_overlay_rect` (borderless `Block` fill + inset `Paragraph`). + +**Placement:** both float at the output panel's inner bottom-right, +drawn **last over modals**, badge **stacked directly above** the caption +when both show; caption **wraps to ≤ 3 lines** then ellipsises; clamp/ +skip guard for tiny terminals. + +**Process:** ADR-first (user chose), pre-build `/runda` (10 findings, +folded in) + whole-implementation `/runda` (**PASS, no blockers**). Every +fork user-confirmed via mockups/questions, incl. the two post-draft +follow-ups: `Ctrl+]` trigger (over `Ctrl+!`, which `autocast` cannot +send — not a single ASCII byte) and wrap-to-3-line captions. + +## §4. Two things to know about the implementation + +1. **Ownership split (intentional, mirrors `input`/`input_indicator`):** + `demo_caption`/`demo_caption_capturing`/`demo_caption_buffer` are + driven by `App::update` (input); `demo_badge` is **set** by + `App::update` but its expiry is **timed by the runtime** + (`demo_badge_seq` bumps so a repeated key restarts the timer). +2. **`Ctrl-C` is inert while capturing** — by spec ("every other key is + inert"); exit capture with `Ctrl+]`. User-acknowledged; flagged in + the ADR. The only behaviour worth a second look if it ever annoys. + +## §5. Honest coverage note + +Everything *testable* is tested (label fn, full caption FSM incl. +over-modal + demo-off, `nearest_deadline`, all rendering, CLI parse/env). +The **only** untested wiring is inside `run_loop` (the badge-timer +arm/clear and `app.demo_mode = demo_mode`) — `run_loop` is not +unit-testable (terminal + DB + channels), exactly the posture the +existing `IndicatorDebounce` already takes. A future Tier-4 PTY harness +(ADR-0008 TT4, still unwired) would close it. + +## §6. How to take over + +1. Read handoffs 62 → 63 → 64, `CLAUDE.md`, `docs/requirements.md`, + `docs/adr/README.md`, and **ADR-0047** (fully landed). +2. **Pending:** the docs finalization commit (ADR-0047 status → + implemented; README index; this handoff). Commit as + `docs: session handoff 64 + ADR-0047 implemented (#22/#24)` (the user + confirms commit messages). Then close **#22** on Gitea. +3. **For demo-overlay work:** `App` has `demo_mode`, `demo_badge`, + `demo_badge_seq`, `demo_caption`, `demo_caption_capturing`, + `demo_caption_buffer`, `last_output_area`. Rendering: + `render_demo_overlays` / `render_badge_box` / `render_caption_box` / + `fill_overlay_rect` in `ui.rs`; colours `DEMO_OVERLAY_FG/BG` in + `theme.rs`; key handling `handle_demo_caption_key` + the top-of- + `handle_key` gate; timer in `runtime.rs` (`nearest_deadline`, + `DEMO_BADGE_TTL`). + +## §7. Remaining open landscape (from handoff-63, minus the closed items) + +1. **Wire the overlays into the website casts** — `casts.mjs` on the + `website` branch can now emit `^]`/text/`^]` for captions and rely on + automatic badges. Website-branch follow-up (OOS for #22's app scope). +2. **TT5 CI** — 2290 green, no pipeline yet. +3. **SD1 `seed`** then **H2 `hint`** — the unblockers for **A1** + app-commands; own ADRs. +4. **V2/S3 multi-result tabs** / **V4 journal** — larger output-model + redesign, own ADR. +5. **C3a modify relationship** — small (drop+add covers it). +6. **Tutorial/lesson system** — acknowledged in scope; needs its own + ADR; ADR-0047's overlay primitive is what it will reuse. + +Run a `cargo sweep` at some point — `target/` grew across this +build-heavy session.