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