Files
rdbms-playground/docs/adr/0046-sidebar-navigation-and-responsive-input-hint.md
T
claude@clouddev1 94825d0f36 feat(ui): relationships sidebar panel + schema data (#21, ADR-0046 DB2/DB4)
The left column now stacks a Tables panel over a Relationships panel.
Each relationship renders as three narrow lines — its name, then the
endpoints broken at the arrow (Customers.id -> / indented
Orders.customer_id) — ellipsized past the inner width. The panel is
content-sized within [5 rows ("(none)" when empty), half the column];
the Tables panel keeps the rest (>=3 rows). Phase C adds focus+scroll
for content beyond the cap (clipped for now).

Data path: a new worker Request::ReadAllRelationships +
Database::read_all_relationships returns full RelationshipSchema
records; the runtime posts them via a RelationshipsRefreshed event
alongside the schema-cache refresh, and the App holds them in a new
`relationships` field.

ADR deviation (recorded in ADR-0046 DB2 + index): DB2 specified this
data on SchemaCache; it lives on the App instead — SchemaCache is
walker/completion-facing and needs only relationship names (untouched),
while the full records are UI-only, so App is the cleaner home and it
avoids editing ~23 SchemaCache literals. No behavioural difference.

Tests: panel-height bounds, the three-line render, the empty "(none)"
case, a snapshot, read_all_relationships end-to-end (real DB via the
m:n junction), and the event->field handler.
2026-06-10 18:44:27 +00:00

28 KiB
Raw Blame History

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. 35 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 5057; 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 (DA1DA4): self-contained, no sidebar dependency; fixes #20 and the baseline of #23.
  • Phase B — optional, richer sidebar (DB1DB3): visibility model + relationships panel + schema-cache enrichment.
  • Phase C — navigation mode (DC1DC4): 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.

Data home — App, not SchemaCache (revised at implementation, 2026-06-10). The design first proposed an additive SchemaCache.relationship_details: Vec<RelationshipSchema> field. Implementation revised this to a parallel App.relationships: Vec<RelationshipSchema> field for two reasons: (1) SchemaCache is walker/completion-facing — it needs only relationship names (unchanged in SchemaCache.relationships, still borrowed as &Vec<String> by IdentSource::Relationships); the full records are UI-only, so App is the architecturally correct home, mirroring app.tables (which the items panel already reads alongside the cache). (2) Adding a field to SchemaCache would force edits to ~23 full struct literals across the test suite, whereas App gains one field. The /runda guard it answered — don't break completion by retyping relationships — is fully honoured either way. Delivery: a worker Request::ReadAllRelationships (→ Database::read_all_relationships, returning Vec<RelationshipSchema> via the existing read_all_relationships(conn)); the runtime's refresh_schema_cache posts a new AppEvent::RelationshipsRefreshed alongside SchemaCacheRefreshed, and the App stores it. No behavioural difference from the original design.

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 (4050 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 ("Outline"); 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 ~4050 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.
  • App.relationships: Vec<RelationshipSchema> (DB2) — the full relationship records for the sidebar panel, delivered by AppEvent::RelationshipsRefreshed from the runtime's schema refresh. SchemaCache.relationships: Vec<String> (names, for completion) is unchanged. (See DB2 for why this lives on App, not SchemaCache.)

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 shortcutCtrl-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 renderingoverlay with Clear (chosen, keeps the right panels unchanging) vs re-splitting the layout (would reflow output). → DC2.
  5. Navigation-mode printablesignore (chosen, user) vs drop-to-input-and-type. → DC4.
  6. Hint anti-jumpfix height to terminal geometry (chosen) vs damping/hysteresis vs always-reserve-max. → DA1.
  7. Height thresholdsH < 40 compact / H ≥ 40 comfortable, with 1/2 and 2/2 splits (chosen, user). → DA2.
  8. Visibility persistencesession-only (chosen, user) vs per-project in project.yaml. → DB1.
  9. Persistent show/hide toggledeferred (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/Downline-scroll the focused panel (chosen, user) vs leaving scroll to PageUp/PageDown only. → DC3.
  11. Relationships placementa 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:

  • Relationship data path: Database::read_all_relationships returns full records through the worker thread (integration test, real DB via an m:n junction); AppEvent::RelationshipsRefreshed populates App.relationships; SchemaCache.relationships names are undisturbed (completion still resolves them).
  • 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.