From 2eac798a5b6c11c16712c0a926ac57bb113caac9 Mon Sep 17 00:00:00 2001 From: Sarav Date: Wed, 5 Aug 2026 10:55:02 +0530 Subject: [PATCH 1/3] fix(tui): per-route vertical reserve, required panel props (#1069 round-2 review) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to the merged #1069, addressing the round-2 review. - Split the vertical reserve per route: HOME_VERTICAL_RESERVE (10) and SESSION_VERTICAL_RESERVE (7) accurately reflect each route's chrome (home has a top spacer + footer; session doesn't), instead of one constant that fit only session. Thresholds re-expressed in usable terms (MEDIUM_MIN_HEIGHT 13, FULL_MIN_HEIGHT 34) so `full` still engages at a ~44-row terminal on home and the classic 80x24 clears the medium floor with margin. - Make WelcomePanel's availableWidth/availableHeight props REQUIRED (drop the raw-terminal fallback + the component's own useTerminalDimensions), so a call site that forgets to scale is a compile error, not a silent regression. - Height now gets the same available-space treatment as width (terminal minus the route's prompt/footer chrome), so a short window shrinks the panel instead of crowding the prompt. - Centralize PANEL_HORIZONTAL_PADDING (4) — was a bare `-4` in the home call site and test helper. Tests: add the sidebar-toggle transition (session 150x50 full -> medium when the sidebar opens — the exact #1067 case), the classic 80x24 on both routes, and keep the isolated-gate / boundary / degenerate coverage. tsgo clean; oxlint 0/0. Reviewed by a 2-model consensus panel (both SOUND); the PANEL_HORIZONTAL_PADDING and sidebar-toggle test came out of that review. Co-Authored-By: Claude Opus 4.8 --- .../tui/src/component/welcome-panel-utils.ts | 44 ++++++++++------- packages/tui/src/component/welcome-panel.tsx | 31 ++++++------ packages/tui/src/routes/home.tsx | 10 ++-- packages/tui/src/routes/session/index.tsx | 6 +-- .../component/welcome-panel-utils.test.ts | 47 ++++++++++++++----- 5 files changed, 87 insertions(+), 51 deletions(-) diff --git a/packages/tui/src/component/welcome-panel-utils.ts b/packages/tui/src/component/welcome-panel-utils.ts index 853abc2e6..3e74a32b1 100644 --- a/packages/tui/src/component/welcome-panel-utils.ts +++ b/packages/tui/src/component/welcome-panel-utils.ts @@ -7,36 +7,46 @@ // has on both axes: // - width: the terminal minus the caller's padding and any sibling sidebar. // - height: the terminal minus the fixed chrome that always shares the column -// with the panel (top spacer + prompt + footer), so a big panel is -// chosen only when it won't crowd the prompt off a short terminal. +// with the panel (see the per-route reserves below), so a big panel +// is chosen only when it won't crowd the prompt off a short terminal. // `medium` (no wordmark) is the common case; `full` only when there's real room. // // Naming: these are the MINIMUMS a tier requires, matched with strict `<` -// (`width < FULL_MIN_WIDTH` → not full). MEDIUM_MIN_* is the floor for medium -// (below → compact); FULL_MIN_* is the floor for full (below → medium). All in +// (`width < FULL_MIN_WIDTH` -> not full). MEDIUM_MIN_* is the floor for medium +// (below -> compact); FULL_MIN_* is the floor for full (below -> medium). All in // terms of AVAILABLE (usable) size, not the raw terminal. export type WelcomePanelVariant = "full" | "medium" | "compact" -/** - * Rows the panel must leave for the always-present chrome below/around it (the - * prompt, the footer, and the home top spacer). Callers subtract this from the - * terminal height to get the panel's usable height. An estimate — the prompt can - * grow with multi-line input, but at rest this is the fixed cost. - */ -export const PANEL_VERTICAL_RESERVE = 8 +// Rows the panel must leave for the always-present chrome that shares its column. +// The two routes have different chrome, so they reserve different amounts; the +// shared thresholds below then transition ~3 rows of terminal height apart +// between the routes (session, with less chrome, shows a bigger variant sooner). +// Estimates — the prompt can grow with multi-line input; this is the at-rest cost. +// +// home (routes/home.tsx): top spacer (2) + prompt wrapper paddingTop (1) + prompt +// (~4) + footer (~3, feature-plugins/home/footer.tsx) ≈ 10. +export const HOME_VERTICAL_RESERVE = 10 +// session (routes/session/index.tsx): two column gaps (2) + paddingBottom (1) + +// prompt (~4); no top spacer, no footer in that column ≈ 7. +export const SESSION_VERTICAL_RESERVE = 7 + +// Columns the home slot spends on its own left/right padding (2 + 2); the caller +// subtracts this to get the panel's usable width. (Session's contentWidth applies +// the same 4 as part of its own content-column math.) +export const PANEL_HORIZONTAL_PADDING = 4 -/** Minimum usable size for the medium panel; below either → compact (one line). */ +/** Minimum usable size for the medium panel; below either -> compact (one line). */ export const MEDIUM_MIN_WIDTH = 60 -export const MEDIUM_MIN_HEIGHT = 16 -/** Minimum usable size for the full wordmark panel; below either → medium. */ +export const MEDIUM_MIN_HEIGHT = 13 +/** Minimum usable size for the full wordmark panel; below either -> medium. */ export const FULL_MIN_WIDTH = 110 -export const FULL_MIN_HEIGHT = 36 +export const FULL_MIN_HEIGHT = 34 /** * Choose the WelcomePanel layout from the panel's AVAILABLE size — width already - * minus padding/sidebar, height already minus PANEL_VERTICAL_RESERVE. Not the - * raw terminal (that's the #1067 bug: a sidebar-narrowed column, or a short + * minus padding/sidebar, height already minus the route's vertical reserve. Not + * the raw terminal (that's the #1067 bug: a sidebar-narrowed column, or a short * terminal, would still pick `full`). */ export function welcomePanelVariant(width: number, height: number): WelcomePanelVariant { diff --git a/packages/tui/src/component/welcome-panel.tsx b/packages/tui/src/component/welcome-panel.tsx index b8c16eff9..e42982d70 100644 --- a/packages/tui/src/component/welcome-panel.tsx +++ b/packages/tui/src/component/welcome-panel.tsx @@ -1,6 +1,5 @@ import { Match, Show, Switch, createMemo } from "solid-js" import { TextAttributes } from "@opentui/core" -import { useTerminalDimensions } from "@opentui/solid" import { useTheme } from "../context/theme" import { Logo } from "./logo" import { InstallationVersion } from "@opencode-ai/core/installation/version" @@ -16,27 +15,31 @@ const CONNECT_CTA = "Connect your AI model to start." // blank its top rows. // // Responsive (issue #1067): the full two-column boot box is a ~13-row bordered -// box that ate ~40% of the screen. It now scales down by AVAILABLE size, -// following the repo's breakpoint idiom (createMemo over useTerminalDimensions, -// cf. routes/session/permission.tsx:450, component/upgrade-indicator.tsx:14): +// box that ate ~40% of the screen. It now scales down by AVAILABLE size — the +// caller measures the terminal (useTerminalDimensions) and passes what the panel +// actually gets, following the repo's breakpoint idiom (a createMemo over the +// reactive dimensions, cf. routes/session/permission.tsx:450, +// component/upgrade-indicator.tsx:14): // full — wordmark + full description (large windows only) -// medium — title + one condensed line, no wordmark (the common case) +// medium — title + a condensed description (one line on a wide terminal, two at +// medium's narrow end), no wordmark (the common case) // compact — a short line; the border title already carries the version // // `availableWidth` / `availableHeight` are the space the panel actually gets, not // the whole terminal — the caller subtracts its padding, any sibling sidebar -// (session's contentWidth), and the fixed prompt/footer chrome -// (PANEL_VERTICAL_RESERVE). Using the raw terminal would keep `full` selected in -// a sidebar-narrowed column or a short window and swell the panel back up — the -// bug #1067 is about. Both fall back to the terminal dimension when omitted. -export function WelcomePanel(props: { availableWidth?: number; availableHeight?: number }) { +// (session's contentWidth), and the route's vertical reserve (HOME/SESSION_ +// VERTICAL_RESERVE). Using the raw terminal would keep `full` selected in a +// sidebar-narrowed column or a short window and swell the panel back up — the bug +// #1067 is about. Both props are REQUIRED: a call site that forgot one would +// silently get the pre-fix raw-terminal behavior, so the type system guards it +// (there's no in-repo render test of the call sites). +export function WelcomePanel(props: { availableWidth: number; availableHeight: number }) { const { theme } = useTheme() const ready = useReady() - const dimensions = useTerminalDimensions() - const variant = createMemo(() => - welcomePanelVariant(props.availableWidth ?? dimensions().width, props.availableHeight ?? dimensions().height), - ) + // props are reactive getters, so reading them inside the memo tracks — the + // variant recomputes when the caller's dimensions/sidebar change. + const variant = createMemo(() => welcomePanelVariant(props.availableWidth, props.availableHeight)) const title = InstallationVersion === "local" ? " Altimate Code " : ` Altimate Code v${InstallationVersion} ` diff --git a/packages/tui/src/routes/home.tsx b/packages/tui/src/routes/home.tsx index caa6d10e8..7dfa42437 100644 --- a/packages/tui/src/routes/home.tsx +++ b/packages/tui/src/routes/home.tsx @@ -19,7 +19,7 @@ import { useTheme } from "../context/theme" // one-line "Get started: /connect ... /discover ..." hint below, which duplicated the // same guidance the panel's "Tips for getting started" section now covers. import { WelcomePanel } from "../component/welcome-panel" -import { PANEL_VERTICAL_RESERVE } from "../component/welcome-panel-utils" +import { HOME_VERTICAL_RESERVE, PANEL_HORIZONTAL_PADDING } from "../component/welcome-panel-utils" // altimate_change end let once = false @@ -110,11 +110,11 @@ export function Home() { {/* Size to the panel's real space, not the whole terminal (#1067): - -4 for this column's paddingLeft/Right; -PANEL_VERTICAL_RESERVE for - the top spacer + prompt + footer that share the height. */} + -PANEL_HORIZONTAL_PADDING for this column's paddingLeft/Right; + -HOME_VERTICAL_RESERVE for the top spacer + prompt + footer. */} diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index 0521a6248..c07587e1f 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -26,7 +26,7 @@ import { useTuiPaths, useTuiTerminalEnvironment } from "../../context/runtime" import { Spinner } from "../../component/spinner" // altimate_change — shared boot box at the top of the session scrollback import { WelcomePanel } from "../../component/welcome-panel" -import { PANEL_VERTICAL_RESERVE } from "../../component/welcome-panel-utils" +import { SESSION_VERTICAL_RESERVE } from "../../component/welcome-panel-utils" import { createSyntaxStyleMemo, generateSubtleSyntax, selectedForeground, useTheme } from "../../context/theme" import { BoxRenderable, ScrollBoxRenderable, addDefaultParsers, TextAttributes, RGBA } from "@opentui/core" import { Prompt, type PromptRef } from "../../component/prompt" @@ -1189,10 +1189,10 @@ export function Session() { {/* Size to the panel's real space, not the terminal (#1067): contentWidth already subtracts the sidebar + padding; - -PANEL_VERTICAL_RESERVE leaves room for the prompt + footer. */} + -SESSION_VERTICAL_RESERVE leaves room for the prompt. */} {/* altimate_change end */} diff --git a/packages/tui/test/component/welcome-panel-utils.test.ts b/packages/tui/test/component/welcome-panel-utils.test.ts index 2d1e22b84..0e1750e78 100644 --- a/packages/tui/test/component/welcome-panel-utils.test.ts +++ b/packages/tui/test/component/welcome-panel-utils.test.ts @@ -2,18 +2,25 @@ import { expect, test } from "bun:test" import { FULL_MIN_HEIGHT, FULL_MIN_WIDTH, + HOME_VERTICAL_RESERVE, MEDIUM_MIN_HEIGHT, MEDIUM_MIN_WIDTH, - PANEL_VERTICAL_RESERVE, + PANEL_HORIZONTAL_PADDING, + SESSION_VERTICAL_RESERVE, welcomePanelVariant, } from "../../src/component/welcome-panel-utils" // Comfortably above the full floor on one axis, used to isolate the OTHER axis // so a single gate's removal is provable (each test below fails if its `<` check -// is deleted from the source). +// is deleted from the source, or flipped to `<=`). const TALL = FULL_MIN_HEIGHT + 10 const WIDE = FULL_MIN_WIDTH + 20 +// Map a terminal size to the AVAILABLE size each route feeds the pure function. +const home = (w: number, h: number) => welcomePanelVariant(w - PANEL_HORIZONTAL_PADDING, h - HOME_VERTICAL_RESERVE) +const session = (w: number, h: number, sidebar: boolean) => + welcomePanelVariant(w - (sidebar ? 42 : 0) - PANEL_HORIZONTAL_PADDING, h - SESSION_VERTICAL_RESERVE) + test("full requires BOTH width and height to clear the full floor", () => { expect(welcomePanelVariant(WIDE, TALL)).toBe("full") expect(welcomePanelVariant(FULL_MIN_WIDTH, FULL_MIN_HEIGHT)).toBe("full") // exactly at the floor @@ -42,20 +49,36 @@ test("compact→medium boundary is exact (at the floor is medium)", () => { }) test("everyday terminals get medium, not the oversized wordmark", () => { - // Inputs are AVAILABLE size (terminal minus padding/sidebar on width, minus - // PANEL_VERTICAL_RESERVE on height). A 106x31 terminal → ~(102, 23): - expect(welcomePanelVariant(102, 23)).toBe("medium") - // 80x24 terminal → ~(76, 16) — medium exactly at the height floor: - expect(welcomePanelVariant(76, 16)).toBe("medium") + expect(home(106, 31)).toBe("medium") + expect(home(80, 24)).toBe("medium") // #1067 session case: a 130-col terminal with the 42-col sidebar leaves ~84 // usable cols → medium (was wrongly full when it used the whole terminal width). - expect(welcomePanelVariant(130 - 42 - 4, 50 - PANEL_VERTICAL_RESERVE)).toBe("medium") + expect(session(130, 50, true)).toBe("medium") +}) + +test("the classic 80x24 is medium on both routes, with margin off the compact floor", () => { + // The review flagged 80x24 sitting on the exact medium floor; it now clears it + // on both routes (home reserves more chrome, so it's the tighter one). + expect(home(80, 24)).toBe("medium") + expect(session(80, 24, false)).toBe("medium") +}) + +test("full engages on a large window; ~one row below the floor stays medium", () => { + expect(home(120, 44)).toBe("full") // FULL_MIN_HEIGHT(34) + HOME_VERTICAL_RESERVE(10) + expect(home(120, 43)).toBe("medium") +}) + +test("toggling the session sidebar flips the panel full → medium on a wide window (#1067)", () => { + // The exact regression #1067 reports: on a wide window, opening the 42-col + // sidebar must shrink the panel out of `full` — it no longer has ~110 usable cols. + expect(session(150, 50, false)).toBe("full") + expect(session(150, 50, true)).toBe("medium") }) -test("a short terminal drops to compact once prompt/footer chrome is reserved (#1067 height)", () => { - // 120x22: wide, but only ~14 usable rows after the ~8-row chrome → compact, - // where the raw terminal height (22) would have picked medium. - expect(welcomePanelVariant(120 - 4, 22 - PANEL_VERTICAL_RESERVE)).toBe("compact") +test("a short terminal drops to compact once the route's chrome is reserved (#1067 height)", () => { + // 120x22: wide, but too few usable rows after the home chrome → compact, where + // the raw terminal height (22) would have picked medium. + expect(home(120, 22)).toBe("compact") }) test("degenerate sizes collapse to compact", () => { From 9d4c36cd79b62661be4bd047d717798412d79fa9 Mon Sep 17 00:00:00 2001 From: Sarav Date: Fri, 7 Aug 2026 08:22:28 +0530 Subject: [PATCH 2/3] fix(tui): address welcome-panel round-3 review (reserves, overlay, tests) - correct per-route vertical reserves to the real chrome: HOME 10->15, SESSION 7->9, derived from PROMPT_REST_HEIGHT=6 plus the counted home_bottom slot and footer (the prompt is 5-6 rows at rest, not ~4) - retune height thresholds honestly: MEDIUM_MIN_HEIGHT 13->8 (the real medium panel height, so 80x24 is a genuine medium fit, not a pinned margin), FULL_MIN_HEIGHT 34->30 (a documented product breakpoint, not a fit minimum) - centralize the route arithmetic: export homeAvailable/sessionAvailable and call them from both the routes and the tests, so a wrong reserve/threshold fails a test instead of being mirrored in a test-only copy - centralize PANEL_HORIZONTAL_PADDING and add SIDEBAR_WIDTH (drop the duplicated 42) - keep the panel on session's contentWidth basis (narrows with the sidebar, incl. the dimmed overlay) so the panel stays aligned with the messages; documented - tests: reserve-pinned-to-chrome, overlay narrowing, negative dimensions Co-Authored-By: Claude Opus 4.8 --- .../tui/src/component/welcome-panel-utils.ts | 96 ++++++++++++++----- packages/tui/src/routes/home.tsx | 16 ++-- packages/tui/src/routes/session/index.tsx | 23 +++-- .../component/welcome-panel-utils.test.ts | 65 ++++++++++--- 4 files changed, 142 insertions(+), 58 deletions(-) diff --git a/packages/tui/src/component/welcome-panel-utils.ts b/packages/tui/src/component/welcome-panel-utils.ts index 3e74a32b1..ee2531cc8 100644 --- a/packages/tui/src/component/welcome-panel-utils.ts +++ b/packages/tui/src/component/welcome-panel-utils.ts @@ -5,43 +5,58 @@ // bordered box, so on a typical terminal it eats ~40% of the screen with no way // to shrink it (issue #1067). We scale it down by the space the panel ACTUALLY // has on both axes: -// - width: the terminal minus the caller's padding and any sibling sidebar. +// - width: the terminal minus the caller's padding and any sibling sidebar +// that consumes layout width. // - height: the terminal minus the fixed chrome that always shares the column -// with the panel (see the per-route reserves below), so a big panel -// is chosen only when it won't crowd the prompt off a short terminal. +// with the panel (the per-route reserves below), so a big panel is +// chosen only when it won't crowd the prompt off a short terminal. // `medium` (no wordmark) is the common case; `full` only when there's real room. // -// Naming: these are the MINIMUMS a tier requires, matched with strict `<` -// (`width < FULL_MIN_WIDTH` -> not full). MEDIUM_MIN_* is the floor for medium -// (below -> compact); FULL_MIN_* is the floor for full (below -> medium). All in -// terms of AVAILABLE (usable) size, not the raw terminal. +// Route arithmetic lives here (homeAvailable / sessionAvailable) so the routes +// and the tests share ONE definition — a test that maps a terminal size to a +// variant then exercises the real call-site math, not a copy of it. export type WelcomePanelVariant = "full" | "medium" | "compact" -// Rows the panel must leave for the always-present chrome that shares its column. -// The two routes have different chrome, so they reserve different amounts; the -// shared thresholds below then transition ~3 rows of terminal height apart -// between the routes (session, with less chrome, shows a bigger variant sooner). -// Estimates — the prompt can grow with multi-line input; this is the at-rest cost. +// --- Chrome reserves (rows the panel must leave for always-present siblings) --- // -// home (routes/home.tsx): top spacer (2) + prompt wrapper paddingTop (1) + prompt -// (~4) + footer (~3, feature-plugins/home/footer.tsx) ≈ 10. -export const HOME_VERTICAL_RESERVE = 10 -// session (routes/session/index.tsx): two column gaps (2) + paddingBottom (1) + -// prompt (~4); no top spacer, no footer in that column ≈ 7. -export const SESSION_VERTICAL_RESERVE = 7 - -// Columns the home slot spends on its own left/right padding (2 + 2); the caller -// subtracts this to get the panel's usable width. (Session's contentWidth applies -// the same 4 as part of its own content-column math.) +// Prompt tree at rest (component/prompt/index.tsx): top rule 1 + inner paddingTop +// 1 + textarea 1 + separator 1 + agent/model meta row 1 + idle hint row 1 = 6. +// Measured against the rendered tree, not estimated — the earlier "~4" undercounted +// it (the meta and hint rows are always in flow). +const PROMPT_REST_HEIGHT = 6 + +// home (routes/home.tsx): top spacer 2 (``) + prompt wrapper +// paddingTop 1 + prompt 6 + home_bottom slot 3 (feature-plugins/home/tips.tsx +// paddingTop, rendered unconditionally — flexbox shrinks content, not padding) + +// footer 3 (feature-plugins/home/footer.tsx) = 15. +export const HOME_VERTICAL_RESERVE = 2 + 1 + PROMPT_REST_HEIGHT + 3 + 3 +// session (routes/session/index.tsx): two column gaps 2 + paddingBottom 1 + +// prompt 6 = 9. No top spacer and no footer share this column. +export const SESSION_VERTICAL_RESERVE = 2 + 1 + PROMPT_REST_HEIGHT + +// Columns the home slot spends on its own left/right padding (2 + 2). Session +// subtracts the same via sessionAvailable(); both routes import this constant so +// the value has a single source of truth. export const PANEL_HORIZONTAL_PADDING = 4 +// Width the session sidebar occupies WHEN it consumes layout width (i.e. it is +// rendered in-flow, not as an overlay). Shared with the route + tests so they +// can't drift — the drift that produced #1067. +export const SIDEBAR_WIDTH = 42 -/** Minimum usable size for the medium panel; below either -> compact (one line). */ +// --- Thresholds --- +// MEDIUM_MIN_* is a real FIT requirement: below it the medium panel (~8 rows / +// ~a title + one/two-line description) would not fit, so drop to the one-line +// compact. FULL_MIN_* is a product BREAKPOINT, not a fit minimum: the full panel +// is only ~13 rows, but we require far more available space so the branded +// wordmark appears only on a genuinely large terminal and never dominates a +// small one (the #1067 ask). All in AVAILABLE (usable) terms, not the raw +// terminal, matched with strict `<`. export const MEDIUM_MIN_WIDTH = 60 -export const MEDIUM_MIN_HEIGHT = 13 -/** Minimum usable size for the full wordmark panel; below either -> medium. */ +export const MEDIUM_MIN_HEIGHT = 8 export const FULL_MIN_WIDTH = 110 -export const FULL_MIN_HEIGHT = 34 +// ~45-row home terminal / ~39-row session terminal after the reserves above. +export const FULL_MIN_HEIGHT = 30 /** * Choose the WelcomePanel layout from the panel's AVAILABLE size — width already @@ -54,3 +69,32 @@ export function welcomePanelVariant(width: number, height: number): WelcomePanel if (width < FULL_MIN_WIDTH || height < FULL_MIN_HEIGHT) return "medium" return "full" } + +/** Available panel size on the home route for a given terminal size. */ +export function homeAvailable(terminalWidth: number, terminalHeight: number): { width: number; height: number } { + return { + width: terminalWidth - PANEL_HORIZONTAL_PADDING, + height: terminalHeight - HOME_VERTICAL_RESERVE, + } +} + +/** + * Available panel size on the session route. Subtracts SIDEBAR_WIDTH whenever the + * sidebar is open (`sidebarVisible`) — the panel shares the same content-column + * basis as the messages (session's `contentWidth`), so the two stay aligned. + * + * On narrow terminals the sidebar renders as a dimmed full-area overlay rather + * than in-flow; the content column still narrows uniformly (panel + messages) + * and both restore to full width when it closes, so we deliberately size to the + * narrowed column rather than the transient obscured width. + */ +export function sessionAvailable( + terminalWidth: number, + terminalHeight: number, + sidebarVisible: boolean, +): { width: number; height: number } { + return { + width: terminalWidth - (sidebarVisible ? SIDEBAR_WIDTH : 0) - PANEL_HORIZONTAL_PADDING, + height: terminalHeight - SESSION_VERTICAL_RESERVE, + } +} diff --git a/packages/tui/src/routes/home.tsx b/packages/tui/src/routes/home.tsx index 7dfa42437..d321a37f8 100644 --- a/packages/tui/src/routes/home.tsx +++ b/packages/tui/src/routes/home.tsx @@ -19,7 +19,7 @@ import { useTheme } from "../context/theme" // one-line "Get started: /connect ... /discover ..." hint below, which duplicated the // same guidance the panel's "Tips for getting started" section now covers. import { WelcomePanel } from "../component/welcome-panel" -import { HOME_VERTICAL_RESERVE, PANEL_HORIZONTAL_PADDING } from "../component/welcome-panel-utils" +import { homeAvailable } from "../component/welcome-panel-utils" // altimate_change end let once = false @@ -68,6 +68,9 @@ export function Home() { if (configured === "auto") return Math.max(75, Math.floor(dimensions().width * 0.7)) return configured ?? 75 }) + // Panel's available space (terminal minus this route's padding + vertical + // reserve). Shared arithmetic in welcome-panel-utils so tests exercise it. + const panelAvailable = createMemo(() => homeAvailable(dimensions().width, dimensions().height)) let sent = false onMount(() => { @@ -109,13 +112,10 @@ export function Home() { - {/* Size to the panel's real space, not the whole terminal (#1067): - -PANEL_HORIZONTAL_PADDING for this column's paddingLeft/Right; - -HOME_VERTICAL_RESERVE for the top spacer + prompt + footer. */} - + {/* Size to the panel's real space, not the whole terminal (#1067). + homeAvailable() subtracts this column's padding and the top + spacer + prompt + home_bottom + footer reserve. */} + diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index c07587e1f..772deb126 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -26,7 +26,7 @@ import { useTuiPaths, useTuiTerminalEnvironment } from "../../context/runtime" import { Spinner } from "../../component/spinner" // altimate_change — shared boot box at the top of the session scrollback import { WelcomePanel } from "../../component/welcome-panel" -import { SESSION_VERTICAL_RESERVE } from "../../component/welcome-panel-utils" +import { PANEL_HORIZONTAL_PADDING, SIDEBAR_WIDTH, sessionAvailable } from "../../component/welcome-panel-utils" import { createSyntaxStyleMemo, generateSubtleSyntax, selectedForeground, useTheme } from "../../context/theme" import { BoxRenderable, ScrollBoxRenderable, addDefaultParsers, TextAttributes, RGBA } from "@opentui/core" import { Prompt, type PromptRef } from "../../component/prompt" @@ -272,7 +272,15 @@ export function Session() { return false }) const showTimestamps = createMemo(() => timestamps() === "show") - const contentWidth = createMemo(() => dimensions().width - (sidebarVisible() ? 42 : 0) - 4) + const contentWidth = createMemo( + () => dimensions().width - (sidebarVisible() ? SIDEBAR_WIDTH : 0) - PANEL_HORIZONTAL_PADDING, + ) + // Welcome panel's available space — same content-column basis as contentWidth + // (narrows with the sidebar whenever open, incl. the dimmed overlay on narrow + // terminals) so the panel stays aligned with the messages and both restore + // together when it closes. Height reserves the prompt row. Shared arithmetic in + // welcome-panel-utils so the tests exercise this exact math. + const panelAvailable = createMemo(() => sessionAvailable(dimensions().width, dimensions().height, sidebarVisible())) const providers = createMemo(() => Model.index(sync.data.provider)) const scrollAcceleration = createMemo(() => getScrollAcceleration(tuiConfig)) @@ -1187,13 +1195,10 @@ export function Session() { /discover) starts a session. Outside the scrollbox: the bordered panel does not paint reliably inside the scroll viewport. */} - {/* Size to the panel's real space, not the terminal (#1067): - contentWidth already subtracts the sidebar + padding; - -SESSION_VERTICAL_RESERVE leaves room for the prompt. */} - + {/* Size to the panel's real space, not the terminal (#1067). + panelAvailable() subtracts the in-flow sidebar + padding and + reserves the prompt row (see the memo above). */} + {/* altimate_change end */} welcomePanelVariant(w - PANEL_HORIZONTAL_PADDING, h - HOME_VERTICAL_RESERVE) -const session = (w: number, h: number, sidebar: boolean) => - welcomePanelVariant(w - (sidebar ? 42 : 0) - PANEL_HORIZONTAL_PADDING, h - SESSION_VERTICAL_RESERVE) +// Map a terminal size to a variant THROUGH the exact functions the routes call +// (homeAvailable / sessionAvailable), so these tests exercise the real call-site +// arithmetic rather than a private copy of it. `sidebarVisible` is the route's +// sidebarVisible() — the content column narrows whenever the sidebar is open. +const home = (w: number, h: number) => { + const a = homeAvailable(w, h) + return welcomePanelVariant(a.width, a.height) +} +const session = (w: number, h: number, sidebarVisible: boolean) => { + const a = sessionAvailable(w, h, sidebarVisible) + return welcomePanelVariant(a.width, a.height) +} + +test("reserves stay pinned to the counted chrome", () => { + // If someone changes the reserve to a wrong literal, this fails. The values are + // derived sums of the documented per-route chrome (see welcome-panel-utils.ts). + expect(HOME_VERTICAL_RESERVE).toBe(15) + expect(SESSION_VERTICAL_RESERVE).toBe(9) +}) test("full requires BOTH width and height to clear the full floor", () => { expect(welcomePanelVariant(WIDE, TALL)).toBe("full") @@ -51,37 +67,56 @@ test("compact→medium boundary is exact (at the floor is medium)", () => { test("everyday terminals get medium, not the oversized wordmark", () => { expect(home(106, 31)).toBe("medium") expect(home(80, 24)).toBe("medium") - // #1067 session case: a 130-col terminal with the 42-col sidebar leaves ~84 - // usable cols → medium (was wrongly full when it used the whole terminal width). + // #1067 session case: a 130-col terminal with the in-flow 42-col sidebar leaves + // ~84 usable cols → medium (was wrongly full when it used the whole terminal). expect(session(130, 50, true)).toBe("medium") }) -test("the classic 80x24 is medium on both routes, with margin off the compact floor", () => { - // The review flagged 80x24 sitting on the exact medium floor; it now clears it - // on both routes (home reserves more chrome, so it's the tighter one). +test("the classic 80x24 is medium on both routes (a real fit, not a fake margin)", () => { + // 80x24 → home available (76 × 9), session available (76 × 15). Both clear the + // medium floor (60 × 8) — the medium panel is ~8 rows, so this actually fits. expect(home(80, 24)).toBe("medium") expect(session(80, 24, false)).toBe("medium") }) test("full engages on a large window; ~one row below the floor stays medium", () => { - expect(home(120, 44)).toBe("full") // FULL_MIN_HEIGHT(34) + HOME_VERTICAL_RESERVE(10) - expect(home(120, 43)).toBe("medium") + // full needs available height ≥ FULL_MIN_HEIGHT(30); home reserves 15, so the + // terminal must be ≥ 45 rows. + expect(home(120, 45)).toBe("full") + expect(home(120, 44)).toBe("medium") }) -test("toggling the session sidebar flips the panel full → medium on a wide window (#1067)", () => { - // The exact regression #1067 reports: on a wide window, opening the 42-col - // sidebar must shrink the panel out of `full` — it no longer has ~110 usable cols. +test("toggling the in-flow session sidebar flips the panel full → medium on a wide window (#1067)", () => { + // The exact regression #1067 reports: on a wide window the sidebar is in-flow, + // and opening it must shrink the panel out of `full` (no longer ≥110 usable cols). expect(session(150, 50, false)).toBe("full") expect(session(150, 50, true)).toBe("medium") }) +test("the content column narrows whenever the sidebar is open, incl. the overlay (aligned with messages)", () => { + // On a ≤120-col terminal the sidebar is a dimmed full-area overlay, but the + // content column (messages + panel) still narrows uniformly so they stay + // aligned and both restore when it closes — sizing to the narrowed column, not + // the transient obscured width. 100 cols: open → 54 usable → compact; closed → + // 96 → medium. (Matches session's contentWidth basis; deliberate, per review.) + expect(session(100, 50, true)).toBe("compact") + expect(session(100, 50, false)).toBe("medium") +}) + test("a short terminal drops to compact once the route's chrome is reserved (#1067 height)", () => { // 120x22: wide, but too few usable rows after the home chrome → compact, where // the raw terminal height (22) would have picked medium. expect(home(120, 22)).toBe("compact") }) +test("negative available height (a tiny terminal) collapses to compact, never throws", () => { + // 80x5 → home available height 5 - 15 = -10; must resolve, not crash. + expect(home(80, 5)).toBe("compact") + expect(session(80, 5, false)).toBe("compact") +}) + test("degenerate sizes collapse to compact", () => { expect(welcomePanelVariant(0, 0)).toBe("compact") expect(welcomePanelVariant(1, 1)).toBe("compact") + expect(welcomePanelVariant(-5, -5)).toBe("compact") }) From 2762efb97c4c2629956997034bd6f8297a7fe211 Mon Sep 17 00:00:00 2001 From: Sarav Date: Fri, 7 Aug 2026 08:32:26 +0530 Subject: [PATCH 3/3] test(tui): render-test the welcome panel variants (real component) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mounts the real WelcomePanel through the app context stack at fixed available sizes and asserts each variant renders its distinct content (full: "What is Altimate Code"; medium: the condensed line, no what-is section; compact: the single connect-CTA line). Covers the .tsx Switch that the pure welcomePanelVariant unit test can't. Deliberately not a full Home/Session route render — that drags in Prompt's ~28-context dependency graph; the route wiring is now a shared one-liner through homeAvailable/sessionAvailable. Co-Authored-By: Claude Opus 4.8 --- .../tui/test/component/welcome-panel.test.tsx | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 packages/tui/test/component/welcome-panel.test.tsx diff --git a/packages/tui/test/component/welcome-panel.test.tsx b/packages/tui/test/component/welcome-panel.test.tsx new file mode 100644 index 000000000..b5e735d18 --- /dev/null +++ b/packages/tui/test/component/welcome-panel.test.tsx @@ -0,0 +1,97 @@ +/** @jsxImportSource @opentui/solid */ +import { testRender } from "@opentui/solid" +import { afterEach, expect, test } from "bun:test" +import { resetSetupComplete } from "../../src/component/altimate-onboarding" +import { WelcomePanel } from "../../src/component/welcome-panel" +import { TuiConfigProvider } from "../../src/config" +import { FULL_MIN_HEIGHT, FULL_MIN_WIDTH, MEDIUM_MIN_WIDTH } from "../../src/component/welcome-panel-utils" +import { ArgsProvider } from "../../src/context/args" +import { ExitProvider } from "../../src/context/exit" +import { KVProvider } from "../../src/context/kv" +import { ProjectProvider } from "../../src/context/project" +import { RouteProvider } from "../../src/context/route" +import { SDKProvider } from "../../src/context/sdk" +import { SyncProvider } from "../../src/context/sync" +import { ThemeProvider } from "../../src/context/theme" +import { ToastProvider } from "../../src/ui/toast" +import { TestTuiContexts } from "../fixture/tui-environment" +import { createTuiResolvedConfig } from "../fixture/tui-runtime" +import { createEventSource, createFetch, directory } from "../fixture/tui-sdk" + +// Render the REAL WelcomePanel through the routes' variant sizing and assert the +// rendered content per variant — the piece the pure welcomePanelVariant unit test +// can't cover (that the .tsx Switch actually renders the right box for a variant). +// Sizing is driven by the availableWidth/availableHeight props exactly as the +// routes pass them; the canvas is generous so content is captured without clipping. +async function renderPanel(availableWidth: number, availableHeight: number) { + resetSetupComplete() // ready() = false → deterministic (unconnected: shows the connect CTA) + const calls = createFetch() + const source = createEventSource() + const app = await testRender( + () => ( + + {}}> + + + + + + + + + + + + + + + + + + + + + + ), + { width: 160, height: 40 }, + ) + await app.renderOnce() + await new Promise((resolve) => setTimeout(resolve, 25)) + await app.renderOnce() + let frame = app.captureCharFrame() + for (let attempt = 0; attempt < 5 && frame.trim().length === 0; attempt++) { + await new Promise((resolve) => setTimeout(resolve, 25)) + await app.renderOnce() + frame = app.captureCharFrame() + } + return { app, frame } +} + +let current: Awaited> | undefined +afterEach(() => { + current?.renderer.destroy() + current = undefined +}) + +test("full variant renders the wordmark + what-is section at a large available size", async () => { + const { app, frame } = await renderPanel(FULL_MIN_WIDTH, FULL_MIN_HEIGHT) + current = app + // "What is Altimate Code" is unique to the full two-column box. + expect(frame).toContain("What is Altimate Code") +}) + +test("medium variant renders the condensed line (no what-is section) below the full width floor", async () => { + const { app, frame } = await renderPanel(FULL_MIN_WIDTH - 1, FULL_MIN_HEIGHT) + current = app + expect(frame).toContain("Gives your AI real context") + expect(frame).not.toContain("What is Altimate Code") +}) + +test("compact variant renders a single line below the medium width floor", async () => { + const { app, frame } = await renderPanel(MEDIUM_MIN_WIDTH - 1, FULL_MIN_HEIGHT) + current = app + // Unconnected → the connect CTA; neither the medium nor full body is present. + expect(frame).toContain("Connect your AI model to start") + expect(frame).not.toContain("Gives your AI real context") + expect(frame).not.toContain("What is Altimate Code") +})