Skip to content

Commit 721fee6

Browse files
saravmajesticclaude
andcommitted
fix(tui): make the welcome panel responsive to terminal size (#1067)
The full two-column boot box (block wordmark + description) is ~65 cols wide and ~13 rows tall, so on a typical terminal it ate ~40-50% of the screen with no way to shrink it (#1067). It now scales by terminal size, using the repo's breakpoint idiom (useTerminalDimensions + createMemo, cf. routes/session/permission.tsx, component/upgrade-indicator.tsx) — reactive, so it adapts live on resize: - full — the two-column wordmark box, reserved for large windows (>=110w & >=44h) - medium — title + one condensed line, no wordmark (the common case, ~6 rows) - compact — a single line; the border title already shows the version (<60w or <18h) The breakpoint decision is a pure function in welcome-panel-utils.ts (mirroring upgrade-indicator-utils.ts) so it's unit-testable without the render/sync context; 4 tests cover the thresholds and that the smaller dimension wins. Scope: responsiveness only — no dismiss/collapse behavior change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 8ae6c02 commit 721fee6

3 files changed

Lines changed: 159 additions & 45 deletions

File tree

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Pure breakpoint logic for WelcomePanel, split out (like upgrade-indicator-utils)
2+
// so it's unit-testable without the component's render/context dependencies.
3+
//
4+
// The full two-column boot box (block wordmark + description) is ~65 cols wide
5+
// and ~8 rows tall, which eats half a small terminal (issue #1067). We scale it
6+
// down by terminal size. Both axes matter — the wordmark is wide AND tall — so
7+
// each threshold is a min across width and height. Values follow the repo's
8+
// breakpoint idiom (routes/session/permission.tsx uses width < 80; upgrade-
9+
// indicator uses width < 100).
10+
11+
export type WelcomePanelVariant = "full" | "medium" | "compact"
12+
13+
// The full panel is ~13 rows tall, so it's reserved for genuinely large windows
14+
// — on a typical laptop terminal (80–110 wide, 24–35 tall) it would still eat
15+
// ~40% of the screen, which is the problem #1067 is about. So `medium` (no
16+
// wordmark, ~6 rows) is the common case; `full` only when there's real room.
17+
/** Below these, drop to the single-line compact panel. */
18+
export const COMPACT_MAX_WIDTH = 60
19+
export const COMPACT_MAX_HEIGHT = 18
20+
/** At/above these, show the full two-column wordmark panel; below → medium. */
21+
export const MEDIUM_MAX_WIDTH = 110
22+
export const MEDIUM_MAX_HEIGHT = 44
23+
24+
/** Choose the WelcomePanel layout for a given terminal size. */
25+
export function welcomePanelVariant(width: number, height: number): WelcomePanelVariant {
26+
if (width < COMPACT_MAX_WIDTH || height < COMPACT_MAX_HEIGHT) return "compact"
27+
if (width < MEDIUM_MAX_WIDTH || height < MEDIUM_MAX_HEIGHT) return "medium"
28+
return "full"
29+
}
Lines changed: 98 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,78 +1,131 @@
1-
import { Show } from "solid-js"
1+
import { Show, createMemo } from "solid-js"
22
import { TextAttributes } from "@opentui/core"
3+
import { useTerminalDimensions } from "@opentui/solid"
34
import { useTheme } from "../context/theme"
45
import { Logo } from "./logo"
56
import { InstallationVersion } from "@opencode-ai/core/installation/version"
67
import { useReady } from "./altimate-onboarding"
8+
import { welcomePanelVariant } from "./welcome-panel-utils"
79

8-
// altimate_change — Claude-Code-style full-width boot box: big block wordmark on
9-
// the left and a single "What is Altimate Code" section on the right. Shared
10-
// between the home route and the session view so the header stays consistent
11-
// when a command (e.g. /discover) starts a session. zIndex keeps it above
12-
// transient top toasts (update/MCP) that would otherwise blank its top rows.
10+
// altimate_change — Claude-Code-style boot box: "What is Altimate Code" plus the
11+
// readiness-aware CTA. Shared between the home route and the session view so the
12+
// header stays consistent when a command (e.g. /discover) starts a session.
13+
// zIndex keeps it above transient top toasts (update/MCP) that would otherwise
14+
// blank its top rows.
15+
//
16+
// Responsive (issue #1067): the full two-column boot box with the block wordmark
17+
// is ~65 cols wide and ~8 rows tall, which eats half a small terminal. So it
18+
// scales down by terminal size, following the repo's breakpoint idiom
19+
// (createMemo on useTerminalDimensions, e.g. routes/session/permission.tsx,
20+
// component/upgrade-indicator.tsx). Both axes matter — the wordmark is wide AND
21+
// tall:
22+
// full — wordmark + full description (large terminals)
23+
// medium — title + one condensed line, no wordmark
24+
// compact — a single line; the border title already shows the version
1325
export function WelcomePanel() {
1426
const { theme } = useTheme()
1527
const ready = useReady()
28+
const dimensions = useTerminalDimensions()
29+
30+
const variant = createMemo(() => welcomePanelVariant(dimensions().width, dimensions().height))
31+
const compact = createMemo(() => variant() === "compact")
32+
const medium = createMemo(() => variant() === "medium")
33+
const full = createMemo(() => variant() === "full")
34+
35+
const title = InstallationVersion === "local" ? " Altimate Code " : ` Altimate Code v${InstallationVersion} `
36+
1637
return (
1738
<box
1839
border
1940
borderStyle="rounded"
2041
borderColor={theme.border}
21-
title={InstallationVersion === "local" ? " Altimate Code " : ` Altimate Code v${InstallationVersion} `}
42+
title={title}
2243
titleAlignment="left"
2344
flexShrink={0}
2445
width="100%"
2546
zIndex={2000}
2647
backgroundColor={theme.background}
27-
flexDirection="row"
48+
flexDirection="column"
2849
>
29-
{/* left column — the block-letter wordmark (59 cols wide) */}
30-
<box
31-
width={65}
32-
flexShrink={0}
33-
alignItems="center"
34-
justifyContent="center"
35-
gap={1}
36-
paddingTop={1}
37-
paddingBottom={1}
38-
paddingLeft={1}
39-
>
40-
<text fg={theme.text} attributes={TextAttributes.BOLD}>
41-
Welcome to Altimate Code
42-
</text>
43-
<Logo />
44-
</box>
45-
{/* right column — tips + what-is sections */}
46-
<box
47-
flexGrow={1}
48-
border={["left"]}
49-
borderColor={theme.border}
50-
paddingLeft={2}
51-
paddingRight={2}
52-
paddingTop={1}
53-
paddingBottom={1}
54-
gap={1}
55-
>
56-
<box gap={0}>
57-
<text fg={theme.accent} attributes={TextAttributes.BOLD}>
58-
What is Altimate Code
50+
{/* compact — one line; the border title already carries the version */}
51+
<Show when={compact()}>
52+
<box paddingLeft={2} paddingRight={2} width="100%">
53+
<text fg={ready() ? theme.text : theme.primary} wrapMode="word" width="100%">
54+
{ready() ? "Your data-aware AI harness." : "Connect your AI model to start."}
5955
</text>
60-
<text fg={theme.text} wrapMode="word" width="100%">
61-
Altimate Code is a specialized data engineering harness that sits between any LLM and your entire data
62-
stack.
56+
</box>
57+
</Show>
58+
59+
{/* medium — title + one condensed description + CTA, no block wordmark */}
60+
<Show when={medium()}>
61+
<box paddingLeft={2} paddingRight={2} paddingTop={1} paddingBottom={1} gap={0} width="100%">
62+
<text fg={theme.text} attributes={TextAttributes.BOLD}>
63+
Welcome to Altimate Code
6364
</text>
6465
<text fg={theme.text} wrapMode="word" width="100%">
65-
It gives your AI real context — column-level lineage, SQL analysis, dbt, and live warehouse metadata — so it
66-
reasons about your data instead of guessing.
66+
A data-engineering harness that gives your AI real context — column-level lineage, SQL analysis, dbt, and
67+
live warehouse metadata.
6768
</text>
68-
{/* CTA only until a model is connected — stale afterwards */}
6969
<Show when={!ready()}>
7070
<text fg={theme.primary} wrapMode="word" width="100%">
7171
Connect your AI model to start.
7272
</text>
7373
</Show>
7474
</box>
75-
</box>
75+
</Show>
76+
77+
{/* full — the original two-column boot box */}
78+
<Show when={full()}>
79+
<box flexDirection="row" width="100%">
80+
{/* left column — the block-letter wordmark */}
81+
<box
82+
width={65}
83+
flexShrink={0}
84+
alignItems="center"
85+
justifyContent="center"
86+
gap={1}
87+
paddingTop={1}
88+
paddingBottom={1}
89+
paddingLeft={1}
90+
>
91+
<text fg={theme.text} attributes={TextAttributes.BOLD}>
92+
Welcome to Altimate Code
93+
</text>
94+
<Logo />
95+
</box>
96+
{/* right column — what-is section */}
97+
<box
98+
flexGrow={1}
99+
border={["left"]}
100+
borderColor={theme.border}
101+
paddingLeft={2}
102+
paddingRight={2}
103+
paddingTop={1}
104+
paddingBottom={1}
105+
gap={1}
106+
>
107+
<box gap={0}>
108+
<text fg={theme.accent} attributes={TextAttributes.BOLD}>
109+
What is Altimate Code
110+
</text>
111+
<text fg={theme.text} wrapMode="word" width="100%">
112+
Altimate Code is a specialized data engineering harness that sits between any LLM and your entire data
113+
stack.
114+
</text>
115+
<text fg={theme.text} wrapMode="word" width="100%">
116+
It gives your AI real context — column-level lineage, SQL analysis, dbt, and live warehouse metadata —
117+
so it reasons about your data instead of guessing.
118+
</text>
119+
{/* CTA only until a model is connected — stale afterwards */}
120+
<Show when={!ready()}>
121+
<text fg={theme.primary} wrapMode="word" width="100%">
122+
Connect your AI model to start.
123+
</text>
124+
</Show>
125+
</box>
126+
</box>
127+
</box>
128+
</Show>
76129
</box>
77130
)
78131
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { expect, test } from "bun:test"
2+
import {
3+
COMPACT_MAX_HEIGHT,
4+
COMPACT_MAX_WIDTH,
5+
MEDIUM_MAX_HEIGHT,
6+
MEDIUM_MAX_WIDTH,
7+
welcomePanelVariant,
8+
} from "../../src/component/welcome-panel-utils"
9+
10+
test("large terminals get the full two-column boot box", () => {
11+
expect(welcomePanelVariant(MEDIUM_MAX_WIDTH + 20, MEDIUM_MAX_HEIGHT + 10)).toBe("full")
12+
expect(welcomePanelVariant(MEDIUM_MAX_WIDTH, MEDIUM_MAX_HEIGHT)).toBe("full") // at the threshold
13+
})
14+
15+
test("a narrow OR short terminal drops the wordmark (medium)", () => {
16+
expect(welcomePanelVariant(80, 24)).toBe("medium")
17+
expect(welcomePanelVariant(MEDIUM_MAX_WIDTH - 1, 40)).toBe("medium") // narrow but tall
18+
expect(welcomePanelVariant(120, MEDIUM_MAX_HEIGHT - 1)).toBe("medium") // wide but short
19+
})
20+
21+
test("a very small terminal collapses to the single-line compact panel", () => {
22+
expect(welcomePanelVariant(50, 12)).toBe("compact")
23+
expect(welcomePanelVariant(COMPACT_MAX_WIDTH - 1, 40)).toBe("compact") // very narrow, any height
24+
expect(welcomePanelVariant(120, COMPACT_MAX_HEIGHT - 1)).toBe("compact") // very short, any width
25+
})
26+
27+
test("both axes gate each step down — the smaller dimension wins", () => {
28+
// Wide enough for full on width, but height forces compact.
29+
expect(welcomePanelVariant(200, COMPACT_MAX_HEIGHT - 1)).toBe("compact")
30+
// Tall enough for full on height, but width forces compact.
31+
expect(welcomePanelVariant(COMPACT_MAX_WIDTH - 1, 200)).toBe("compact")
32+
})

0 commit comments

Comments
 (0)