diff --git a/docs/adr/0046-sidebar-navigation-and-responsive-input-hint.md b/docs/adr/0046-sidebar-navigation-and-responsive-input-hint.md new file mode 100644 index 0000000..dd90d36 --- /dev/null +++ b/docs/adr/0046-sidebar-navigation-and-responsive-input-hint.md @@ -0,0 +1,526 @@ +# ADR-0046: Schema sidebar focus/navigation mode and responsive input & hint layout (UI #20 / #21 / #23) + +## Status + +Accepted (2026-06-10); **implementation pending**, phased **A → B → C** +(see *Decision — phasing*). Closes Gitea issues **#20** (hint-panel +height jumpiness), **#21** (database-structure / left-column +improvements), and **#23** (long command input). Issue #23's own note +("handle after #21 is decided") is honoured: the input work is split so +the part that depends on the sidebar's width budget lands with it. + +Builds on and honours: **ADR-0003** (the persistent Simple/Advanced +mode model — navigation mode is *not* a third input mode, see DC1), +**ADR-0027** (the input validity indicator's reserved 6 right columns — +horizontal scroll and 2-line display preserve that reserve), +**ADR-0044** (relationship visualization — the relationships panel +renders the same `RelationshipSchema` data the `show relationship` +diagram already consumes), **ADR-0013 / ADR-0043** (the +`RelationshipSchema` model: name, parent/child tables, list-based +compound columns, referential actions), **ADR-0015** (project file +format — sidebar visibility is **session-only**, so the format is +untouched), and **ADR-0002** (no engine name in user-facing strings). +Preserves the **pure-render-from-`App`-state** invariant (CLAUDE.md): +visual changes here are driven either by new `App` *state* fields +(mutated in `update()`) or by pure *render-time* functions of the frame +geometry (see State section); `update()` stays pure-sync. + +**Requirements & issues touched (verified against `requirements.md`).** +Evolves **S1** (the always-present three-region layout — the left items +region becomes width-optional, DB1). **Overrides S2**, which planned +additional element kinds as *nested* items in the tables list; +relationships get their own panel instead (DB2/DB4 — see Genuine forks +§11). **Corrects S4**: its "keyboard-toggleable hint area" was never +implemented (no toggle keybinding exists in the code) and is not wanted +— the hint panel became indispensable once completion moved into it +(ADR-0022) — so the toggle phrase is struck from `requirements.md` and +no toggle is added here. Extends **I1a** (single-line cursor editing → +horizontal scroll, DA3) and honours **S6 / ADR-0027** (the 6-column +validity-indicator reserve, DA3/DA4). The PageUp/PageDown context-rebind +(DC3) does **not** regress **V4**'s output scroll, which stays live in +input mode. Adjacent but separate: Gitea **#22** (an in-app +overlay/annotation layer for casts and guided lessons — its own ADR) +shares the overlay-render and screencast context with DC2's `Clear` +overlay; the two are meant to coexist, not merge. + +## Context + +Three UI issues were raised together because they are coupled through +the terminal's width and height budget; treating them as one decision +avoids three conflicting partial fixes. + +**Current layout (verified in `src/ui.rs`).** `render()` splits +vertically into `Min(8)` main / `Length(1)` project label / `Length(1)` +status. The main area splits horizontally into a **fixed +`Length(28)`** left column (`render_items_panel`, a "Tables" list with +indented index names) and a `Min(20)` right column. The left column's +block has `Borders::ALL`, so its **usable inner width is 26 columns** +(28 − 2 borders). The right column splits vertically into output +(`Min(5)`), input (`Length(3)`), and hint (`Length(hint_content)`). + +**#20 — hint jumpiness.** `hint_content` is recomputed **every frame** +as `clamp(wrapped_lines, 1, MAX_HINT_ROWS=3) + 2`, i.e. 3–5 rows. As the +user types and hint strings appear, grow, and vanish, the hint panel +resizes and **shoves the input and output panels**, producing the +flicker visible in screencasts. The root cause is that height tracks +*content* rather than terminal *geometry*. + +The hint catalog (`src/friendly/strings/en-US.yaml`) was measured: the +two longest strings are `value_literal_slot` (106 chars) and +`create_table_element` (102); four more are 50–57; the rest ≤ 50. The +wrapping consequence is sharp: at a right-column inner width ≥ ~54 +columns the worst string needs **at most 2 lines**; a **3rd line is +only ever required when the right column is narrower than ~54** (a +sub-~83-column terminal *with the sidebar shown*, or a sub-~55-column +terminal). On the project's screencasts (90 columns wide, sidebar +hidden — see DB1) two lines are provably sufficient. + +**#21 — the left column.** A persistent 26-column Tables list is rarely +filled even half-way by a teaching database, yet it permanently costs +horizontal space the output and input panels want — acutely so on the +90-column screencasts. The pedagogical value of an *always-visible* +schema overview is real (CLAUDE.md "pedagogy wins ties"), so the column +is **kept but made optional and more useful**, not deleted. + +**#23 — long input.** The command input is a **single logical `String`** +rendered by a `Paragraph` with no wrap and no horizontal scroll +(`render_input_panel`); text past the panel width **clips silently**. +The cursor is a byte offset on a char boundary; Up/Down drive history. +The fix needs the width the sidebar's removal frees, hence the coupling. + +**Keybinding space (verified).** Taken: Tab/Shift-Tab (completion), +Enter (submit), Up/Down (history), Left/Right/Home/End (cursor), +PageUp/PageDown (output scroll), Backspace/Delete, Esc +(completion-undo / modal cancel), Ctrl-C (quit). Reserved-but-deferred +(I1b readline): Ctrl-A/E/W/K/U. Printable keys all route to the input. +Terminal-hijacked and therefore unusable: Ctrl-S/Q (flow control), +Ctrl-Z (suspend), Ctrl-H (backspace), Ctrl-I/M (Tab/Enter), Ctrl-G +(BEL). This leaves a narrow band of safe combinations for new controls. + +## Decision — phasing + +The work ships in three phases so the screencasts benefit from the +least-controversial part first and the riskiest part (the focus/scroll +model) is isolated: + +- **Phase A — input & hint (DA1–DA4):** self-contained, no sidebar + dependency; fixes #20 and the baseline of #23. +- **Phase B — optional, richer sidebar (DB1–DB3):** visibility model + + relationships panel + schema-cache enrichment. +- **Phase C — navigation mode (DC1–DC4):** the Ctrl-O focus/scroll/ + expand model that makes the sidebar browsable. + +Each phase is independently shippable and independently green. + +## Decision — Phase A: responsive input & hint heights + +### DA1 — Hint height is a function of terminal geometry, fixed between resizes + +The hint panel's height is **decoupled from hint content**. It is +computed from the terminal's width and height **once per resize** and +held constant as the user types. Because the panel no longer resizes on +every keystroke, it never shoves the input/output panels — the #20 jump +is eliminated at the source, not damped. Content that exceeds the fixed +height is ellipsized (the existing `clamp_wrapped` truncation), which is +now a rare, width-driven event rather than a per-keystroke one. + +### DA2 — Responsive height buckets + +Heights are chosen by terminal **height** (rows), with the hint's +optional 3rd line gated on right-column **width** (per the Context +measurement): + +| Terminal height | Input content rows | Hint content rows | +| --- | --- | --- | +| **Compact** (`H < 40` — covers the 25-row screencasts) | 1 (+ horizontal scroll, DA3) | 2 | +| **Comfortable** (`H ≥ 40` — fullscreen terminals) | 2 (soft-wrap, DA4) | 2 (→ 3 only if right-column inner < ~54) | + +A safety degradation protects tiny terminals: the output panel's +`Min(5)` is honoured first; if rows are insufficient, the hint shrinks +to 1, then the input to 1. The `40`-row threshold is a tunable constant. + +### DA3 — Input horizontal scroll (single logical line) + +The input keeps its **single-`String`** model (no embedded newlines — +this is explicitly *not* multi-line input, see Out of scope). A new +`App` field `input_scroll_offset: usize` tracks the first visible +column; the renderer shows a window of the line and keeps the cursor in +view, mirroring the candidate-line horizontal-scroll markers already in +`render_candidate_line`. The ADR-0027 6-column indicator reserve is +preserved (the scroll window is the text area = `inner.width − 6`, not +the full inner width). Because `update()` does not know the panel width, +the renderer feeds it back via a `note_input_viewport(text_width)` call +(the analogue of the existing `note_output_viewport`), against which the +offset is clamped to keep the cursor visible. `input_scroll_offset` +**resets to 0** whenever the buffer is replaced wholesale — on `submit`, +on history navigation (Up/Down), and on any clear. This is the baseline +#23 fix and is sufficient on its own for the compact (1-row) layout. + +### DA4 — Two-line input display when tall (`H ≥ 40`) + +On comfortable terminals the input renders across **2 visual rows** by +soft-wrapping the single logical line, with the cursor mapped to a +(row, col) within the two rows. Content longer than two rows scrolls +the two-row window horizontally (DA3) so the cursor stays visible. The +**ADR-0027 `[ERR]`/`[WRN]` indicator stays anchored to the right edge +of the *first* row** (its 6-column reserve applies to row 1; the soft- +wrap on row 1 stops 6 columns short, row 2 uses the full text width) — +S6 is preserved. + +This is display-only over the same single-`String` model — distinct +from the deferred true multi-line-input feature (I1, which adds +*multiple logical lines* with Enter-inserts-newline). **Forward-compat +note:** I1, when built, should reuse DA4's row-rendering and cursor +(row, col) mapping rather than introduce a parallel one — DA4 is the +substrate, not a competitor. + +## Decision — Phase B: optional, richer sidebar + +### DB1 — Width-derived visibility plus transient peek (session-only) + +Sidebar visibility is **derived, not stored**: the sidebar is visible +iff the terminal **width > 90** *or* navigation mode is currently +focused on a sidebar panel (the Ctrl-O peek, DC1). It is recomputed +every frame from terminal width and `NavFocus`; nothing persists to +`project.yaml` (ADR-0015 untouched), so it is session-only by +construction — and there is no stored visibility field to keep in sync. + +At ≤ 90 columns the sidebar is hidden by default — so the 90-column +screencasts never show it and the output panel gets the full width it +needs there — but `Ctrl-O` temporarily reveals it for the duration of a +browse and re-hides it on exit (DC1). + +**No persistent show/hide toggle (resolved 2026-06-10, user).** Issue +#21's original wording asked for "a keystroke to show and hide it"; the +Ctrl-O peek covers that need, so no separate toggle and no +force-shown/force-hidden override is added. Visibility stays a pure +function of `(terminal width, NavFocus)` — the simplest model that +satisfies the requirement. Should pinning ever prove necessary, a +persistent override is an additive follow-up (see Out of scope). + +### DB2 — Add a relationships panel; enrich the schema cache + +The left column gains a **second panel** below Tables: a list of the +project's relationships. This is a deliberate **override of S2**, whose +note proposed additional element kinds (relations, views) as *nested* +items inside the existing tables list. Relationships are *cross-table*, +not per-table, so nesting them under a single table reads wrong; a +sibling panel is the honest shape (user-confirmed 2026-06-10). S2's +"without restructuring" intent is still met — the items column simply +holds two stacked panels (DB4) instead of one. + +The panel needs the full `RelationshipSchema` (name, parent/child +tables, list-based columns, on-delete/on-update actions) that the `show +relationship` path already fetches. **`SchemaCache` is *extended*, not +retyped:** its existing `relationships: Vec` is left as-is +(`completion.rs` borrows it as `&Vec` via +`IdentSource::Relationships` for relationship-name completion, and +several test fixtures construct it) and a **new field +`relationship_details: Vec`** is added alongside, +populated by the same cache refresh that runs on schema change (the +refresh is taught to query relationship detail, which today it does not +— it only lists names). Retyping the existing field would break the +completion borrow and the fixtures; adding a field is the +zero-ripple change. + +The panel has **two display states** keyed off focus (DC2): + +- **Unfocused (26-col)** — an ambient glance. Per relationship: the + name (ellipsized past the inner width), and the endpoints broken at + the arrow to fit a narrow column: + + ``` + Customers_Orders + Customers.id -> + Orders.customer_id + ``` + +- **Focused + expanded (40–50 col, DC2)** — a browse view. At the wider + width the endpoints fit on one line + (`Customers.id -> Orders.customer_id`); the arrow-break is used only + when even the expanded width cannot hold a (possibly compound) + endpoint pair. The wider width minimises horizontal truncation so the + panel needs **mainly vertical scrolling** (DC3). + +### DB3 — Sidebar width unchanged when unfocused + +The unfocused sidebar keeps `Length(28)` / 26 inner columns. Widening +happens only on focus (DC2), as an overlay, so the unfocused layout and +the right-column reflow are unchanged from today. + +### DB4 — Vertical split of the two left-column panels + +The items column stacks **Tables (top)** and **Relationships (bottom)**. +The Relationships panel's height is content-driven within bounds, so it +stays small when there is little to show and never dominates the column +(user-chosen 2026-06-10): + +- **No relationships:** fixed at **5 rows** (3 content + 2 border), + rendering a single `None` line. This is the floor. +- **With relationships:** grows with content (`content_rows + 2`, where + the unfocused format is ~3 rows per relationship) up to a **cap of + 50 % of the column height**; beyond the cap the panel **scrolls** + (DC3). Formally `rel_h = clamp(content_rows + 2, 5, ⌊col_h / 2⌋)`. +- **Tables** takes the remainder (`col_h − rel_h`) and scrolls if it + overflows (it, too, is a focusable, scrollable panel — DC3). +- **Degradation:** on a column too short to honour the 5-row floor plus + a usable Tables panel (`col_h < ~10`), the floor yields first so + Tables keeps at least its border + one row; both panels stay + renderable. The `50 %` cap and `5`-row floor are tunable constants. + +Heights are a pure render-time function of the column height and the +cached relationship count, so they are unit-testable without a terminal +(see Testing). + +## Decision — Phase C: navigation mode + +### DC1 — `Ctrl-O` navigation mode: a focus cycle, not an input mode + +`Ctrl-O` enters a **navigation mode** that is orthogonal to the +Simple/Advanced input mode (ADR-0003) — it changes *where keystrokes +go*, not *how commands parse*. It drives a focus cycle: + +1. **Press 1 →** focus the **Tables** panel (revealing the sidebar if + it is currently hidden — a temporary peek). +2. **Press 2 →** focus the **Relationships** panel. +3. **Press 3 →** leave navigation mode: restore the sidebar width, + re-hide it if the peek revealed it, and return focus to the command + input. + +`Esc` exits navigation mode directly from any focused panel (a +short-cut for step 3); `Esc` is otherwise only completion-undo, which +does not apply while browsing. + +**Why `Ctrl-O` and not `Ctrl-B`.** `Ctrl-B` is the *default tmux prefix* +and `Ctrl-A` is *screen's* — a multiplexer eats them before the app +sees them, so either would make navigation mode unreachable for the many +students who run inside tmux/screen. `Ctrl-O` is not a multiplexer +prefix; in the raw mode the TUI sets, its legacy line-discipline meaning +(discard-output) is disabled, so it reaches the app. It is free in the +app today (the main key handler's catch-all, `app.rs:1001`). The +mnemonic is weak ("**O**utline"); reachability won over mnemonic. + +**Routing.** Navigation mode is handled inside the **main** key handler, +which runs only when no modal is open (`app.rs:919` gates on +`self.modal.is_some()`). So `Ctrl-O` and the nav keys are **inert while +a modal dialog is active** — modals keep full keyboard ownership. Within +the main handler, a `NavFocus != Input` branch precedes the normal +input-editing arms and routes keys per DC3/DC4. + +### DC2 — Expand-on-focus as an overlay + +A focused sidebar panel widens to **~40–50 columns**, rendered as an +**overlay**: the renderer draws a `Clear` over the affected right-column +region and paints the wide panel on top. The output/input/hint panels +underneath keep their exact layout — **unused and unchanging** while +browsing — and are restored by the next frame on exit. This is cheap +because the renderer is a pure function of `App` state: focus state +selects the width and the overlay path. (The input underneath is +inactive in navigation mode, so occluding it is harmless.) + +### DC3 — Scroll the focused panel; focus highlight + +While a sidebar panel is focused it scrolls, reusing the output panel's +proven mechanism (a `usize` offset clamped against a renderer-reported +viewport via a `note_*_viewport` call): + +- **Up / Down — line-by-line** scroll (the lazygit `j`/`k` feel; + user-chosen 2026-06-10). +- **PageUp / PageDown — page** scroll. + +This is a context-sensitive rebind: Up/Down drive *history* and +PageUp/PageDown scroll the *output* in input mode, whereas in navigation +mode they scroll the *focused sidebar panel*. The two contexts never +apply simultaneously (`NavFocus` selects which). The focused panel shows +an **accent border** so it is obvious where keys are going (lazygit +convention). + +### DC4 — Other keys are inert in navigation mode + +The command input is visibly occluded by the overlay while browsing, so +keys that have no navigation meaning are **inert** rather than acting on +the hidden input. Specifically, **only** `Ctrl-O` (advance focus), +Up/Down + PageUp/PageDown (scroll, DC3), and `Esc` (exit) are live; +printable characters, Enter, Tab, Backspace/Delete, Left/Right, and +Home/End all do nothing until navigation mode is exited. The occlusion +signals "not typing," so swallowing these is clearer than letting them +silently edit an invisible buffer. + +## State, keybindings, and cross-cutting wiring + +**Stored `App` state** (mutated in `update()`, read by the renderer): + +- `input_scroll_offset: usize` (DA3) — reset on submit / history-nav / + clear. +- `NavFocus { Input, SidebarTables, SidebarRelationships }` (DC1) — the + navigation-mode focus cursor; `Input` ≙ not in navigation mode. +- Per-panel scroll offsets for the Tables and Relationships panels, each + clamped against a renderer-reported viewport (DC3), mirroring + `output_scroll` / `note_output_viewport`. +- `SchemaCache` gains **`relationship_details: Vec`** + (DB2) — *additive*; the existing `relationships: Vec` (names, + used by `completion.rs` `IdentSource::Relationships`) is unchanged. The + cache refresh is extended to populate the new field. + +**Render-time derived** (pure functions of `frame.area()` + cached +counts — *not* stored fields; this keeps the pure-render invariant and +makes the geometry logic unit-testable without a terminal): + +- Sidebar visibility — `(width > 90) || NavFocus is a sidebar panel` + (DB1). +- Input/hint row counts — a pure helper `panel_heights(area) -> + (input_rows, hint_rows)` (DA1/DA2), the same helper the renderer and + the Tier-1 tests call. +- Left-column split `rel_h = clamp(content_rows + 2, 5, ⌊col_h/2⌋)` + (DB4). +- Input width fed back to `update()` via `note_input_viewport` + (DA3), since `update()` cannot read `frame.area()`. + +Keybindings introduced/affected: + +| Key | Input mode | Navigation mode | +| --- | --- | --- | +| `Ctrl-O` | enter nav mode, focus Tables (peek-reveal) | advance focus (Tables → Relationships → exit) | +| `Up` / `Down` | history (unchanged) | line-scroll focused panel | +| `PageUp` / `PageDown` | scroll output (unchanged) | page-scroll focused panel | +| `Esc` | completion-undo (unchanged) | exit nav mode directly | +| printable / Enter / Tab / Backspace / Left / Right / Home / End | edit/submit input (unchanged) | inert | + +All nav keys are inert while a modal is open (the main handler is gated +on `!modal.is_some()`, `app.rs:919`). + +Renderer changes (`src/ui.rs`): geometry-driven hint/input height +(DA1/DA2), input window + cursor windowing (DA3) and 2-row soft-wrap +with row-1 indicator (DA4), the relationships panel + two-panel split +(DB2/DB4), the focus accent border and expand-on-focus `Clear` overlay +(DC2/DC3); `note_input_viewport` feedback added alongside the existing +`note_output_viewport`. + +## Genuine forks (escalated, resolved 2026-06-10) + +1. **Left column fate** — remove entirely vs narrow vs **keep + make + optional and richer** (chosen, user). → DB1/DB2. +2. **Focus/scroll model** — a navigation mode (chosen, user) vs + modeless modifier-key scroll vs deferring scroll. → DC1. +3. **Navigation shortcut** — **`Ctrl-O`** (chosen, user); `Ctrl-B` + *rejected on review* (it is the default tmux prefix → unreachable + inside tmux); Ctrl-T also viable; terminal-hijacked combos excluded. + → DC1. +4. **Expand-on-focus rendering** — **overlay with `Clear`** (chosen, + keeps the right panels unchanging) vs re-splitting the layout (would + reflow output). → DC2. +5. **Navigation-mode printables** — **ignore** (chosen, user) vs + drop-to-input-and-type. → DC4. +6. **Hint anti-jump** — **fix height to terminal geometry** (chosen) + vs damping/hysteresis vs always-reserve-max. → DA1. +7. **Height thresholds** — `H < 40` compact / `H ≥ 40` comfortable, with + 1/2 and 2/2 splits (chosen, user). → DA2. +8. **Visibility persistence** — **session-only** (chosen, user) vs + per-project in `project.yaml`. → DB1. +9. **Persistent show/hide toggle** — **deferred** (chosen, user): the + Ctrl-O peek covers #21's "keystroke to show and hide", so visibility + stays width-derived with no override. → DB1. +10. **Nav-mode Up/Down** — **line-scroll the focused panel** (chosen, + user) vs leaving scroll to PageUp/PageDown only. → DC3. +11. **Relationships placement** — **a separate sibling panel** (chosen, + user — *overrides S2*) vs nesting relations inside the tables list + per S2's documented extension model. → DB2/DB4. +12. **Hint-area toggle (S4)** — **no toggle** (chosen, user): the hint + panel is indispensable since completion moved into it; S4's stale + "keyboard-toggleable" claim (never implemented) is struck from + `requirements.md`. → Status (Requirements & issues touched). + +## Testing + +Tier-1 (`app.rs` pure `update()` unit tests), **Tier-2 (`insta` +snapshots, `src/snapshots/`) for the visual surfaces** — this change is +heavily render-side, so the geometry/format/overlay assertions belong in +snapshots, not only behavioural tests — and Tier-3 integration. +Test-first per CLAUDE.md. The geometry helpers (`panel_heights`, the +DB4 split, visibility) are **pure functions** exercised directly in +Tier-1 without a terminal. + +Phase A: +- **Hint anti-jump:** `panel_heights(area)` is invariant under changing + hint content at a fixed terminal size (assert it does not change as + `app.hint` varies); it *does* change across the `H < 40` / `H ≥ 40` + boundary and the width-< 54 boundary. +- **Height buckets:** compact → input 1 row / hint 2; comfortable → + input 2 / hint 2 (3 only when right-column inner < ~54); tiny-terminal + degradation honours output `Min(5)`. +- **Input horizontal scroll:** a line longer than the panel keeps the + cursor visible while moving Left/Right/Home/End; ADR-0027's 6-column + reserve is intact; no characters are lost (buffer = full string); + `input_scroll_offset` resets on submit / history-nav / clear. +- **Two-line input:** at `H ≥ 40` a line wrapping to two rows renders + both rows with correct cursor (row, col), the `[ERR]`/`[WRN]` + indicator on row 1's right edge (Tier-2 snapshot); a longer line + scrolls. + +Phase B: +- **Schema-cache enrichment:** after each schema mutation the cache + carries full `relationship_details` (name, parent/child, columns, + actions) *and* the existing `relationships` names; `completion.rs` + `IdentSource::Relationships` still resolves names (the additive field + did not disturb it). +- **Relationships panel render (Tier-2):** empty → a single `None` line + at the 5-row floor; the unfocused narrow format (name + arrow-break, + ellipsis past inner width); a compound endpoint pair arrow-breaks + correctly. +- **Two-panel split (DB4):** `rel_h = clamp(content_rows + 2, 5, + ⌊col_h/2⌋)` — 5 when empty; grows with content; capped at 50 %; + Tables takes the remainder; degrades sanely at `col_h < 10`. +- **Width-derived visibility:** width ≤ 90 hides, > 90 shows, recomputed + on resize (the peek interaction is covered under Phase C). + +Phase C: +- **Focus cycle:** `Ctrl-O` cycles Input → Tables → Relationships → + Input; `Esc` exits directly; a peek-revealed sidebar re-hides on exit; + a width-shown (> 90) sidebar stays shown on exit; `Ctrl-O` is inert + while a modal is open. +- **Expand overlay (Tier-2):** focusing widens to the expanded width; + the underlying output/input/hint state is unchanged across enter/exit + (no reflow); the focus accent border marks the focused panel. +- **Scroll rebind:** in nav mode Up/Down line-scroll and PageUp/PageDown + page-scroll the focused panel (clamped to its viewport); in input mode + Up/Down still drive history and PageUp/PageDown still scroll output + (no V4 regression); inert keys (printable/Enter/Tab/Backspace) do + nothing in nav mode. + +All tiers green, zero skips; clippy clean (nursery). + +## Out of scope + +- **True multi-line input (I1)** — Enter-inserts-newline / Ctrl-Enter- + submits over a multi-logical-line buffer. DA3/DA4 keep a single + logical line; this remains a separate, deferred feature. +- **Readline shortcuts (I1b)** — Ctrl-A/E/W/K/U stay reserved-deferred; + not touched here. +- **Cross-session sidebar persistence** — visibility is session-only + (DB1); persisting it would amend ADR-0015. +- **The output panel as a third navigation focus target** — navigation + mode cycles the two sidebar panels only; output keeps its input-mode + PageUp/PageDown scroll. +- **Relationship search / filtering within the panel** — the panel is a + scrollable list; no query box. +- **Relationship rename / edit from the panel** — it is read-only; + mutation stays with the DSL/SQL commands. +- **A persistent show/hide toggle / force-shown override** (DB1, + resolved deferred) — visibility is width-derived + Ctrl-O peek; a + pin/force override is an additive follow-up if ever needed. +- **A hint-area toggle (old S4 wording)** — not implemented today and + not wanted (the hint panel is indispensable since completion moved in; + fork §12). The stale "keyboard-toggleable" phrase is removed from S4. +- **In-app overlay / keystroke-annotation layer (Gitea #22)** — a + separate feature with its own ADR; DC2's `Clear` overlay is built to + coexist with it, not to provide it. + +## Accepted consequences + +- **Width-threshold discontinuity.** Because `Auto` visibility flips at + width 90 and the sidebar costs 28 columns, widening a terminal across + the boundary (89 → 91) makes the *output* narrower (≈ 89 → 63 inner) + as the sidebar appears. This is inherent to any width-gated auto-hide + and is accepted: 90 is the screencast width, real terminals sit well + to one side of it, and `Ctrl-O` peek covers the in-between case. The + `90` threshold is a tunable constant. diff --git a/docs/adr/README.md b/docs/adr/README.md index 99b3a78..7d92890 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -51,3 +51,4 @@ This directory contains the project's ADRs, recorded per - [ADR-0043 — Compound-primary-key foreign-key references (T3)](0043-compound-pk-foreign-key-references.md) — **Accepted + implemented 2026-06-09** (all four forks confirmed at the recommended option: full-PK matching, house-style uniform lists, parenthesized DSL syntax, bare-SQL-FK auto-expansion). Closes `requirements.md` **T3** `[x]` — the relationship model went list-based across six layers (single-column preserved, no migration), DSL `from P.(a,b) to C.(x,y)` + SQL `FOREIGN KEY (a,b) REFERENCES P(x,y)` parse/execute/enforce, 12 tests in `tests/it/compound_fk.rs`. Closes the open leg of `requirements.md` **T3**: a foreign key that *references* a parent's compound primary key. A 2026-06-09 audit found single-column FK woven through ~15–20 sites (metadata table, `RelationshipSchema`, `project.yaml` `RawEndpoint`, both grammar surfaces, executor FK-DDL emission, per-column type-compat, display) — earns an ADR, not an inline build. **Decision:** reference the parent's **full** compound PK, matched **positionally** to an equal-length child column list, per-pair `fk_target_type` compat (ADR-0011, element-wise); DSL `from

