docs: ADR-0046 UI sidebar nav-mode + responsive input/hint (#20/#21/#23)
Accepted; implementation pending, phased A→B→C. Treats the three coupled UI issues as one decision (shared width/height budget): - #20 hint jumpiness: hint height becomes a function of terminal geometry, fixed between resizes, so it no longer shoves the input/output panels. - #21 left column: kept but width-optional (hidden by default ≤90), with a new relationships sibling panel and a Ctrl-O navigation/focus mode (peek-reveal, expand-on-focus overlay, scroll). - #23 long input: single-logical-line horizontal scroll plus a 2-row display when tall, preserving the ADR-0027 indicator reserve. A pre-build /runda DA pass drove key corrections: Ctrl-B→Ctrl-O (Ctrl-B is the tmux prefix), an additive SchemaCache.relationship_details field (retyping would break completion), full nav-mode key disposition + modal gate, and Tier-2 snapshot coverage. Reconciles requirements S1 (evolved), S2 (overridden — separate relationships panel), and S4 (corrected — the stale "keyboard-toggleable" hint claim is struck; no toggle added). Updates docs/adr/README.md index and docs/requirements.md S1/S2/S4.
This commit is contained in:
@@ -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<String>` is left as-is
|
||||
(`completion.rs` borrows it as `&Vec<String>` via
|
||||
`IdentSource::Relationships` for relationship-name completion, and
|
||||
several test fixtures construct it) and a **new field
|
||||
`relationship_details: Vec<RelationshipSchema>`** 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<RelationshipSchema>`**
|
||||
(DB2) — *additive*; the existing `relationships: Vec<String>` (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.
|
||||
@@ -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 <P>.(a, b) to <C>.(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<String>`) 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 <name>` (one full diagram), `show table <T>` (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 <T1> to <T2> [as <name>]` 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<RelationshipSchema>` (the existing names-only `relationships: Vec<String>` 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)
|
||||
|
||||
Reference in New Issue
Block a user