Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 73 additions & 19 deletions packages/tui/src/component/welcome-panel-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,42 +5,96 @@
// 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 (top spacer + prompt + footer), so a big panel is
// 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 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
// --- Chrome reserves (rows the panel must leave for always-present siblings) ---
//
// 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

/** Minimum usable size for the medium panel; below either → compact (one line). */
// home (routes/home.tsx): top spacer 2 (`<box height={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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The padding centralization is incomplete: PANEL_HORIZONTAL_PADDING is used at the home call site, but the session route's contentWidth still hardcodes the bare - 4 rather than referencing the constant (the new doc comment even acknowledges session "applies the same 4 as part of its own content-column math"). If this constant ever changes, the session width math won't follow and drifts out of sync; consider having session/index.tsx import PANEL_HORIZONTAL_PADDING so both routes share one source of truth.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/tui/src/component/welcome-panel-utils.ts, line 37:

<comment>The padding centralization is incomplete: PANEL_HORIZONTAL_PADDING is used at the home call site, but the session route's contentWidth still hardcodes the bare `- 4` rather than referencing the constant (the new doc comment even acknowledges session "applies the same 4 as part of its own content-column math"). If this constant ever changes, the session width math won't follow and drifts out of sync; consider having session/index.tsx import PANEL_HORIZONTAL_PADDING so both routes share one source of truth.</comment>

<file context>
@@ -7,36 +7,46 @@
+// 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). */
</file context>

// 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

// --- 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 = 16
/** 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 = 36
// ~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
* 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 {
if (width < MEDIUM_MIN_WIDTH || height < MEDIUM_MIN_HEIGHT) return "compact"
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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: On a narrow session with the sidebar toggled open, the welcome panel is sized as though 42 columns were removed even though the overlay leaves its layout box full width, so medium content can be laid out across the obscured area and appear truncated. The available-width calculation should distinguish the in-flow (wide()) sidebar from the absolute overlay, or the panel wrapper should be constrained to the narrowed width.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/tui/src/component/welcome-panel-utils.ts, line 97:

<comment>On a narrow session with the sidebar toggled open, the welcome panel is sized as though 42 columns were removed even though the overlay leaves its layout box full width, so medium content can be laid out across the obscured area and appear truncated. The available-width calculation should distinguish the in-flow (`wide()`) sidebar from the absolute overlay, or the panel wrapper should be constrained to the narrowed width.</comment>

<file context>
@@ -54,3 +69,32 @@ export function welcomePanelVariant(width: number, height: number): WelcomePanel
+  sidebarVisible: boolean,
+): { width: number; height: number } {
+  return {
+    width: terminalWidth - (sidebarVisible ? SIDEBAR_WIDTH : 0) - PANEL_HORIZONTAL_PADDING,
+    height: terminalHeight - SESSION_VERTICAL_RESERVE,
+  }
</file context>

height: terminalHeight - SESSION_VERTICAL_RESERVE,
}
}
31 changes: 17 additions & 14 deletions packages/tui/src/component/welcome-panel.tsx
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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} `

Expand Down
18 changes: 10 additions & 8 deletions packages/tui/src/routes/home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 { homeAvailable } from "../component/welcome-panel-utils"
// altimate_change end

let once = false
Expand Down Expand Up @@ -68,6 +68,11 @@ export function Home() {
if (configured === "auto") return Math.max(75, Math.floor(dimensions().width * 0.7))
return configured ?? 75
})
// altimate_change start — WelcomePanel responsive sizing (#1067): the panel's
// available space = terminal minus this route's padding + vertical reserve.
// Shared arithmetic in welcome-panel-utils so the tests exercise it.
const panelAvailable = createMemo(() => homeAvailable(dimensions().width, dimensions().height))
// altimate_change end
let sent = false

onMount(() => {
Expand Down Expand Up @@ -109,13 +114,10 @@ export function Home() {
<box height={2} flexShrink={0} />
<box width="100%" flexShrink={0}>
<pluginRuntime.Slot name="home_logo" mode="replace">
{/* 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. */}
<WelcomePanel
availableWidth={dimensions().width - 4}
availableHeight={dimensions().height - PANEL_VERTICAL_RESERVE}
/>
{/* 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. */}
<WelcomePanel availableWidth={panelAvailable().width} availableHeight={panelAvailable().height} />
</pluginRuntime.Slot>
</box>
<box flexGrow={1} minHeight={0} />
Expand Down
23 changes: 14 additions & 9 deletions packages/tui/src/routes/session/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 { 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"
Expand Down Expand Up @@ -272,7 +272,15 @@ export function Session() {
return false
})
const showTimestamps = createMemo(() => timestamps() === "show")
const contentWidth = createMemo(() => dimensions().width - (sidebarVisible() ? 42 : 0) - 4)
// altimate_change start — WelcomePanel responsive sizing (#1067). panelAvailable is the single
// source of the panel's usable size (via welcome-panel-utils, unit-tested); it narrows with the
// sidebar whenever open — including the dimmed full-area overlay on narrow terminals — so the
// panel and the messages stay aligned and both restore to full width when it closes. contentWidth
// (the shared content-column width) derives from it, so the width math has one definition and the
// two cannot structurally drift.
const panelAvailable = createMemo(() => sessionAvailable(dimensions().width, dimensions().height, sidebarVisible()))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: panelAvailable().width duplicates the contentWidth() formula

The note above states panelAvailable shares contentWidth's content-column basis, and sessionAvailable(...).width computes the exact same value as the contentWidth memo (line 275): width - (sidebarVisible ? SIDEBAR_WIDTH : 0) - PANEL_HORIZONTAL_PADDING. Since sessionAvailable is already the unit-tested single source, consider deriving contentWidth from it (declare panelAvailable first, then const contentWidth = createMemo(() => panelAvailable().width)) so the width math has one definition and can't structurally drift.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

const contentWidth = createMemo(() => panelAvailable().width)
// altimate_change end
const providers = createMemo(() => Model.index(sync.data.provider))

const scrollAcceleration = createMemo(() => getScrollAcceleration(tuiConfig))
Expand Down Expand Up @@ -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. */}
<box flexShrink={0}>
{/* 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. */}
<WelcomePanel
availableWidth={contentWidth()}
availableHeight={dimensions().height - PANEL_VERTICAL_RESERVE}
/>
{/* 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). */}
<WelcomePanel availableWidth={panelAvailable().width} availableHeight={panelAvailable().height} />
</box>
{/* altimate_change end */}
<scrollbox
Expand Down
86 changes: 72 additions & 14 deletions packages/tui/test/component/welcome-panel-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,41 @@ 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,
SESSION_VERTICAL_RESERVE,
homeAvailable,
sessionAvailable,
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 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")
expect(welcomePanelVariant(FULL_MIN_WIDTH, FULL_MIN_HEIGHT)).toBe("full") // exactly at the floor
Expand Down Expand Up @@ -42,23 +65,58 @@ 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")
// #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(home(106, 31)).toBe("medium")
expect(home(80, 24)).toBe("medium")
// #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 (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", () => {
// 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 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("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("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")
})
Loading
Loading