diff --git a/ui-tui/src/__tests__/loaders.test.ts b/ui-tui/src/__tests__/loaders.test.ts index cc3d838a260..cab44b8d2b5 100644 --- a/ui-tui/src/__tests__/loaders.test.ts +++ b/ui-tui/src/__tests__/loaders.test.ts @@ -9,7 +9,11 @@ describe('ShimmerRows leniency (agent-authored calls)', () => { const { createElement } = await import('react') const { screen, height } = renderToScreen( - createElement(ShimmerRows, { rows: 3, width: 20, t: { color: { completionBg: '#1a1a2e', label: '#DAA520', muted: '#B8860B' } } }), + createElement(ShimmerRows, { + rows: 3, + width: 20, + t: { color: { completionBg: '#1a1a2e', label: '#DAA520', muted: '#B8860B' } } + }), 30 ) diff --git a/ui-tui/src/__tests__/theme.test.ts b/ui-tui/src/__tests__/theme.test.ts index c7a00c19632..98bfb060639 100644 --- a/ui-tui/src/__tests__/theme.test.ts +++ b/ui-tui/src/__tests__/theme.test.ts @@ -303,7 +303,10 @@ describe('fromSkin', () => { }) it('keeps truecolor light Apple Terminal in truecolor (adapting, not ansi256-bucketing)', async () => { - const { contrastRatio, fromSkin } = await importThemeWithEnv({ COLORTERM: 'truecolor', TERM_PROGRAM: 'Apple_Terminal' }) + const { contrastRatio, fromSkin } = await importThemeWithEnv({ + COLORTERM: 'truecolor', + TERM_PROGRAM: 'Apple_Terminal' + }) const theme = fromSkin({ banner_text: '#FFF8DC' }, {}) // No ansi256 bucketing on truecolor terminals — a truly invisible cream @@ -418,10 +421,7 @@ describe('derived tone ladder', () => { const { fromSkin } = await importThemeWithCleanEnv() // A seeds-only skin (no dim/label/menu keys authored at all). - const { color } = fromSkin( - { banner_accent: '#DD4A3A', banner_text: '#F1E6CF', banner_title: '#C7A96B' }, - {} - ) + const { color } = fromSkin({ banner_accent: '#DD4A3A', banner_text: '#F1E6CF', banner_title: '#C7A96B' }, {}) // Muted recedes from THIS skin's text toward the background with an // accent tint — a red-family derivative, never another skin's gold. diff --git a/ui-tui/src/__tests__/widgetGrid.test.ts b/ui-tui/src/__tests__/widgetGrid.test.ts index 3fed524b6e3..fb99e7c90f6 100644 --- a/ui-tui/src/__tests__/widgetGrid.test.ts +++ b/ui-tui/src/__tests__/widgetGrid.test.ts @@ -193,11 +193,7 @@ describe('layoutGridAreas', () => { columns: 2, gap: 0, height: 12, - items: [ - { col: 1, id: 'pinned', row: 2 }, - { id: 'auto-a' }, - { id: 'auto-b' } - ], + items: [{ col: 1, id: 'pinned', row: 2 }, { id: 'auto-a' }, { id: 'auto-b' }], rowGap: 0, rows: 2, width: 10 diff --git a/ui-tui/src/app/createGatewayEventHandler.ts b/ui-tui/src/app/createGatewayEventHandler.ts index 1be2ea41598..320d7441064 100644 --- a/ui-tui/src/app/createGatewayEventHandler.ts +++ b/ui-tui/src/app/createGatewayEventHandler.ts @@ -47,10 +47,16 @@ const themeForSkin = (s: GatewaySkin) => { // still works — it just overrides every key it lists. const paired = detectLightMode() ? s.light_colors : s.dark_colors - const colors = - paired && Object.keys(paired).length ? { ...(s.colors ?? {}), ...paired } : (s.colors ?? {}) + const colors = paired && Object.keys(paired).length ? { ...(s.colors ?? {}), ...paired } : (s.colors ?? {}) - return fromSkin(colors, s.branding ?? {}, s.banner_logo ?? '', s.banner_hero ?? '', s.tool_prefix ?? '', s.help_header ?? '') + return fromSkin( + colors, + s.branding ?? {}, + s.banner_logo ?? '', + s.banner_hero ?? '', + s.tool_prefix ?? '', + s.help_header ?? '' + ) } // Patch the live theme AND persist it for the next launch's first frame @@ -95,7 +101,12 @@ const themesEqual = (a: Theme, b: Theme) => { } } - return a.brand.name === b.brand.name && a.brand.prompt === b.brand.prompt && a.bannerLogo === b.bannerLogo && a.bannerHero === b.bannerHero + return ( + a.brand.name === b.brand.name && + a.brand.prompt === b.brand.prompt && + a.bannerLogo === b.bannerLogo && + a.bannerHero === b.bannerHero + ) } const applySkin = (s: GatewaySkin) => { diff --git a/ui-tui/src/components/appChrome.tsx b/ui-tui/src/components/appChrome.tsx index 556770394d3..97994e82722 100644 --- a/ui-tui/src/components/appChrome.tsx +++ b/ui-tui/src/components/appChrome.tsx @@ -799,7 +799,9 @@ export function TranscriptScrollbar({ scrollRef, t }: TranscriptScrollbarProps) {`${'┃\n'.repeat(Math.max(0, thumb - 1))}${thumb > 0 ? '┃' : ''}`} ) : null} {vp - thumbTop - thumb > 0 ? ( - {`${'│\n'.repeat(Math.max(0, vp - thumbTop - thumb - 1))}${vp - thumbTop - thumb > 0 ? '│' : ''}`} + {`${'│\n'.repeat(Math.max(0, vp - thumbTop - thumb - 1))}${vp - thumbTop - thumb > 0 ? '│' : ''}`} ) : null} )} diff --git a/ui-tui/src/components/branding.tsx b/ui-tui/src/components/branding.tsx index e8495f717e4..fcc5f6f130d 100644 --- a/ui-tui/src/components/branding.tsx +++ b/ui-tui/src/components/branding.tsx @@ -401,117 +401,115 @@ export function SessionPanel({ info, maxWidth, sid, t }: SessionPanelProps) { const infoColumn = ( - {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) ── */} - - setToolsOpen(v => !v)} open={toolsOpen} t={t} title="Available Tools" /> - {toolsOpen && toolsBody()} + {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} + + )} + + )} - {/* ── Skills (collapsed by default) ── */} + {/* ── Tools (expanded by default) ── */} + + setToolsOpen(v => !v)} open={toolsOpen} t={t} title="Available Tools" /> + {toolsOpen && toolsBody()} + + + {/* ── Skills (collapsed by default) ── */} + + setSkillsOpen(v => !v)} + open={skillsOpen} + suffix={skillsCatCount > 0 ? `in ${skillsCatCount} categor${skillsCatCount === 1 ? 'y' : 'ies'}` : undefined} + t={t} + title="Available Skills" + /> + {skillsOpen && skillsBody()} + + + {/* ── System Prompt (collapsed by default) ── */} + {sysPromptLen > 0 && ( setSkillsOpen(v => !v)} - open={skillsOpen} - suffix={ - skillsCatCount > 0 ? `in ${skillsCatCount} categor${skillsCatCount === 1 ? 'y' : 'ies'}` : undefined - } + onToggle={() => setSystemOpen(v => !v)} + open={systemOpen} + suffix={`— ${sysPromptLen.toLocaleString()} chars`} t={t} - title="Available Skills" + title="System Prompt" /> - {skillsOpen && skillsBody()} + {systemOpen && systemBody()} + )} - {/* ── System Prompt (collapsed by default) ── */} - {sysPromptLen > 0 && ( - - setSystemOpen(v => !v)} - open={systemOpen} - suffix={`— ${sysPromptLen.toLocaleString()} chars`} - t={t} - title="System Prompt" - /> - {systemOpen && systemBody()} - - )} + {/* ── MCP Servers (collapsed by default) ── */} + {mcpServers.length > 0 && ( + + setMcpOpen(v => !v)} + open={mcpOpen} + suffix="connected" + t={t} + title="MCP Servers" + /> + {mcpOpen && mcpBody()} + + )} - {/* ── MCP Servers (collapsed by default) ── */} - {mcpServers.length > 0 && ( - - setMcpOpen(v => !v)} - open={mcpOpen} - suffix="connected" - t={t} - title="MCP Servers" - /> - {mcpOpen && mcpBody()} - - )} + - + + {/* Lazy boot: never print "0 tools · 0 skills" while counts load. */} + {info.lazy && !toolsTotal ? '… ' : `${toolsTotal} `}tools{' · '} + {info.lazy && !skillsTotal ? '… ' : `${skillsTotal} `}skills + {mcpConnected ? ` · ${mcpConnected} MCP` : ''} + {' · '} + /help for commands + - - {/* Lazy boot: never print "0 tools · 0 skills" while counts load. */} - {info.lazy && !toolsTotal ? '… ' : `${toolsTotal} `}tools{' · '} - {info.lazy && !skillsTotal ? '… ' : `${skillsTotal} `}skills - {mcpConnected ? ` · ${mcpConnected} MCP` : ''} - {' · '} - /help for commands - - - {typeof info.update_behind === 'number' && info.update_behind > 0 && ( + {typeof info.update_behind === 'number' && info.update_behind > 0 && ( + + ! {info.update_behind} {info.update_behind === 1 ? 'commit' : 'commits'} behind + + {' '} + - run{' '} + - ! {info.update_behind} {info.update_behind === 1 ? 'commit' : 'commits'} behind - - {' '} - - run{' '} - - - {info.update_command || 'hermes update'} - - - {' '} - to update - + {info.update_command || 'hermes update'} - )} + + {' '} + to update + + + )} - {info.install_warning && ( - - ! {info.install_warning} - - )} + {info.install_warning && ( + + ! {info.install_warning} + + )} ) diff --git a/ui-tui/src/components/gridStreamsDemo.tsx b/ui-tui/src/components/gridStreamsDemo.tsx index a838666e4c3..ce07acb48a1 100644 --- a/ui-tui/src/components/gridStreamsDemo.tsx +++ b/ui-tui/src/components/gridStreamsDemo.tsx @@ -90,12 +90,13 @@ const sparkRows = (history: number[], width: number, rows: number): string[] => // ── stream panels ─────────────────────────────────────────────────────────── -const TOKEN_WORDS = - (`Hermes streams tokens into the promoted cell while the grid reshapes around it. ` + - `Cells are keyed by id, so promotion never resets a panel — history, cursors and ` + - `tickers all survive the relayout. Row and column tracks re-solve to integer ` + - `terminal cells on every change, spans bridge the gaps they cross, and dense ` + - `auto-placement backfills the holes the promoted panel leaves behind. `).split(' ') +const TOKEN_WORDS = ( + `Hermes streams tokens into the promoted cell while the grid reshapes around it. ` + + `Cells are keyed by id, so promotion never resets a panel — history, cursors and ` + + `tickers all survive the relayout. Row and column tracks re-solve to integer ` + + `terminal cells on every change, spans bridge the gaps they cross, and dense ` + + `auto-placement backfills the holes the promoted panel leaves behind. ` +).split(' ') function TokenStream({ height, t, width }: { height: number; t: Theme; width: number }) { const tick = useTick(90) diff --git a/ui-tui/src/components/gridTestOverlay.tsx b/ui-tui/src/components/gridTestOverlay.tsx index b53ed031f67..9edab4af848 100644 --- a/ui-tui/src/components/gridTestOverlay.tsx +++ b/ui-tui/src/components/gridTestOverlay.tsx @@ -114,7 +114,7 @@ function AreasDemo({ cols, state, t }: { cols: number; state: GridTestState; t: const rowSpanFirst = state.rows >= 2 ? 2 : 1 const colSpanSecond = state.cols >= 2 ? 2 : 1 - const extraSlots = (rowSpanFirst - 1) + (colSpanSecond - 1) + const extraSlots = rowSpanFirst - 1 + (colSpanSecond - 1) const itemCount = Math.max(1, state.rows * state.cols - extraSlots) const cursorInside = (cell: GridAreaCell) => @@ -126,7 +126,9 @@ function AreasDemo({ cols, state, t }: { cols: number; state: GridTestState; t: const widgets: GridAreaWidget[] = Array.from({ length: itemCount }, (_, idx) => ({ colSpan: idx === 1 ? colSpanSecond : 1, id: `area-c${idx + 1}`, - render: (cell: GridAreaCell) => , + render: (cell: GridAreaCell) => ( + + ), rowSpan: idx === 0 ? rowSpanFirst : 1 })) diff --git a/ui-tui/src/components/overlayPrimitives.tsx b/ui-tui/src/components/overlayPrimitives.tsx index f86787f4ae4..b7cb7b60d98 100644 --- a/ui-tui/src/components/overlayPrimitives.tsx +++ b/ui-tui/src/components/overlayPrimitives.tsx @@ -116,7 +116,11 @@ export function MenuRow({ active, index, label, t }: { active: boolean; index: n return ( - + {active ? '▸ ' : ' '} {index}. {label} diff --git a/ui-tui/src/components/textInput.tsx b/ui-tui/src/components/textInput.tsx index c049ce078ff..c65fc8c5441 100644 --- a/ui-tui/src/components/textInput.tsx +++ b/ui-tui/src/components/textInput.tsx @@ -667,7 +667,9 @@ export function TextInput({ } if (!display && placeholder) { - return hintCursorCell(placeholder[0] ?? ' ', placeholderColor) + colorizeHint(placeholder.slice(1), placeholderColor) + return ( + hintCursorCell(placeholder[0] ?? ' ', placeholderColor) + colorizeHint(placeholder.slice(1), placeholderColor) + ) } if (selected) { diff --git a/ui-tui/src/components/widgetGrid.tsx b/ui-tui/src/components/widgetGrid.tsx index 6867210d697..0c34c791534 100644 --- a/ui-tui/src/components/widgetGrid.tsx +++ b/ui-tui/src/components/widgetGrid.tsx @@ -255,17 +255,12 @@ export const GridAreas = memo(function GridAreas({ const AreaCell = memo(function AreaCell({ cell, widget }: { cell: GridAreaCell; widget?: GridAreaWidget }) { const node = - widget?.render?.(cell) ?? (typeof widget?.children === 'function' ? widget.children(cell) : widget?.children) ?? null + widget?.render?.(cell) ?? + (typeof widget?.children === 'function' ? widget.children(cell) : widget?.children) ?? + null return ( - + {node} ) diff --git a/ui-tui/src/lib/color.ts b/ui-tui/src/lib/color.ts index 3d7bd080844..d051934cfd8 100644 --- a/ui-tui/src/lib/color.ts +++ b/ui-tui/src/lib/color.ts @@ -46,8 +46,7 @@ export function parseColor(input: string): Rgb | null { return null } -export const toHex = (rgb: Rgb): string => - '#' + rgb.map(c => clampChannel(c).toString(16).padStart(2, '0')).join('') +export const toHex = (rgb: Rgb): string => '#' + rgb.map(c => clampChannel(c).toString(16).padStart(2, '0')).join('') /** sRGB lerp `a → b` by `t` in [0,1]. Unparseable inputs return `a` unchanged. */ export function mix(a: string, b: string, t: number): string { @@ -71,7 +70,9 @@ function channelLuminance(value: number): number { export function relativeLuminance(color: string): null | number { const rgb = parseColor(color) - return rgb ? 0.2126 * channelLuminance(rgb[0]) + 0.7152 * channelLuminance(rgb[1]) + 0.0722 * channelLuminance(rgb[2]) : null + return rgb + ? 0.2126 * channelLuminance(rgb[0]) + 0.7152 * channelLuminance(rgb[1]) + 0.0722 * channelLuminance(rgb[2]) + : null } /** WCAG contrast ratio between two colors (1–21). Null when unparseable. */ diff --git a/ui-tui/src/lib/widgetGrid.ts b/ui-tui/src/lib/widgetGrid.ts index ae891eb857f..ee4fb09edad 100644 --- a/ui-tui/src/lib/widgetGrid.ts +++ b/ui-tui/src/lib/widgetGrid.ts @@ -52,7 +52,11 @@ const columnCountForWidth = (width: number, minColumnWidth: number, gap: number, } const buildColumnWidths = (width: number, columnCount: number, gap: number) => - resolveGridTracks(width, gap, Array.from({ length: Math.max(1, toInt(columnCount, 1)) }, () => ({ fr: 1 }))) + resolveGridTracks( + width, + gap, + Array.from({ length: Math.max(1, toInt(columnCount, 1)) }, () => ({ fr: 1 })) + ) const spanWidth = (columns: number[], colStart: number, span: number, gap: number) => { const end = Math.min(columns.length, colStart + span) @@ -229,7 +233,10 @@ export function resolveGridTracks(total: number, gap: number, tracks: GridTrackS }) while (unpinned.length) { - const shares = distributeByWeight(Math.max(0, remaining), unpinned.map(idx => trackFr(tracks[idx]!))) + const shares = distributeByWeight( + Math.max(0, remaining), + unpinned.map(idx => trackFr(tracks[idx]!)) + ) const violating = unpinned.filter((idx, i) => shares[i]! < trackMin(tracks[idx]!)) if (!violating.length) { @@ -467,10 +474,7 @@ export function layoutGridAreas({ const explicitRowTracks = rows === undefined ? [] : normalizeTracks(rows, 1) const rowCount = Math.max(1, explicitRowTracks.length, placedRowCount) - const rowTracks: GridTrackSize[] = Array.from( - { length: rowCount }, - (_, idx) => explicitRowTracks[idx] ?? { fr: 1 } - ) + const rowTracks: GridTrackSize[] = Array.from({ length: rowCount }, (_, idx) => explicitRowTracks[idx] ?? { fr: 1 }) const columnSizes = resolveGridTracks(safeWidth, safeGap, columnTracks) const rowSizes = resolveGridTracks(safeHeight, safeRowGap, rowTracks) diff --git a/ui-tui/src/theme.ts b/ui-tui/src/theme.ts index 28ce5c0f14d..fa28d8ffd5b 100644 --- a/ui-tui/src/theme.ts +++ b/ui-tui/src/theme.ts @@ -580,8 +580,7 @@ export function deriveTones(seeds: { statusFg: grayOf(mix(text, bg, isLight ? 0.3 : 0.24)), surface, activeRow: mix(surface, accent, 0.25), - selection: - isLight && seeds.shellDollar ? mix(bg, seeds.shellDollar, 0.2) : mix(surface, accent, 0.28), + selection: isLight && seeds.shellDollar ? mix(bg, seeds.shellDollar, 0.2) : mix(surface, accent, 0.28), border: mix(accent, bg, 0.25) } }