93266b99c9
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.
527 lines
27 KiB
Markdown
527 lines
27 KiB
Markdown
# 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.
|