.(a, b) to .(x, y)` (single form unchanged), SQL `FOREIGN KEY (x, y) REFERENCES P(a, b)` (extend the existing one-cap lists; bare table-level FK auto-expands to the parent PK when arities match). **Storage — no migration (back-compat not required, user-confirmed 2026-06-09; no installed base):** the relationship endpoint joins the list convention `project.yaml` *already* uses — `columns: [a, b]` like `primary_key: [id]` and index `columns: [...]` (the endpoint was the lone scalar `column:` holdout); the metadata `TEXT` columns are unchanged and store the list **comma-joined** (`a,b`; the bare name for single — safe because identifiers are `[A-Za-z0-9_]+`). No F3 migrator, no version bump; accepted trade-off is that a pre-change `project.yaml` with relationships won't load (clean cutover). In-memory model goes list-based (`Vec`) through all six layers; the enforced FK is the rebuilt child-table DDL (`FOREIGN KEY (a,b) REFERENCES P(x,y)`), one relationship = one undo step (ADR-0013). Genuine forks escalated: matching policy (full-PK vs subset), storage (house-style uniform lists vs normalized table), DSL syntax (parenthesized vs repeated-dotted), bare-SQL-FK auto-expansion. OOS: subset/non-PK (UNIQUE-targeted) FK references; any single-column behaviour change - [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 2026-06-10; implementation pending, phased A→B→C** (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"). 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). `SchemaCache` is **extended additively** with `relationship_details: Vec` (the existing names-only `relationships: Vec` is kept for completion); 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) diff --git a/docs/requirements.md b/docs/requirements.md index 625ce85..68fa1fe 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -73,24 +73,38 @@ since ADR-0027.) panel (right), input field (bottom). *(Verified 2026-06-07: `ui.rs:26-58` lays out a horizontal split — items panel left, right column subdivided into output - panel / input field / hint panel; rendered every frame.)* + panel / input field / hint panel; rendered every frame. + **ADR-0046 evolves this:** the left items region becomes + width-optional — hidden by default at ≤ 90 columns, peek-revealed + via `Ctrl-O` navigation mode — so the three-region layout is the + wide-terminal default, not an invariant.)* - [x] **S2** Items list shows tables and per-table indexes; designed to extend to additional element kinds (relations, views, etc.) without restructuring. *(ADR-0025: the items panel renders a nested list — each table with its index names indented beneath it. The nested - model is the extension point for future element kinds.)* + model is the extension point for future element kinds. + **ADR-0046 overrides the nesting approach for relationships:** + because relationships are cross-table rather than per-table, they + get their own sibling panel stacked below the tables list, not + nested items within it — user-confirmed 2026-06-10.)* - [/] **S3** Output panel renders a visualization of the currently selected item and supports multiple tabs. *(Partial, verified 2026-06-07: single-element structure visualisation renders (`output_render.rs:82-180`); **multiple tabs are not implemented** — the output is one line buffer, no tab abstraction. Same multi-tab gap as V2.)* -- [x] **S4** Hint area below the input field; keyboard-toggleable - for inspecting hints about the current input or last error. +- [x] **S4** Hint area below the input field, showing hints about + the current input or last error. *(Verified 2026-06-07: `ui.rs:1088-1110` `render_hint_panel` / `resolve_hint_lines` — a dynamic 1–`MAX_HINT_ROWS` panel below - the input showing ambient hints, candidates, or the last error.)* + the input showing ambient hints, candidates, or the last error. + **Correction (2026-06-10, ADR-0046):** the original wording said + the area was "keyboard-toggleable"; that was never implemented and + is deliberately dropped — the panel became indispensable once + completion moved into it (ADR-0022), so it is always on. ADR-0046 + replaces its content-driven height with a geometry-driven one to + stop the resize jump (#20); no toggle is added.)* - [x] **S5** Mode label and distinct border style on the input field communicate the current input mode at all times. *(Verified 2026-06-07: `ui.rs:896-934` `render_input_panel` —