mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-28 06:51:16 +08:00
refactor(tui): use semantic theme text colors
Replace decorative/base palette usage in TUI components with semantic theme text tokens and remove hardcoded overlay colors from FPS and heart indicators.
This commit is contained in:
@@ -28,7 +28,7 @@ describe('syntax highlighter', () => {
|
||||
|
||||
expect(colors).toContain(t.color.bronze) // const
|
||||
expect(colors).toContain(t.color.amber) // 'hi'
|
||||
expect(colors).toContain(t.color.cornsilk) // 42
|
||||
expect(colors).toContain(t.color.text) // 42
|
||||
})
|
||||
|
||||
it('falls through unchanged for unknown langs', () => {
|
||||
|
||||
@@ -383,7 +383,7 @@ function Field({ name, t, value }: { name: string; t: Theme; value: ReactNode })
|
||||
return (
|
||||
<Text wrap="truncate-end">
|
||||
<Text color={t.color.label}>{name} · </Text>
|
||||
<Text color={t.color.cornsilk}>{value}</Text>
|
||||
<Text color={t.color.text}>{value}</Text>
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
@@ -411,7 +411,7 @@ function Detail({ id, node, t }: { id?: string; node: SubagentNode; t: Theme })
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text bold color={t.color.cornsilk} wrap="wrap">
|
||||
<Text bold color={t.color.text} wrap="wrap">
|
||||
{id ? <Text color={t.color.amber}>#{id} </Text> : null}
|
||||
<Text color={color}>{glyph}</Text> {item.goal}
|
||||
</Text>
|
||||
@@ -472,7 +472,7 @@ function Detail({ id, node, t }: { id?: string; node: SubagentNode; t: Theme })
|
||||
))}
|
||||
|
||||
{filesRead.slice(0, 8).map((p, i) => (
|
||||
<Text color={t.color.cornsilk} key={`r-${i}`} wrap="truncate-end">
|
||||
<Text color={t.color.text} key={`r-${i}`} wrap="truncate-end">
|
||||
<Text color={t.color.dim}>·</Text> {p}
|
||||
</Text>
|
||||
))}
|
||||
@@ -484,7 +484,7 @@ function Detail({ id, node, t }: { id?: string; node: SubagentNode; t: Theme })
|
||||
{toolLines.length > 0 ? (
|
||||
<OverlaySection count={toolLines.length} defaultOpen t={t} title="Tool calls">
|
||||
{toolLines.map((line, i) => (
|
||||
<Text color={t.color.cornsilk} key={i} wrap="wrap">
|
||||
<Text color={t.color.text} key={i} wrap="wrap">
|
||||
<Text color={t.color.dim}>·</Text> {line}
|
||||
</Text>
|
||||
))}
|
||||
@@ -494,7 +494,7 @@ function Detail({ id, node, t }: { id?: string; node: SubagentNode; t: Theme })
|
||||
{outputTail.length > 0 ? (
|
||||
<OverlaySection count={outputTail.length} defaultOpen t={t} title="Output">
|
||||
{outputTail.map((entry, i) => (
|
||||
<Text color={entry.isError ? t.color.error : t.color.cornsilk} key={i} wrap="wrap">
|
||||
<Text color={entry.isError ? t.color.error : t.color.text} key={i} wrap="wrap">
|
||||
<Text bold color={entry.isError ? t.color.error : t.color.amber}>
|
||||
{entry.tool}
|
||||
</Text>{' '}
|
||||
@@ -507,7 +507,7 @@ function Detail({ id, node, t }: { id?: string; node: SubagentNode; t: Theme })
|
||||
{item.notes.length ? (
|
||||
<OverlaySection count={item.notes.length} t={t} title="Progress">
|
||||
{item.notes.slice(-6).map((line, i) => (
|
||||
<Text color={t.color.cornsilk} key={i} wrap="wrap">
|
||||
<Text color={t.color.text} key={i} wrap="wrap">
|
||||
<Text color={t.color.label}>·</Text> {line}
|
||||
</Text>
|
||||
))}
|
||||
@@ -516,7 +516,7 @@ function Detail({ id, node, t }: { id?: string; node: SubagentNode; t: Theme })
|
||||
|
||||
{item.summary ? (
|
||||
<OverlaySection defaultOpen t={t} title="Summary">
|
||||
<Text color={t.color.cornsilk} wrap="wrap">
|
||||
<Text color={t.color.text} wrap="wrap">
|
||||
{item.summary}
|
||||
</Text>
|
||||
</OverlaySection>
|
||||
@@ -552,7 +552,7 @@ function ListRow({
|
||||
const paren = line ? line.indexOf('(') : -1
|
||||
const toolShort = line ? (paren > 0 ? line.slice(0, paren) : line).trim() : ''
|
||||
const trailing = toolShort ? ` · ${compactPreview(toolShort, 14)}` : ''
|
||||
const fg = active ? t.color.amber : t.color.cornsilk
|
||||
const fg = active ? t.color.amber : t.color.text
|
||||
|
||||
return (
|
||||
<Text bold={active} color={fg} inverse={active} wrap="truncate-end">
|
||||
@@ -585,7 +585,7 @@ function DiffPane({
|
||||
}) {
|
||||
return (
|
||||
<Box flexDirection="column" width={width}>
|
||||
<Text bold color={t.color.cornsilk}>
|
||||
<Text bold color={t.color.text}>
|
||||
{label}
|
||||
</Text>
|
||||
|
||||
@@ -661,20 +661,20 @@ function DiffView({
|
||||
Δ
|
||||
</Text>
|
||||
|
||||
<Text color={t.color.cornsilk}>
|
||||
<Text color={t.color.text}>
|
||||
{diffMetricLine('agents', aTotals.descendantCount, bTotals.descendantCount, round)}
|
||||
</Text>
|
||||
<Text color={t.color.cornsilk}>{diffMetricLine('tools', aTotals.totalTools, bTotals.totalTools, round)}</Text>
|
||||
<Text color={t.color.cornsilk}>
|
||||
<Text color={t.color.text}>{diffMetricLine('tools', aTotals.totalTools, bTotals.totalTools, round)}</Text>
|
||||
<Text color={t.color.text}>
|
||||
{diffMetricLine('depth', aTotals.maxDepthFromHere, bTotals.maxDepthFromHere, round)}
|
||||
</Text>
|
||||
<Text color={t.color.cornsilk}>
|
||||
<Text color={t.color.text}>
|
||||
{diffMetricLine('duration', aTotals.totalDuration, bTotals.totalDuration, n => `${n.toFixed(1)}s`)}
|
||||
</Text>
|
||||
<Text color={t.color.cornsilk}>
|
||||
<Text color={t.color.text}>
|
||||
{diffMetricLine('tokens', sumTokens(aTotals), sumTokens(bTotals), fmtTokens)}
|
||||
</Text>
|
||||
<Text color={t.color.cornsilk}>{diffMetricLine('cost', aTotals.costUsd, bTotals.costUsd, dollars)}</Text>
|
||||
<Text color={t.color.text}>{diffMetricLine('cost', aTotals.costUsd, bTotals.costUsd, dollars)}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
|
||||
@@ -15,8 +15,6 @@ import type { Theme } from '../theme.js'
|
||||
import type { Msg, Usage } from '../types.js'
|
||||
|
||||
const FACE_TICK_MS = 2500
|
||||
const HEART_COLORS = ['#ff5fa2', '#ff4d6d']
|
||||
|
||||
function FaceTicker({ color, startedAt }: { color: string; startedAt?: null | number }) {
|
||||
const [tick, setTick] = useState(() => Math.floor(Math.random() * 1000))
|
||||
const [now, setNow] = useState(() => Date.now())
|
||||
@@ -169,7 +167,7 @@ export function GoodVibesHeart({ tick, t }: { tick: number; t: Theme }) {
|
||||
return
|
||||
}
|
||||
|
||||
const palette = [...HEART_COLORS, t.color.amber]
|
||||
const palette = [t.color.error, t.color.warn, t.color.amber]
|
||||
setColor(palette[Math.floor(Math.random() * palette.length)]!)
|
||||
setActive(true)
|
||||
|
||||
|
||||
@@ -162,7 +162,7 @@ const ComposerPane = memo(function ComposerPane({
|
||||
<Text color={ui.theme.color.dim}>{i === 0 ? `${ui.theme.brand.prompt} ` : ' '}</Text>
|
||||
</Box>
|
||||
|
||||
<Text color={ui.theme.color.cornsilk}>{line || ' '}</Text>
|
||||
<Text color={ui.theme.color.text}>{line || ' '}</Text>
|
||||
</Box>
|
||||
))}
|
||||
|
||||
@@ -262,6 +262,7 @@ export const AppLayout = memo(function AppLayout({
|
||||
transcript
|
||||
}: AppLayoutProps) {
|
||||
const overlay = useStore($overlayState)
|
||||
const ui = useStore($uiState)
|
||||
|
||||
// Inline mode skips AlternateScreen so the host terminal's native
|
||||
// scrollback captures rows scrolled off the top; composer + progress
|
||||
@@ -302,7 +303,7 @@ export const AppLayout = memo(function AppLayout({
|
||||
|
||||
{SHOW_FPS && (
|
||||
<Box flexShrink={0} justifyContent="flex-end" paddingRight={1}>
|
||||
<FpsOverlay />
|
||||
<FpsOverlay t={ui.theme} />
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -77,7 +77,7 @@ export function SessionPanel({ info, sid, t }: SessionPanelProps) {
|
||||
{shown.map(([k, vs]) => (
|
||||
<Text key={k} wrap="truncate">
|
||||
<Text color={t.color.dim}>{strip(k)}: </Text>
|
||||
<Text color={t.color.cornsilk}>{truncLine(strip(k) + ': ', vs)}</Text>
|
||||
<Text color={t.color.text}>{truncLine(strip(k) + ': ', vs)}</Text>
|
||||
</Text>
|
||||
))}
|
||||
|
||||
@@ -149,7 +149,7 @@ export function SessionPanel({ info, sid, t }: SessionPanelProps) {
|
||||
<Text color={t.color.dim}>{`[${s.transport}]`}</Text>
|
||||
<Text color={t.color.dim}>: </Text>
|
||||
{s.connected ? (
|
||||
<Text color={t.color.cornsilk}>
|
||||
<Text color={t.color.text}>
|
||||
{s.tools} tool{s.tools === 1 ? '' : 's'}
|
||||
</Text>
|
||||
) : (
|
||||
@@ -162,7 +162,7 @@ export function SessionPanel({ info, sid, t }: SessionPanelProps) {
|
||||
|
||||
<Text />
|
||||
|
||||
<Text color={t.color.cornsilk}>
|
||||
<Text color={t.color.text}>
|
||||
{flat(info.tools).length} tools{' · '}
|
||||
{flat(info.skills).length} skills
|
||||
{info.mcp_servers?.length ? ` · ${info.mcp_servers.length} MCP` : ''}
|
||||
@@ -171,7 +171,7 @@ export function SessionPanel({ info, sid, t }: SessionPanelProps) {
|
||||
</Text>
|
||||
|
||||
{learningLine && (
|
||||
<Text color={t.color.cornsilk} dimColor italic>
|
||||
<Text color={t.color.text} dimColor italic>
|
||||
{learningLine} · /learned
|
||||
</Text>
|
||||
)}
|
||||
@@ -217,12 +217,12 @@ export function Panel({ sections, t, title }: PanelProps) {
|
||||
{sec.rows?.map(([k, v], ri) => (
|
||||
<Text key={ri} wrap="truncate">
|
||||
<Text color={t.color.dim}>{k.padEnd(20)}</Text>
|
||||
<Text color={t.color.cornsilk}>{v}</Text>
|
||||
<Text color={t.color.text}>{v}</Text>
|
||||
</Text>
|
||||
))}
|
||||
|
||||
{sec.items?.map((item, ii) => (
|
||||
<Text color={t.color.cornsilk} key={ii} wrap="truncate">
|
||||
<Text color={t.color.text} key={ii} wrap="truncate">
|
||||
{item}
|
||||
</Text>
|
||||
))}
|
||||
|
||||
@@ -5,23 +5,25 @@ import { useStore } from '@nanostores/react'
|
||||
|
||||
import { SHOW_FPS } from '../config/env.js'
|
||||
import { $fpsState } from '../lib/fpsStore.js'
|
||||
import type { Theme } from '../theme.js'
|
||||
|
||||
const fpsColor = (fps: number) => (fps >= 50 ? 'green' : fps >= 30 ? 'yellow' : 'red')
|
||||
const fpsColor = (fps: number, t: Theme) =>
|
||||
fps >= 50 ? t.color.statusGood : fps >= 30 ? t.color.statusWarn : t.color.error
|
||||
|
||||
export function FpsOverlay() {
|
||||
export function FpsOverlay({ t }: { t: Theme }) {
|
||||
if (!SHOW_FPS) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <FpsOverlayInner />
|
||||
return <FpsOverlayInner t={t} />
|
||||
}
|
||||
|
||||
function FpsOverlayInner() {
|
||||
function FpsOverlayInner({ t }: { t: Theme }) {
|
||||
const { fps, lastDurationMs, totalFrames } = useStore($fpsState)
|
||||
|
||||
// Zero-pad widths so digit churn doesn't jitter the corner.
|
||||
return (
|
||||
<Text color={fpsColor(fps)}>
|
||||
<Text color={fpsColor(fps, t)}>
|
||||
{fps.toFixed(1).padStart(5)}fps · {lastDurationMs.toFixed(1).padStart(5)}ms · #{totalFrames}
|
||||
</Text>
|
||||
)
|
||||
|
||||
@@ -213,7 +213,7 @@ function LedgerDetails({ item, t, width }: LedgerDetailsProps) {
|
||||
<Text color={t.color.gold} wrap="truncate-end">
|
||||
{memoryLike ? item.name : item.summary}
|
||||
</Text>
|
||||
{memoryLike ? <Text color={t.color.cornsilk}>{item.summary}</Text> : null}
|
||||
{memoryLike ? <Text color={t.color.text}>{item.summary}</Text> : null}
|
||||
{item.count ? <Text color={t.color.dim}>used: {item.count}×</Text> : null}
|
||||
{item.learned_from ? <Text color={t.color.dim}>from: {item.learned_from}</Text> : null}
|
||||
{item.via ? <Text color={t.color.dim}>via: {item.via}</Text> : null}
|
||||
|
||||
@@ -48,7 +48,7 @@ export function ApprovalPrompt({ onChoice, req, t }: ApprovalPromptProps) {
|
||||
|
||||
<Box flexDirection="column" paddingLeft={1}>
|
||||
{shown.map((line, i) => (
|
||||
<Text color={t.color.cornsilk} key={i} wrap="truncate-end">
|
||||
<Text color={t.color.text} key={i} wrap="truncate-end">
|
||||
{line || ' '}
|
||||
</Text>
|
||||
))}
|
||||
@@ -85,7 +85,7 @@ export function ClarifyPrompt({ cols = 80, onAnswer, onCancel, req, t }: Clarify
|
||||
const heading = (
|
||||
<Text bold>
|
||||
<Text color={t.color.amber}>ask</Text>
|
||||
<Text color={t.color.cornsilk}> {req.question}</Text>
|
||||
<Text color={t.color.text}> {req.question}</Text>
|
||||
</Text>
|
||||
)
|
||||
|
||||
@@ -185,8 +185,8 @@ export function ConfirmPrompt({ onCancel, onConfirm, req, t }: ConfirmPromptProp
|
||||
const accent = req.danger ? t.color.error : t.color.warn
|
||||
|
||||
const rows = [
|
||||
{ color: t.color.cornsilk, label: req.cancelLabel ?? 'No' },
|
||||
{ color: req.danger ? t.color.error : t.color.cornsilk, label: req.confirmLabel ?? 'Yes' }
|
||||
{ color: t.color.text, label: req.cancelLabel ?? 'No' },
|
||||
{ color: req.danger ? t.color.error : t.color.text, label: req.confirmLabel ?? 'Yes' }
|
||||
]
|
||||
|
||||
return (
|
||||
@@ -197,7 +197,7 @@ export function ConfirmPrompt({ onCancel, onConfirm, req, t }: ConfirmPromptProp
|
||||
|
||||
{req.detail ? (
|
||||
<Box paddingLeft={1}>
|
||||
<Text color={t.color.cornsilk} wrap="truncate-end">
|
||||
<Text color={t.color.text} wrap="truncate-end">
|
||||
{req.detail}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
@@ -283,7 +283,7 @@ export function SkillsHub({ gw, onClose, t }: SkillsHubProps) {
|
||||
</Text>
|
||||
|
||||
<Text color={t.color.dim}>{info?.category ?? selectedCat}</Text>
|
||||
{info?.description ? <Text color={t.color.cornsilk}>{info.description}</Text> : null}
|
||||
{info?.description ? <Text color={t.color.text}>{info.description}</Text> : null}
|
||||
{info?.path ? <Text color={t.color.dim}>path: {info.path}</Text> : null}
|
||||
{!info && !err ? <Text color={t.color.dim}>loading…</Text> : null}
|
||||
{err ? <Text color={t.color.label}>error: {err}</Text> : null}
|
||||
|
||||
@@ -460,7 +460,7 @@ function SubagentAccordion({
|
||||
{item.tools.map((line, index) => (
|
||||
<TreeTextRow
|
||||
branch={index === item.tools.length - 1 ? 'last' : 'mid'}
|
||||
color={t.color.cornsilk}
|
||||
color={t.color.text}
|
||||
content={
|
||||
<>
|
||||
<Text color={t.color.amber}>● </Text>
|
||||
@@ -792,7 +792,7 @@ export const ToolTrail = memo(function ToolTrail({
|
||||
|
||||
if (parsed) {
|
||||
groups.push({
|
||||
color: parsed.mark === '✗' ? t.color.error : t.color.cornsilk,
|
||||
color: parsed.mark === '✗' ? t.color.error : t.color.text,
|
||||
content: parsed.call,
|
||||
details: [],
|
||||
key: `tr-${i}`,
|
||||
@@ -815,7 +815,7 @@ export const ToolTrail = memo(function ToolTrail({
|
||||
const label = toolTrailLabel(line.slice(9).replace(/…$/, '').trim())
|
||||
|
||||
groups.push({
|
||||
color: t.color.cornsilk,
|
||||
color: t.color.text,
|
||||
content: label,
|
||||
details: [{ color: t.color.dim, content: 'drafting...', dimColor: true, key: `tr-${i}-d` }],
|
||||
key: `tr-${i}`,
|
||||
@@ -849,7 +849,7 @@ export const ToolTrail = memo(function ToolTrail({
|
||||
const label = formatToolCall(tool.name, tool.context || '')
|
||||
|
||||
groups.push({
|
||||
color: t.color.cornsilk,
|
||||
color: t.color.text,
|
||||
key: tool.id,
|
||||
label,
|
||||
details: [],
|
||||
@@ -1001,7 +1001,7 @@ export const ToolTrail = memo(function ToolTrail({
|
||||
<Text color={t.color.dim} dim={!thinkingLive}>
|
||||
<Text color={t.color.amber}>{openThinking ? '▾ ' : '▸ '}</Text>
|
||||
{thinkingLive ? (
|
||||
<Text bold color={t.color.cornsilk}>
|
||||
<Text bold color={t.color.text}>
|
||||
Thinking
|
||||
</Text>
|
||||
) : (
|
||||
|
||||
@@ -9,7 +9,7 @@ import type { TodoItem } from '../types.js'
|
||||
const rowColor = (t: Theme, status: TodoItem['status']) => {
|
||||
const tone = todoTone(status)
|
||||
|
||||
return tone === 'active' ? t.color.cornsilk : tone === 'body' ? t.color.statusFg : t.color.dim
|
||||
return tone === 'active' ? t.color.text : tone === 'body' ? t.color.statusFg : t.color.dim
|
||||
}
|
||||
|
||||
export const TodoPanel = memo(function TodoPanel({
|
||||
@@ -58,7 +58,7 @@ export const TodoPanel = memo(function TodoPanel({
|
||||
<Box onClick={handleToggle}>
|
||||
<Text color={t.color.dim}>
|
||||
<Text color={t.color.amber}>{effectiveCollapsed ? '▸ ' : '▾ '}</Text>
|
||||
<Text bold color={t.color.cornsilk}>
|
||||
<Text bold color={t.color.text}>
|
||||
Todo
|
||||
</Text>{' '}
|
||||
<Text color={t.color.statusFg} dim>
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Theme } from '../theme.js'
|
||||
import type { Role } from '../types.js'
|
||||
|
||||
export const ROLE: Record<Role, (t: Theme) => { body: string; glyph: string; prefix: string }> = {
|
||||
assistant: t => ({ body: t.color.cornsilk, glyph: t.brand.tool, prefix: t.color.bronze }),
|
||||
assistant: t => ({ body: t.color.text, glyph: t.brand.tool, prefix: t.color.bronze }),
|
||||
system: t => ({ body: '', glyph: '·', prefix: t.color.dim }),
|
||||
tool: t => ({ body: t.color.dim, glyph: '⚡', prefix: t.color.dim }),
|
||||
user: t => ({ body: t.color.label, glyph: t.brand.prompt, prefix: t.color.label })
|
||||
|
||||
@@ -99,7 +99,7 @@ export function highlightLine(line: string, lang: string, t: Theme): Token[] {
|
||||
if (ch === '"' || ch === "'" || ch === '`') {
|
||||
tokens.push([t.color.amber, tok])
|
||||
} else if (ch >= '0' && ch <= '9') {
|
||||
tokens.push([t.color.cornsilk, tok])
|
||||
tokens.push([t.color.text, tok])
|
||||
} else if (spec.keywords.has(tok)) {
|
||||
tokens.push([t.color.bronze, tok])
|
||||
} else {
|
||||
|
||||
@@ -2,7 +2,7 @@ export interface ThemeColors {
|
||||
gold: string
|
||||
amber: string
|
||||
bronze: string
|
||||
cornsilk: string
|
||||
text: string
|
||||
dim: string
|
||||
completionBg: string
|
||||
completionCurrentBg: string
|
||||
@@ -93,7 +93,7 @@ export const DARK_THEME: Theme = {
|
||||
gold: '#FFD700',
|
||||
amber: '#FFBF00',
|
||||
bronze: '#CD7F32',
|
||||
cornsilk: '#FFF8DC',
|
||||
text: '#FFF8DC',
|
||||
// Bumped from the old `#B8860B` darkgoldenrod (~53% luminance) which
|
||||
// read as barely-visible on dark terminals for long body text. The
|
||||
// new value sits ~60% luminance — readable without losing the "muted /
|
||||
@@ -144,7 +144,7 @@ export const LIGHT_THEME: Theme = {
|
||||
gold: '#8B6914',
|
||||
amber: '#A0651C',
|
||||
bronze: '#7A4F1F',
|
||||
cornsilk: '#3D2F13',
|
||||
text: '#3D2F13',
|
||||
dim: '#7A5A0F',
|
||||
completionBg: '#F5F5F5',
|
||||
completionCurrentBg: mix('#F5F5F5', '#A0651C', 0.25),
|
||||
@@ -222,10 +222,10 @@ export function fromSkin(
|
||||
gold: c('banner_title') ?? d.color.gold,
|
||||
amber,
|
||||
bronze: c('banner_border') ?? d.color.bronze,
|
||||
cornsilk: c('banner_text') ?? d.color.cornsilk,
|
||||
text: c('ui_text') ?? c('banner_text') ?? d.color.text,
|
||||
dim,
|
||||
completionBg: c('completion_menu_bg') ?? '#FFFFFF',
|
||||
completionCurrentBg: c('completion_menu_current_bg') ?? mix('#FFFFFF', accent, 0.25),
|
||||
completionBg: c('completion_menu_bg') ?? d.color.completionBg,
|
||||
completionCurrentBg: c('completion_menu_current_bg') ?? mix(d.color.completionBg, accent, 0.25),
|
||||
|
||||
label: c('ui_label') ?? d.color.label,
|
||||
ok: c('ui_ok') ?? d.color.ok,
|
||||
|
||||
Reference in New Issue
Block a user