diff --git a/packages/tui/src/component/welcome-panel-utils.ts b/packages/tui/src/component/welcome-panel-utils.ts new file mode 100644 index 0000000000..853abc2e61 --- /dev/null +++ b/packages/tui/src/component/welcome-panel-utils.ts @@ -0,0 +1,46 @@ +// Pure breakpoint logic for WelcomePanel, split out (like upgrade-indicator-utils) +// so it's unit-testable without the component's render/context dependencies. +// +// The full two-column boot box (block wordmark + description) is a ~13-row +// 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. +// - 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. +// `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. + +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 + +/** 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 FULL_MIN_WIDTH = 110 +export const FULL_MIN_HEIGHT = 36 + +/** + * 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 + * terminal, would still pick `full`). + */ +export function welcomePanelVariant(width: number, height: number): WelcomePanelVariant { + if (width < MEDIUM_MIN_WIDTH || height < MEDIUM_MIN_HEIGHT) return "compact" + if (width < FULL_MIN_WIDTH || height < FULL_MIN_HEIGHT) return "medium" + return "full" +} diff --git a/packages/tui/src/component/welcome-panel.tsx b/packages/tui/src/component/welcome-panel.tsx index b9ea310bfd..b8c16eff9c 100644 --- a/packages/tui/src/component/welcome-panel.tsx +++ b/packages/tui/src/component/welcome-panel.tsx @@ -1,78 +1,139 @@ -import { Show } from "solid-js" +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" import { useReady } from "./altimate-onboarding" +import { welcomePanelVariant } from "./welcome-panel-utils" -// altimate_change — Claude-Code-style full-width boot box: big block wordmark on -// the left and a single "What is Altimate Code" section on the right. Shared -// between the home route and the session view so the header stays consistent -// when a command (e.g. /discover) starts a session. zIndex keeps it above -// transient top toasts (update/MCP) that would otherwise blank its top rows. -export function WelcomePanel() { +const CONNECT_CTA = "Connect your AI model to start." + +// altimate_change — Claude-Code-style boot box: "What is Altimate Code" plus the +// readiness-aware CTA. Shared between the home route and the session view so the +// header stays consistent when a command (e.g. /discover) starts a session. +// zIndex keeps it above transient top toasts (update/MCP) that would otherwise +// 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): +// full — wordmark + full description (large windows only) +// medium — title + one condensed line, 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 }) { const { theme } = useTheme() const ready = useReady() + const dimensions = useTerminalDimensions() + + const variant = createMemo(() => + welcomePanelVariant(props.availableWidth ?? dimensions().width, props.availableHeight ?? dimensions().height), + ) + + const title = InstallationVersion === "local" ? " Altimate Code " : ` Altimate Code v${InstallationVersion} ` + return ( - {/* left column — the block-letter wordmark (59 cols wide) */} - - - Welcome to Altimate Code - - - - {/* right column — tips + what-is sections */} - - - - What is Altimate Code - - - Altimate Code is a specialized data engineering harness that sits between any LLM and your entire data - stack. - - - It gives your AI real context — column-level lineage, SQL analysis, dbt, and live warehouse metadata — so it - reasons about your data instead of guessing. - - {/* CTA only until a model is connected — stale afterwards */} - - - Connect your AI model to start. + + {/* compact — a short line; the border title already carries the version. + wrapMode keeps it readable if the terminal is extremely narrow. */} + + + + {ready() ? "Your data-aware AI harness." : CONNECT_CTA} - - - + + + + {/* medium — title + one condensed description + CTA, no block wordmark */} + + + + Welcome to Altimate Code + + + Gives your AI real context on your data stack — lineage, SQL, dbt, and live warehouse metadata. + + + + {CONNECT_CTA} + + + + + + {/* full — the original two-column boot box */} + + + {/* left column — the block-letter wordmark (logo is ~50 cols) */} + + + Welcome to Altimate Code + + + + {/* right column — what-is section */} + + + + What is Altimate Code + + + Altimate Code is a specialized data engineering harness that sits between any LLM and your entire data + stack. + + + It gives your AI real context — column-level lineage, SQL analysis, dbt, and live warehouse metadata — + so it reasons about your data instead of guessing. + + {/* CTA only until a model is connected — stale afterwards */} + + + {CONNECT_CTA} + + + + + + + ) } diff --git a/packages/tui/src/routes/home.tsx b/packages/tui/src/routes/home.tsx index d0926bec82..caa6d10e88 100644 --- a/packages/tui/src/routes/home.tsx +++ b/packages/tui/src/routes/home.tsx @@ -19,6 +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" // altimate_change end let once = false @@ -108,7 +109,13 @@ 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. */} + diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index a00e321070..0521a6248c 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -26,6 +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 { createSyntaxStyleMemo, generateSubtleSyntax, selectedForeground, useTheme } from "../../context/theme" import { BoxRenderable, ScrollBoxRenderable, addDefaultParsers, TextAttributes, RGBA } from "@opentui/core" import { Prompt, type PromptRef } from "../../component/prompt" @@ -1186,7 +1187,13 @@ 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; + -PANEL_VERTICAL_RESERVE leaves room for the prompt + footer. */} + {/* altimate_change end */} { + expect(welcomePanelVariant(WIDE, TALL)).toBe("full") + expect(welcomePanelVariant(FULL_MIN_WIDTH, FULL_MIN_HEIGHT)).toBe("full") // exactly at the floor +}) + +test("full's width gate is real — width one below the floor drops to medium even when tall", () => { + expect(welcomePanelVariant(FULL_MIN_WIDTH - 1, TALL)).toBe("medium") +}) + +test("full's height gate is real — height one below the floor drops to medium even when wide", () => { + expect(welcomePanelVariant(WIDE, FULL_MIN_HEIGHT - 1)).toBe("medium") +}) + +test("compact's width gate is real — width one below the medium floor is compact even when tall", () => { + expect(welcomePanelVariant(MEDIUM_MIN_WIDTH - 1, TALL)).toBe("compact") +}) + +test("compact's height gate is real — height one below the medium floor is compact even when wide", () => { + expect(welcomePanelVariant(WIDE, MEDIUM_MIN_HEIGHT - 1)).toBe("compact") +}) + +test("compact→medium boundary is exact (at the floor is medium)", () => { + expect(welcomePanelVariant(MEDIUM_MIN_WIDTH, MEDIUM_MIN_HEIGHT)).toBe("medium") + expect(welcomePanelVariant(MEDIUM_MIN_WIDTH, TALL)).toBe("medium") + expect(welcomePanelVariant(WIDE, MEDIUM_MIN_HEIGHT)).toBe("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") + // #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") +}) + +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("degenerate sizes collapse to compact", () => { + expect(welcomePanelVariant(0, 0)).toBe("compact") + expect(welcomePanelVariant(1, 1)).toBe("compact") +})