diff --git a/ui-tui/src/banner.ts b/ui-tui/src/banner.ts index 80da8f43d70..748e5a452bc 100644 --- a/ui-tui/src/banner.ts +++ b/ui-tui/src/banner.ts @@ -79,8 +79,8 @@ const colorize = (art: string[], gradient: readonly number[], c: ThemeColors): L return art.map((text, i) => [p[gradient[i]!] ?? c.muted, text]) } -export const LOGO_WIDTH = 98 -export const CADUCEUS_WIDTH = 30 +export const LOGO_WIDTH = Math.max(...LOGO_ART.map(line => line.length)) +export const CADUCEUS_WIDTH = Math.max(...CADUCEUS_ART.map(line => line.length)) export const logo = (c: ThemeColors, customLogo?: string): Line[] => customLogo ? parseRichMarkup(customLogo) : colorize(LOGO_ART, LOGO_GRADIENT, c) diff --git a/ui-tui/src/components/appLayout.tsx b/ui-tui/src/components/appLayout.tsx index 2e35c75c307..8b69b9e4425 100644 --- a/ui-tui/src/components/appLayout.tsx +++ b/ui-tui/src/components/appLayout.tsx @@ -112,9 +112,9 @@ const TranscriptPane = memo(function TranscriptPane({ {row.msg.kind === 'intro' ? ( - + - {row.msg.info && } + {row.msg.info && } ) : row.msg.kind === 'panel' && row.msg.panelData ? ( diff --git a/ui-tui/src/components/branding.tsx b/ui-tui/src/components/branding.tsx index b7590f695e8..4f2bbb5eae5 100644 --- a/ui-tui/src/components/branding.tsx +++ b/ui-tui/src/components/branding.tsx @@ -29,31 +29,92 @@ function InlineLoader({ label, t }: { label: string; t: Theme }) { export function ArtLines({ lines }: { lines: [string, string][] }) { return ( - <> + {lines.map(([c, text], i) => ( - + {text} ))} - + ) } -export function Banner({ t }: { t: Theme }) { - const cols = useStdout().stdout?.columns ?? 80 +// Responsive Banner: full art → compact rule → text → hidden. +// +// Terminals can't scale glyphs, so "responsive" means picking a layout that +// fits the available columns. Thresholds are picked so each tier reads +// comfortably without forcing wrap or truncation drift on box-drawing edges. +const TAG_FULL = 'Nous Research · Messenger of the Digital Gods' +const TAG_MID = 'Messenger of the Digital Gods' +const TAG_TINY = 'Nous Research' +const HIDE_BELOW = 34 +const COMPACT_FROM = 58 + +const clip = (s: string, w: number) => + w <= 0 ? '' : s.length > w ? `${s.slice(0, Math.max(0, w - 1))}…` : s + +const centerIn = (s: string, w: number) => { + const f = clip(s, w) + const slack = Math.max(0, w - f.length) + const left = slack >> 1 + + return `${' '.repeat(left)}${f}${' '.repeat(slack - left)}` +} + +const ruleIn = (label: string, w: number) => { + const f = clip(label, Math.max(1, w - 4)) + const slack = Math.max(0, w - f.length - 2) + const left = slack >> 1 + + return `${'─'.repeat(left)} ${f} ${'─'.repeat(slack - left)}` +} + +function CompactBanner({ cols, t }: { cols: number; t: Theme }) { + // -4 keeps a margin so exact-edge rows don't trip terminal pending-wrap. + const w = Math.max(28, cols - 4) + + return ( + + {ruleIn(t.brand.name, w)} + {centerIn(TAG_FULL, w)} + {'─'.repeat(w)} + + ) +} + +export function Banner({ maxWidth, t }: { maxWidth?: number; t: Theme }) { + const term = useStdout().stdout?.columns ?? 80 + const cols = Math.max(1, Math.min(term, maxWidth ?? term)) + + if (cols < HIDE_BELOW) { + return null + } + const logoLines = logo(t.color, t.bannerLogo || undefined) + const logoW = t.bannerLogo ? artWidth(logoLines) : LOGO_WIDTH + + if (cols >= logoW + 2) { + return ( + + + + {t.brand.icon} {TAG_FULL} + + + ) + } + + if (cols >= COMPACT_FROM) { + return + } + + const name = cols >= 52 ? t.brand.name : (t.brand.name.split(' ')[0] ?? t.brand.name) + const tag = cols >= 64 ? TAG_FULL : cols >= 46 ? TAG_MID : TAG_TINY return ( - {cols >= (t.bannerLogo ? artWidth(logoLines) : LOGO_WIDTH) ? ( - - ) : ( - - {t.brand.icon} NOUS HERMES - - )} - - {t.brand.icon} Nous Research · Messenger of the Digital Gods + {t.brand.icon} {name} + {t.brand.icon} {tag} ) } @@ -96,8 +157,9 @@ function CollapseToggle({ const SKILLS_MAX = 8 const TOOLSETS_MAX = 8 -export function SessionPanel({ info, sid, t }: SessionPanelProps) { - const cols = useStdout().stdout?.columns ?? 100 +export function SessionPanel({ info, maxWidth, sid, t }: SessionPanelProps) { + const term = useStdout().stdout?.columns ?? 100 + const cols = Math.max(20, Math.min(term, maxWidth ?? term)) const heroLines = caduceus(t.color, t.bannerHero || undefined) const leftW = Math.min((artWidth(heroLines) || CADUCEUS_WIDTH) + 4, Math.floor(cols * 0.4)) const wide = cols >= 90 && leftW + 40 < cols @@ -241,13 +303,33 @@ export function SessionPanel({ info, sid, t }: SessionPanelProps) { )} - - - {t.brand.name} - {info.version ? ` v${info.version}` : ''} - {info.release_date ? ` (${info.release_date})` : ''} - - + {wide ? ( + + + {t.brand.name} + {info.version ? ` v${info.version}` : ''} + {info.release_date ? ` (${info.release_date})` : ''} + + + ) : ( + // Narrow layout hides the hero column; surface model/cwd/session + // here so they aren't lost. + + + {info.model.split('/').pop()} + · Nous Research + + + {info.cwd || process.cwd()} + + {sid && ( + + Session: + {sid} + + )} + + )} {/* ── Tools (expanded by default) ── */} @@ -378,6 +460,7 @@ interface PanelProps { interface SessionPanelProps { info: SessionInfo + maxWidth?: number sid?: string | null t: Theme }