mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-29 06:31:32 +00:00
feat(tui): responsive banner tiers
Terminals can't scale glyphs, so the banner now picks a layout per column width instead of always rendering the full 101-col logo: - Wide (>= logo width): full ASCII logo + tagline. - Mid (>= 58 cols): centered rule banner that expands with viewport. - Narrow (>= 34 cols): brand line + tagline, both width-aware. - < 34 cols: hidden. SessionPanel surfaces model/cwd/sid inline when the hero column is hidden, so narrow layouts don't lose that info. Logo width constants derive from the art itself.
This commit is contained in:
parent
0277194e3b
commit
35fdf11145
3 changed files with 110 additions and 27 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -112,9 +112,9 @@ const TranscriptPane = memo(function TranscriptPane({
|
|||
|
||||
{row.msg.kind === 'intro' ? (
|
||||
<Box flexDirection="column" paddingTop={1}>
|
||||
<Banner t={ui.theme} />
|
||||
<Banner maxWidth={Math.max(1, composer.cols - 2)} t={ui.theme} />
|
||||
|
||||
{row.msg.info && <SessionPanel info={row.msg.info} sid={ui.sid} t={ui.theme} />}
|
||||
{row.msg.info && <SessionPanel info={row.msg.info} maxWidth={Math.max(1, composer.cols - 2)} sid={ui.sid} t={ui.theme} />}
|
||||
</Box>
|
||||
) : row.msg.kind === 'panel' && row.msg.panelData ? (
|
||||
<Panel sections={row.msg.panelData.sections} t={ui.theme} title={row.msg.panelData.title} />
|
||||
|
|
|
|||
|
|
@ -29,31 +29,92 @@ function InlineLoader({ label, t }: { label: string; t: Theme }) {
|
|||
|
||||
export function ArtLines({ lines }: { lines: [string, string][] }) {
|
||||
return (
|
||||
<>
|
||||
<Box flexDirection="column" height={lines.length} opaque width={artWidth(lines)}>
|
||||
{lines.map(([c, text], i) => (
|
||||
<Text color={c} key={i}>
|
||||
<Text color={c} key={i} wrap="truncate-end">
|
||||
{text}
|
||||
</Text>
|
||||
))}
|
||||
</>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<Box flexDirection="column" height={3} marginBottom={1} opaque width={w}>
|
||||
<Text bold color={t.color.primary}>{ruleIn(t.brand.name, w)}</Text>
|
||||
<Text color={t.color.muted}>{centerIn(TAG_FULL, w)}</Text>
|
||||
<Text color={t.color.primary}>{'─'.repeat(w)}</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<ArtLines lines={logoLines} />
|
||||
<Text color={t.color.muted} wrap="truncate-end">
|
||||
{t.brand.icon} {TAG_FULL}
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
if (cols >= COMPACT_FROM) {
|
||||
return <CompactBanner cols={cols} t={t} />
|
||||
}
|
||||
|
||||
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 (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
{cols >= (t.bannerLogo ? artWidth(logoLines) : LOGO_WIDTH) ? (
|
||||
<ArtLines lines={logoLines} />
|
||||
) : (
|
||||
<Text bold color={t.color.primary}>
|
||||
{t.brand.icon} NOUS HERMES
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Text color={t.color.muted}>{t.brand.icon} Nous Research · Messenger of the Digital Gods</Text>
|
||||
<Text bold color={t.color.primary} wrap="truncate-end">{t.brand.icon} {name}</Text>
|
||||
<Text color={t.color.muted} wrap="truncate-end">{t.brand.icon} {tag}</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
|
@ -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) {
|
|||
)}
|
||||
|
||||
<Box flexDirection="column" width={w}>
|
||||
<Box justifyContent="center" marginBottom={1}>
|
||||
<Text bold color={t.color.primary}>
|
||||
{t.brand.name}
|
||||
{info.version ? ` v${info.version}` : ''}
|
||||
{info.release_date ? ` (${info.release_date})` : ''}
|
||||
</Text>
|
||||
</Box>
|
||||
{wide ? (
|
||||
<Box justifyContent="center" marginBottom={1}>
|
||||
<Text bold color={t.color.primary}>
|
||||
{t.brand.name}
|
||||
{info.version ? ` v${info.version}` : ''}
|
||||
{info.release_date ? ` (${info.release_date})` : ''}
|
||||
</Text>
|
||||
</Box>
|
||||
) : (
|
||||
// Narrow layout hides the hero column; surface model/cwd/session
|
||||
// here so they aren't lost.
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text color={t.color.accent} wrap="truncate-end">
|
||||
{info.model.split('/').pop()}
|
||||
<Text color={t.color.muted}> · Nous Research</Text>
|
||||
</Text>
|
||||
<Text color={t.color.muted} wrap="truncate-end">
|
||||
{info.cwd || process.cwd()}
|
||||
</Text>
|
||||
{sid && (
|
||||
<Text wrap="truncate-end">
|
||||
<Text color={t.color.sessionLabel}>Session: </Text>
|
||||
<Text color={t.color.sessionBorder}>{sid}</Text>
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* ── Tools (expanded by default) ── */}
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
|
|
@ -378,6 +460,7 @@ interface PanelProps {
|
|||
|
||||
interface SessionPanelProps {
|
||||
info: SessionInfo
|
||||
maxWidth?: number
|
||||
sid?: string | null
|
||||
t: Theme
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue