# ADR-0046: Schema sidebar focus/navigation mode and responsive input & hint layout (UI #20 / #21 / #23) ## Status Accepted (2026-06-10); **implemented 2026-06-10**, phased **A → B → C** (see *Decision — phasing*) across commits `9f5f76b` (DA1/DA2) · `e0b9470` (DA3) · `41bae99` (DA4) · `386627a` (DB1) · `94825d0` (DB2/DB4) · `c9da6ff` (DC1/DC2/DC4) · `22bec61` (DC3 + DC2 refinement). 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. Two decisions landed differently from the original draft and are recorded inline: the relationship data lives on **`App`, not `SchemaCache`** (DB2), and the navigation overlay clears **only the sidebar strip + a one-column gutter** (panels stay visible behind), not the whole area (DC2). 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. **Data home — `App`, not `SchemaCache` (revised at implementation, 2026-06-10).** The design first proposed an additive `SchemaCache.relationship_details: Vec` field. Implementation revised this to a **parallel `App.relationships: Vec`** field for two reasons: (1) `SchemaCache` is *walker/completion-facing* — it needs only relationship **names** (unchanged in `SchemaCache.relationships`, still borrowed as `&Vec` 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` 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 (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 a **45-column** overlay (`NAV_EXPANDED_WIDTH`): the renderer `Clear`s the strip the expanded panel occupies **plus a one-column gutter** (`NAV_OVERLAY_GUTTER`) and paints the wide panel on top. The output/input/hint panels underneath keep their exact layout — **unused and unchanging** while browsing, **still visible to the right** of the overlay (just partially occluded on the left) — and are restored fully by the next frame on exit. The gutter keeps them from butting against the expanded panel's border so the overlay edge reads cleanly. 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.) *Implementation note (2026-06-10):* a full-area clear (hiding the base panels entirely during browse) was tried first and rejected — leaving the base visible is truer to "underneath keep their layout," and the one-column gutter resolves the only wrinkle (the panels' left edges being cut by the overlay reading harshly without separation). ### 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`** (DB2) — the full relationship records for the sidebar panel, delivered by `AppEvent::RelationshipsRefreshed` from the runtime's schema refresh. `SchemaCache.relationships: Vec` (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 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: - **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. *(Superseded 2026-06-12: I1b is now in scope and decided by **ADR-0049** — Esc-clear + Ctrl-A/E/W/K/U in the input field, issue #29.)* - **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. ## Amendment 1 — focus accent is a colour, not bold (2026-06-12) Issue #25. DC3's "accent border" on the focused sidebar panel was first implemented as bright `theme.fg` **plus `Modifier::BOLD`** on the box-drawing border. Bold box-drawing glyphs render as broken / gapped line-art in the asciinema player used for the website casts (vertical strokes don't connect to the corner glyphs) and are fragile in some terminals. **`panel_border_style` now marks focus with a non-bold accent colour — `theme.mode_simple` (blue) — and never `Modifier::BOLD` on a border.** The unfocused border stays muted `theme.border`. This makes the ADR's "accent border (lazygit convention)" wording literal — it is now a true accent hue rather than bold bright-fg — and is what renders cleanly in casts. Bold remains fine on *text* spans (titles, key hints); the constraint is specifically that box-drawing borders carry no bold attribute. Note: this is a pure style change. The Tier-2 snapshots are text-only (`render_to_string` captures cell symbols, not styles), so none needed re-accepting; the Tier-1 `panel_border_style` assertion was updated and a render-level test now checks the actual border cells carry the accent colour and no bold.