Merge remote-tracking branch 'origin/main' into fix/markdown

Made-with: Cursor

# Conflicts:
#	ui-tui/src/components/markdown.tsx
This commit is contained in:
Austin Pickett 2026-04-28 22:01:02 -04:00
commit e4120d1e6d
82 changed files with 3565 additions and 491 deletions

View file

@ -79,15 +79,15 @@ const FILTER_PREDICATES: Record<FilterMode, (n: SubagentNode) => boolean> = {
}
const STATUS_GLYPH: Record<Status, { color: (t: Theme) => string; glyph: string }> = {
running: { color: t => t.color.amber, glyph: '●' },
queued: { color: t => t.color.dim, glyph: '○' },
running: { color: t => t.color.accent, glyph: '●' },
queued: { color: t => t.color.muted, glyph: '○' },
completed: { color: t => t.color.statusGood, glyph: '✓' },
interrupted: { color: t => t.color.warn, glyph: '■' },
failed: { color: t => t.color.error, glyph: '✗' }
}
// Heatmap palette — cold → hot, resolved against the active theme.
const heatPalette = (t: Theme) => [t.color.bronze, t.color.amber, t.color.gold, t.color.warn, t.color.error]
const heatPalette = (t: Theme) => [t.color.border, t.color.accent, t.color.primary, t.color.warn, t.color.error]
// ── Pure helpers ─────────────────────────────────────────────────────
@ -160,8 +160,8 @@ function OverlayScrollbar({
const vBar = (n: number) => (n > 0 ? `${'│\n'.repeat(n - 1)}` : '')
const thumbBody = `${'┃\n'.repeat(Math.max(0, thumb - 1))}`
const thumbColor = grab !== null ? t.color.gold : t.color.amber
const trackColor = hover ? t.color.bronze : t.color.dim
const thumbColor = grab !== null ? t.color.primary : t.color.accent
const trackColor = hover ? t.color.border : t.color.muted
const jump = (row: number, offset: number) => {
if (!s || !scrollable) {
@ -301,7 +301,7 @@ function GanttStrip({
return (
<Box flexDirection="column" marginBottom={1}>
<Text color={t.color.dim}>
<Text color={t.color.muted}>
Timeline · {fmtElapsedLabel(Math.max(0, totalSeconds))}
{windowLabel}
</Text>
@ -309,7 +309,7 @@ function GanttStrip({
{shown.map(({ endAt, idx, node, startAt }) => {
const active = idx === cursor
const { color } = statusGlyph(node.item, t)
const accent = active ? t.color.amber : t.color.dim
const accent = active ? t.color.accent : t.color.muted
const elSec = displayElapsedSeconds(node.item, now)
const elLabel = elSec != null ? fmtElapsedLabel(elSec) : ''
@ -321,7 +321,7 @@ function GanttStrip({
{' '}
</Text>
<Text color={active ? t.color.amber : color}>{bar(startAt, endAt)}</Text>
<Text color={active ? t.color.accent : color}>{bar(startAt, endAt)}</Text>
{elLabel ? (
<Text color={accent}>
@ -333,13 +333,13 @@ function GanttStrip({
)
})}
<Text color={t.color.dim} dim>
<Text color={t.color.muted} dim>
{' '}
{ruler}
</Text>
{totalSeconds > 0 ? (
<Text color={t.color.dim} dim>
<Text color={t.color.muted} dim>
{' '}
{rulerLabels}
</Text>
@ -368,7 +368,7 @@ function OverlaySection({
<Box flexDirection="column" marginTop={1}>
<Box onClick={() => toggleOverlaySection(title, defaultOpen)}>
<Text color={t.color.label}>
<Text color={t.color.amber}>{open ? '▾ ' : '▸ '}</Text>
<Text color={t.color.accent}>{open ? '▾ ' : '▸ '}</Text>
{title}
{typeof count === 'number' ? ` (${count})` : ''}
</Text>
@ -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,8 +411,8 @@ function Detail({ id, node, t }: { id?: string; node: SubagentNode; t: Theme })
return (
<Box flexDirection="column">
<Text bold color={t.color.cornsilk} wrap="wrap">
{id ? <Text color={t.color.amber}>#{id} </Text> : null}
<Text bold color={t.color.text} wrap="wrap">
{id ? <Text color={t.color.accent}>#{id} </Text> : null}
<Text color={color}>{glyph}</Text> {item.goal}
</Text>
@ -472,20 +472,20 @@ 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.dim}>·</Text> {p}
<Text color={t.color.text} key={`r-${i}`} wrap="truncate-end">
<Text color={t.color.muted}>·</Text> {p}
</Text>
))}
{filesOverflow > 0 ? <Text color={t.color.dim}>+{filesOverflow} more</Text> : null}
{filesOverflow > 0 ? <Text color={t.color.muted}>+{filesOverflow} more</Text> : null}
</OverlaySection>
) : null}
{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.dim}>·</Text> {line}
<Text color={t.color.text} key={i} wrap="wrap">
<Text color={t.color.muted}>·</Text> {line}
</Text>
))}
</OverlaySection>
@ -494,8 +494,8 @@ 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 bold color={entry.isError ? t.color.error : t.color.amber}>
<Text color={entry.isError ? t.color.error : t.color.text} key={i} wrap="wrap">
<Text bold color={entry.isError ? t.color.error : t.color.accent}>
{entry.tool}
</Text>{' '}
{entry.preview}
@ -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,16 +552,16 @@ 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.accent : t.color.text
return (
<Text bold={active} color={fg} inverse={active} wrap="truncate-end">
{' '}
<Text color={active ? fg : t.color.dim}>{formatRowId(index)} </Text>
<Text color={active ? fg : t.color.muted}>{formatRowId(index)} </Text>
{indentFor(node.item.depth)}
{heatMarker ? <Text color={heatMarker}></Text> : null}
<Text color={active ? fg : color}>{glyph}</Text> {goal}
<Text color={active ? fg : t.color.dim}>
<Text color={active ? fg : t.color.muted}>
{toolsCount}
{kids}
{trailing}
@ -585,16 +585,16 @@ function DiffPane({
}) {
return (
<Box flexDirection="column" width={width}>
<Text bold color={t.color.cornsilk}>
<Text bold color={t.color.text}>
{label}
</Text>
<Text color={t.color.dim} wrap="truncate-end">
<Text color={t.color.muted} wrap="truncate-end">
{snapshot.label}
</Text>
<Box marginTop={1}>
<Text color={t.color.dim} wrap="truncate-end">
<Text color={t.color.muted} wrap="truncate-end">
{formatSummary(totals)}
</Text>
</Box>
@ -606,7 +606,7 @@ function DiffPane({
const { color, glyph } = statusGlyph(s, t)
return (
<Text color={t.color.dim} key={s.id} wrap="truncate-end">
<Text color={t.color.muted} key={s.id} wrap="truncate-end">
<Text color={color}>{glyph}</Text> {s.goal || 'subagent'}
</Text>
)
@ -644,10 +644,10 @@ function DiffView({
return (
<Box flexDirection="column" flexGrow={1} paddingX={1} paddingY={1}>
<Box flexDirection="column" marginBottom={1}>
<Text bold color={t.color.bronze}>
<Text bold color={t.color.border}>
Replay diff
</Text>
<Text color={t.color.dim}>baseline vs candidate · esc/q close</Text>
<Text color={t.color.muted}>baseline vs candidate · esc/q close</Text>
</Box>
<Box flexDirection="row" marginBottom={1}>
@ -657,24 +657,24 @@ function DiffView({
</Box>
<Box flexDirection="column" marginTop={1}>
<Text bold color={t.color.amber}>
<Text bold color={t.color.accent}>
Δ
</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>
)
@ -985,11 +985,11 @@ export function AgentsOverlay({ gw, initialHistoryIndex = 0, onClose, t }: Agent
<Box alignItems="stretch" flexDirection="column" flexGrow={1} paddingX={1} paddingY={1}>
<Box flexDirection="column" marginBottom={1}>
<Text wrap="truncate-end">
<Text bold color={replayMode ? t.color.bronze : t.color.gold}>
<Text bold color={replayMode ? t.color.border : t.color.primary}>
{title}
</Text>
{metaLine ? (
<Text color={t.color.dim}>
<Text color={t.color.muted}>
{' '}
{metaLine}
</Text>
@ -999,7 +999,7 @@ export function AgentsOverlay({ gw, initialHistoryIndex = 0, onClose, t }: Agent
{rows.length === 0 ? (
<Box flexDirection="column" flexGrow={1}>
<Text color={t.color.dim}>No subagents this turn. Trigger delegate_task to populate the tree.</Text>
<Text color={t.color.muted}>No subagents this turn. Trigger delegate_task to populate the tree.</Text>
</Box>
) : mode === 'list' ? (
<Box flexDirection="column" flexGrow={1} flexShrink={1} minHeight={0}>
@ -1034,17 +1034,17 @@ export function AgentsOverlay({ gw, initialHistoryIndex = 0, onClose, t }: Agent
)}
<Box flexDirection="column" marginTop={1}>
{flash ? <Text color={t.color.amber}>{flash}</Text> : null}
{flash ? <Text color={t.color.accent}>{flash}</Text> : null}
{mode === 'list' ? (
<Text color={t.color.dim}>
<Text color={t.color.muted}>
/jk move · g/G top/bottom · Enter/ open detail{controlsHint} · s sort:{SORT_LABEL[sort]} · f filter:
{FILTER_LABEL[filter]}
{history.length > 0 ? ` · [ / ] history ${historyIndex}/${history.length}` : ''}
{' · q close'}
</Text>
) : (
<Text color={t.color.dim}>
<Text color={t.color.muted}>
/jk scroll · PgUp/PgDn page · g/G top/bottom · Esc/ back to list{controlsHint} · q close
</Text>
)}

View file

@ -1,8 +1,11 @@
import { Box, type ScrollBoxHandle, Text } from '@hermes/ink'
import { useStore } from '@nanostores/react'
import { type ReactNode, type RefObject, useEffect, useMemo, useState } from 'react'
import unicodeSpinners from 'unicode-animations'
import { $delegationState } from '../app/delegationStore.js'
import type { IndicatorStyle } from '../app/interfaces.js'
import { $uiState } from '../app/uiStore.js'
import { useTurnSelector } from '../app/turnStore.js'
import { FACES } from '../content/faces.js'
import { VERBS } from '../content/verbs.js'
@ -17,30 +20,103 @@ import type { Msg, Usage } from '../types.js'
const FACE_TICK_MS = 2500
const HEART_COLORS = ['#ff5fa2', '#ff4d6d']
// Compact alternates for the `emoji` and `ascii` indicator styles.
// Each entry is a fixed-width (display-width) glyph.
const EMOJI_FRAMES = ['⚕ ', '🌀', '🤔', '✨', '🍵', '🔮']
const ASCII_FRAMES = ['|', '/', '-', '\\']
// Faster tick for spinner-style indicators — they read as motion only
// at frame rates closer to their authored interval.
const SPINNER_TICK_MS = 100
interface IndicatorRender {
frame: string
intervalMs: number
// When false, FaceTicker hides the rotating verb and just shows the
// glyph + duration. Lets `unicode` stay minimal while the other
// styles keep the verb-rotation flavour users associate with the
// running… status.
showVerb: boolean
}
const renderIndicator = (style: IndicatorStyle, tick: number): IndicatorRender => {
if (style === 'kaomoji') {
return { frame: FACES[tick % FACES.length] ?? '', intervalMs: FACE_TICK_MS, showVerb: true }
}
if (style === 'emoji') {
return {
frame: EMOJI_FRAMES[tick % EMOJI_FRAMES.length] ?? '⚕ ',
intervalMs: SPINNER_TICK_MS * 6,
showVerb: true
}
}
if (style === 'ascii') {
return {
frame: ASCII_FRAMES[tick % ASCII_FRAMES.length] ?? '|',
intervalMs: SPINNER_TICK_MS,
showVerb: true
}
}
// 'unicode' — braille spinner (fixed 1-col). Authored interval is
// ~80ms; honour it but bound below at a safe minimum so React
// re-renders stay reasonable. This style is for users who want
// the cleanest possible status, so no verb rotation either.
const spinner = unicodeSpinners.braille
const frame = spinner.frames[tick % spinner.frames.length] ?? '⠋'
return { frame, intervalMs: Math.max(SPINNER_TICK_MS, spinner.interval), showVerb: false }
}
function FaceTicker({ color, startedAt }: { color: string; startedAt?: null | number }) {
const ui = useStore($uiState)
const style = ui.indicatorStyle
const [tick, setTick] = useState(() => Math.floor(Math.random() * 1000))
const [verbTick, setVerbTick] = useState(() => Math.floor(Math.random() * VERBS.length))
const [now, setNow] = useState(() => Date.now())
// Pre-compute cadence + verb-visibility for the active style so an
// `/indicator` switch re-arms the interval (and skips the verb timer
// for verb-less styles like `unicode`) without leaving the previous
// timer dangling.
const { intervalMs, showVerb } = renderIndicator(style, 0)
useEffect(() => {
const face = setInterval(() => setTick(n => n + 1), FACE_TICK_MS)
const glyph = setInterval(() => setTick(n => n + 1), intervalMs)
const clock = setInterval(() => setNow(Date.now()), 1000)
// Verb timer is gated on `showVerb` — `unicode` style hides the verb
// entirely, so cycling `verbTick` would be an avoidable re-render.
const verb = showVerb ? setInterval(() => setVerbTick(n => n + 1), FACE_TICK_MS) : null
return () => {
clearInterval(face)
clearInterval(glyph)
clearInterval(clock)
if (verb !== null) {
clearInterval(verb)
}
}
}, [])
}, [intervalMs, showVerb])
const { frame } = renderIndicator(style, tick)
const verb = VERBS[verbTick % VERBS.length] ?? ''
const verbSegment = showVerb ? ` ${verb}` : ''
const durationSegment = startedAt ? ` · ${fmtDuration(now - startedAt)}` : ''
return (
<Text color={color}>
{FACES[tick % FACES.length]} {VERBS[tick % VERBS.length]}{startedAt ? ` · ${fmtDuration(now - startedAt)}` : ''}
{frame}
{verbSegment}
{durationSegment}
</Text>
)
}
function ctxBarColor(pct: number | undefined, t: Theme) {
if (pct == null) {
return t.color.dim
return t.color.muted
}
if (pct >= 95) {
@ -93,7 +169,7 @@ function SpawnHud({ t }: { t: Theme }) {
const concRatio = maxConc ? widestLevel / maxConc : 0
const ratio = Math.max(depthRatio, concRatio)
const color = delegation.paused || ratio >= 1 ? t.color.error : ratio >= 0.66 ? t.color.warn : t.color.dim
const color = delegation.paused || ratio >= 1 ? t.color.error : ratio >= 0.66 ? t.color.warn : t.color.muted
const pieces: string[] = []
@ -162,21 +238,21 @@ const modelLabel = (model: string, effort?: string, fast?: boolean) =>
export function GoodVibesHeart({ tick, t }: { tick: number; t: Theme }) {
const [active, setActive] = useState(false)
const [color, setColor] = useState(t.color.amber)
const [color, setColor] = useState(t.color.accent)
useEffect(() => {
if (tick <= 0) {
return
}
const palette = [...HEART_COLORS, t.color.amber]
const palette = [t.color.error, t.color.warn, t.color.accent]
setColor(palette[Math.floor(Math.random() * palette.length)]!)
setActive(true)
const id = setTimeout(() => setActive(false), 650)
return () => clearTimeout(id)
}, [t.color.amber, tick])
}, [t.color.accent, tick])
if (!active) {
return null
@ -217,23 +293,23 @@ export function StatusRule({
return (
<Box height={1}>
<Box flexShrink={1} width={leftWidth}>
<Text color={t.color.bronze} wrap="truncate-end">
<Text color={t.color.border} wrap="truncate-end">
{'─ '}
{busy ? (
<FaceTicker color={statusColor} startedAt={turnStartedAt} />
) : (
<Text color={statusColor}>{status}</Text>
)}
<Text color={t.color.dim}> {modelLabel(model, modelReasoningEffort, modelFast)}</Text>
{ctxLabel ? <Text color={t.color.dim}> {ctxLabel}</Text> : null}
<Text color={t.color.muted}> {modelLabel(model, modelReasoningEffort, modelFast)}</Text>
{ctxLabel ? <Text color={t.color.muted}> {ctxLabel}</Text> : null}
{bar ? (
<Text color={t.color.dim}>
<Text color={t.color.muted}>
{' │ '}
<Text color={barColor}>[{bar}]</Text> <Text color={barColor}>{pct != null ? `${pct}%` : ''}</Text>
</Text>
) : null}
{sessionStartedAt ? (
<Text color={t.color.dim}>
<Text color={t.color.muted}>
{' │ '}
<SessionDuration startedAt={sessionStartedAt} />
</Text>
@ -242,21 +318,21 @@ export function StatusRule({
{voiceLabel ? (
<Text
color={
voiceLabel.startsWith('●') ? t.color.error : voiceLabel.startsWith('◉') ? t.color.warn : t.color.dim
voiceLabel.startsWith('●') ? t.color.error : voiceLabel.startsWith('◉') ? t.color.warn : t.color.muted
}
>
{' │ '}
{voiceLabel}
</Text>
) : null}
{bgCount > 0 ? <Text color={t.color.dim}> {bgCount} bg</Text> : null}
{bgCount > 0 ? <Text color={t.color.muted}> {bgCount} bg</Text> : null}
{showCost && typeof usage.cost_usd === 'number' ? (
<Text color={t.color.dim}> ${usage.cost_usd.toFixed(4)}</Text>
<Text color={t.color.muted}> ${usage.cost_usd.toFixed(4)}</Text>
) : null}
</Text>
</Box>
<Text color={t.color.bronze}> </Text>
<Text color={t.color.border}> </Text>
<Text color={t.color.label}>{cwdLabel}</Text>
</Box>
)
@ -301,8 +377,8 @@ export function TranscriptScrollbar({ scrollRef, t }: TranscriptScrollbarProps)
const thumb = scrollable ? Math.max(1, Math.round((vp * vp) / total)) : vp
const travel = Math.max(1, vp - thumb)
const thumbTop = scrollable ? Math.round((pos / Math.max(1, total - vp)) * travel) : 0
const thumbColor = grab !== null ? t.color.gold : hover ? t.color.amber : t.color.bronze
const trackColor = hover ? t.color.bronze : t.color.dim
const thumbColor = grab !== null ? t.color.primary : hover ? t.color.accent : t.color.border
const trackColor = hover ? t.color.border : t.color.muted
const jump = (row: number, offset: number) => {
if (!s || !scrollable) {

View file

@ -1,4 +1,4 @@
import { AlternateScreen, Box, NoSelect, ScrollBox, Text } from '@hermes/ink'
import { AlternateScreen, Box, NoSelect, ScrollBox, stringWidth, Text } from '@hermes/ink'
import { useStore } from '@nanostores/react'
import { Fragment, memo, useMemo, useRef } from 'react'
@ -124,8 +124,10 @@ const ComposerPane = memo(function ComposerPane({
const ui = useStore($uiState)
const isBlocked = useStore($isBlocked)
const sh = (composer.inputBuf[0] ?? composer.input).startsWith('!')
const pw = 2
const inputColumns = stableComposerColumns(composer.cols, pw)
const promptText = sh ? '$' : ui.theme.brand.prompt
const promptLabel = `${promptText} `
const promptWidth = Math.max(1, stringWidth(promptLabel))
const inputColumns = stableComposerColumns(composer.cols, promptWidth)
const inputHeight = inputVisualHeight(composer.input, inputColumns)
const inputMouseRef = useRef<null | TextInputMouseApi>(null)
@ -146,7 +148,7 @@ const ComposerPane = memo(function ComposerPane({
}
e.stopImmediatePropagation?.()
inputMouseRef.current?.dragAt(e.localRow ?? 0, (e.localCol ?? 0) - pw)
inputMouseRef.current?.dragAt(e.localRow ?? 0, (e.localCol ?? 0) - promptWidth)
}
// Spacer rows live on a different vertical origin; only the column is
@ -158,7 +160,7 @@ const ComposerPane = memo(function ComposerPane({
}
e.stopImmediatePropagation?.()
inputMouseRef.current?.dragAt(0, (e.localCol ?? 0) - pw)
inputMouseRef.current?.dragAt(0, (e.localCol ?? 0) - promptWidth)
}
const endInputDrag = () => inputMouseRef.current?.end()
@ -183,13 +185,13 @@ const ComposerPane = memo(function ComposerPane({
/>
{ui.bgTasks.size > 0 && (
<Text color={ui.theme.color.dim}>
<Text color={ui.theme.color.muted}>
{ui.bgTasks.size} background {ui.bgTasks.size === 1 ? 'task' : 'tasks'} running
</Text>
)}
{status.showStickyPrompt ? (
<Text color={ui.theme.color.dim} wrap="truncate-end">
<Text color={ui.theme.color.muted} wrap="truncate-end">
<Text color={ui.theme.color.label}> </Text>
{status.stickyPrompt}
@ -214,21 +216,21 @@ const ComposerPane = memo(function ComposerPane({
<>
{composer.inputBuf.map((line, i) => (
<Box key={i}>
<Box width={2}>
<Text color={ui.theme.color.dim}>{i === 0 ? `${ui.theme.brand.prompt} ` : ' '}</Text>
<Box width={promptWidth}>
<Text color={ui.theme.color.muted}>{i === 0 ? promptLabel : ' '.repeat(promptWidth)}</Text>
</Box>
<Text color={ui.theme.color.cornsilk}>{line || ' '}</Text>
<Text color={ui.theme.color.text}>{line || ' '}</Text>
</Box>
))}
<Box onMouseDown={captureInputDrag} onMouseDrag={dragFromPromptRow} onMouseUp={endInputDrag} position="relative">
<Box width={pw}>
<Box width={promptWidth}>
{sh ? (
<Text color={ui.theme.color.shellDollar}>$ </Text>
<Text color={ui.theme.color.shellDollar}>{promptLabel}</Text>
) : (
<Text bold color={ui.theme.color.prompt}>
{composer.inputBuf.length ? ' ' : `${ui.theme.brand.prompt} `}
{composer.inputBuf.length ? ' '.repeat(promptWidth) : promptLabel}
</Text>
)}
</Box>
@ -254,7 +256,7 @@ const ComposerPane = memo(function ComposerPane({
)}
</Box>
{!composer.empty && !ui.sid && <Text color={ui.theme.color.dim}> {ui.status}</Text>}
{!composer.empty && !ui.sid && <Text color={ui.theme.color.muted}> {ui.status}</Text>}
<StatusRulePane at="bottom" composer={composer} status={status} />
</NoSelect>
@ -319,6 +321,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
@ -359,7 +362,7 @@ export const AppLayout = memo(function AppLayout({
{SHOW_FPS && (
<Box flexShrink={0} justifyContent="flex-end" paddingRight={1}>
<FpsOverlay />
<FpsOverlay t={ui.theme} />
</Box>
)}
</>

View file

@ -119,7 +119,7 @@ export function FloatingOverlays({
return (
<Box alignItems="flex-start" bottom="100%" flexDirection="column" left={0} position="absolute" right={0}>
{overlay.picker && (
<FloatBox color={ui.theme.color.bronze}>
<FloatBox color={ui.theme.color.border}>
<SessionPicker
gw={gw}
onCancel={() => patchOverlayState({ picker: false })}
@ -130,7 +130,7 @@ export function FloatingOverlays({
)}
{overlay.modelPicker && (
<FloatBox color={ui.theme.color.bronze}>
<FloatBox color={ui.theme.color.border}>
<ModelPicker
gw={gw}
onCancel={() => patchOverlayState({ modelPicker: false })}
@ -142,17 +142,17 @@ export function FloatingOverlays({
)}
{overlay.skillsHub && (
<FloatBox color={ui.theme.color.bronze}>
<FloatBox color={ui.theme.color.border}>
<SkillsHub gw={gw} onClose={() => patchOverlayState({ skillsHub: false })} t={ui.theme} />
</FloatBox>
)}
{overlay.pager && (
<FloatBox color={ui.theme.color.bronze}>
<FloatBox color={ui.theme.color.border}>
<Box flexDirection="column" paddingX={1} paddingY={1}>
{overlay.pager.title && (
<Box justifyContent="center" marginBottom={1}>
<Text bold color={ui.theme.color.gold}>
<Text bold color={ui.theme.color.primary}>
{overlay.pager.title}
</Text>
</Box>
@ -174,7 +174,7 @@ export function FloatingOverlays({
)}
{!!completions.length && (
<FloatBox color={ui.theme.color.gold}>
<FloatBox color={ui.theme.color.primary}>
<Box flexDirection="column" width={Math.max(28, cols - 6)}>
{completions.slice(start, start + viewportSize).map((item, i) => {
const active = start + i === compIdx
@ -190,7 +190,7 @@ export function FloatingOverlays({
{' '}
{item.display}
</Text>
{item.meta ? <Text color={ui.theme.color.dim}> {item.meta}</Text> : null}
{item.meta ? <Text color={ui.theme.color.muted}> {item.meta}</Text> : null}
</Box>
)
})}

View file

@ -26,12 +26,12 @@ export function Banner({ t }: { t: Theme }) {
{cols >= (t.bannerLogo ? artWidth(logoLines) : LOGO_WIDTH) ? (
<ArtLines lines={logoLines} />
) : (
<Text bold color={t.color.gold}>
<Text bold color={t.color.primary}>
{t.brand.icon} NOUS HERMES
</Text>
)}
<Text color={t.color.dim}>{t.brand.icon} Nous Research · Messenger of the Digital Gods</Text>
<Text color={t.color.muted}>{t.brand.icon} Nous Research · Messenger of the Digital Gods</Text>
</Box>
)
}
@ -70,19 +70,19 @@ export function SessionPanel({ info, sid, t }: SessionPanelProps) {
return (
<Box flexDirection="column" marginTop={1}>
<Text bold color={t.color.amber}>
<Text bold color={t.color.accent}>
Available {title}
</Text>
{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.muted}>{strip(k)}: </Text>
<Text color={t.color.text}>{truncLine(strip(k) + ': ', vs)}</Text>
</Text>
))}
{overflow > 0 && (
<Text color={t.color.dim}>
<Text color={t.color.muted}>
(and {overflow} {overflowLabel})
</Text>
)}
@ -91,18 +91,18 @@ export function SessionPanel({ info, sid, t }: SessionPanelProps) {
}
return (
<Box borderColor={t.color.bronze} borderStyle="round" marginBottom={1} paddingX={2} paddingY={1}>
<Box borderColor={t.color.border} borderStyle="round" marginBottom={1} paddingX={2} paddingY={1}>
{wide && (
<Box flexDirection="column" marginRight={2} width={leftW}>
<ArtLines lines={heroLines} />
<Text />
<Text color={t.color.amber}>
<Text color={t.color.accent}>
{info.model.split('/').pop()}
<Text color={t.color.dim}> · Nous Research</Text>
<Text color={t.color.muted}> · Nous Research</Text>
</Text>
<Text color={t.color.dim} wrap="truncate-end">
<Text color={t.color.muted} wrap="truncate-end">
{info.cwd || process.cwd()}
</Text>
@ -117,7 +117,7 @@ export function SessionPanel({ info, sid, t }: SessionPanelProps) {
<Box flexDirection="column" width={w}>
<Box justifyContent="center" marginBottom={1}>
<Text bold color={t.color.gold}>
<Text bold color={t.color.primary}>
{t.brand.name}
{info.version ? ` v${info.version}` : ''}
{info.release_date ? ` (${info.release_date})` : ''}
@ -129,17 +129,17 @@ export function SessionPanel({ info, sid, t }: SessionPanelProps) {
{info.mcp_servers && info.mcp_servers.length > 0 && (
<Box flexDirection="column" marginTop={1}>
<Text bold color={t.color.amber}>
<Text bold color={t.color.accent}>
MCP Servers
</Text>
{info.mcp_servers.map(s => (
<Text key={s.name} wrap="truncate">
<Text color={t.color.dim}>{` ${s.name} `}</Text>
<Text color={t.color.dim}>{`[${s.transport}]`}</Text>
<Text color={t.color.dim}>: </Text>
<Text color={t.color.muted}>{` ${s.name} `}</Text>
<Text color={t.color.muted}>{`[${s.transport}]`}</Text>
<Text color={t.color.muted}>: </Text>
{s.connected ? (
<Text color={t.color.cornsilk}>
<Text color={t.color.text}>
{s.tools} tool{s.tools === 1 ? '' : 's'}
</Text>
) : (
@ -152,12 +152,12 @@ 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` : ''}
{' · '}
<Text color={t.color.dim}>/help for commands</Text>
<Text color={t.color.muted}>/help for commands</Text>
</Text>
{typeof info.update_behind === 'number' && info.update_behind > 0 && (
@ -183,9 +183,9 @@ export function SessionPanel({ info, sid, t }: SessionPanelProps) {
export function Panel({ sections, t, title }: PanelProps) {
return (
<Box borderColor={t.color.bronze} borderStyle="round" flexDirection="column" paddingX={2} paddingY={1}>
<Box borderColor={t.color.border} borderStyle="round" flexDirection="column" paddingX={2} paddingY={1}>
<Box justifyContent="center" marginBottom={1}>
<Text bold color={t.color.gold}>
<Text bold color={t.color.primary}>
{title}
</Text>
</Box>
@ -193,25 +193,25 @@ export function Panel({ sections, t, title }: PanelProps) {
{sections.map((sec, si) => (
<Box flexDirection="column" key={si} marginTop={si > 0 ? 1 : 0}>
{sec.title && (
<Text bold color={t.color.amber}>
<Text bold color={t.color.accent}>
{sec.title}
</Text>
)}
{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.muted}>{k.padEnd(20)}</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>
))}
{sec.text && <Text color={t.color.dim}>{sec.text}</Text>}
{sec.text && <Text color={t.color.muted}>{sec.text}</Text>}
</Box>
))}
</Box>

View file

@ -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>
)

View file

@ -1,5 +1,5 @@
import { Box, Link, Text } from '@hermes/ink'
import { memo, type ReactNode, useMemo } from 'react'
import { Fragment, memo, type ReactNode, useMemo } from 'react'
import { ensureEmojiPresentation } from '../lib/emoji.js'
import { BOX_CLOSE, BOX_OPEN, texToUnicode } from '../lib/mathUnicode.js'
@ -9,9 +9,10 @@ import type { Theme } from '../theme.js'
// `\boxed{X}` regions in `texToUnicode` output are marked with the
// non-printable U+0001 / U+0002 sentinels. Split on them and render the
// boxed segment with `inverse + bold` so it reads as a highlighter-pen
// emphasis on top of whatever color the parent `<Text>` is using (amber
// for math). The leading / trailing space inside the highlight gives a
// one-cell visual margin so the highlight reads as a block, not a hug.
// emphasis on top of whatever color the parent `<Text>` is using (the
// theme accent for math). The leading / trailing space inside the
// highlight gives a one-cell visual margin so the highlight reads as a
// block, not a hug.
const renderMath = (text: string): ReactNode => {
if (!text.includes(BOX_OPEN)) {
return text
@ -144,7 +145,7 @@ const autolinkUrl = (raw: string) =>
const renderAutolink = (k: number, t: Theme, raw: string) => (
<Link key={k} url={autolinkUrl(raw)}>
<Text color={t.color.amber} underline>
<Text color={t.color.accent} underline>
{raw.replace(/^mailto:/, '')}
</Text>
</Link>
@ -171,18 +172,35 @@ export const stripInlineMarkup = (v: string) =>
const renderTable = (k: number, rows: string[][], t: Theme) => {
const widths = rows[0]!.map((_, ci) => Math.max(...rows.map(r => stripInlineMarkup(r[ci] ?? '').length)))
// Thin divider under the header. Without it tables look like prose
// with extra spacing because the header is just accent-coloured text
// (#15534). We avoid full borders on purpose — column widths come
// from `stripInlineMarkup(...).length` (UTF-16 code units, not
// display width), so a real outline often misaligns on emoji and
// East-Asian wide characters; one dim solid rule (`─`) under row 0
// plus tab-style column gaps reads cleanly on every terminal we
// tested.
const sep = widths.map(w => '─'.repeat(Math.max(1, w))).join(' ')
return (
<Box flexDirection="column" key={k} paddingLeft={2}>
{rows.map((row, ri) => (
<Box key={ri}>
{widths.map((w, ci) => (
<Text color={ri === 0 ? t.color.amber : undefined} key={ci}>
<MdInline t={t} text={row[ci] ?? ''} />
{' '.repeat(Math.max(0, w - stripInlineMarkup(row[ci] ?? '').length))}
{ci < widths.length - 1 ? ' ' : ''}
<Fragment key={ri}>
<Box>
{widths.map((w, ci) => (
<Text bold={ri === 0} color={ri === 0 ? t.color.accent : undefined} key={ci}>
<MdInline t={t} text={row[ci] ?? ''} />
{' '.repeat(Math.max(0, w - stripInlineMarkup(row[ci] ?? '').length))}
{ci < widths.length - 1 ? ' ' : ''}
</Text>
))}
</Box>
{ri === 0 && rows.length > 1 ? (
<Text color={t.color.muted} dimColor>
{sep}
</Text>
))}
</Box>
) : null}
</Fragment>
))}
</Box>
)
@ -203,14 +221,14 @@ function MdInline({ t, text }: { t: Theme; text: string }) {
if (m[1] && m[2]) {
parts.push(
<Text color={t.color.dim} key={parts.length}>
<Text color={t.color.muted} key={parts.length}>
[image: {m[1]}] {m[2]}
</Text>
)
} else if (m[3] && m[4]) {
parts.push(
<Link key={parts.length} url={m[4]}>
<Text color={t.color.amber} underline>
<Text color={t.color.accent} underline>
{m[3]}
</Text>
</Link>
@ -228,7 +246,7 @@ function MdInline({ t, text }: { t: Theme; text: string }) {
// are verbatim by definition. Letting MdInline reprocess them
// would corrupt regex examples and shell snippets.
parts.push(
<Text color={t.color.amber} dimColor key={parts.length}>
<Text color={t.color.accent} dimColor key={parts.length}>
{m[7]}
</Text>
)
@ -257,19 +275,19 @@ function MdInline({ t, text }: { t: Theme; text: string }) {
)
} else if (m[13]) {
parts.push(
<Text color={t.color.dim} key={parts.length}>
<Text color={t.color.muted} key={parts.length}>
[{m[13]}]
</Text>
)
} else if (m[14]) {
parts.push(
<Text color={t.color.dim} key={parts.length}>
<Text color={t.color.muted} key={parts.length}>
^{m[14]}
</Text>
)
} else if (m[15]) {
parts.push(
<Text color={t.color.dim} key={parts.length}>
<Text color={t.color.muted} key={parts.length}>
_{m[15]}
</Text>
)
@ -286,13 +304,13 @@ function MdInline({ t, text }: { t: Theme; text: string }) {
} else if (m[17] ?? m[18]) {
// Inline math is run through `texToUnicode` (Greek letters, ,
// operators, sub/superscripts, fractions) and rendered in italic
// amber. Italic is the disambiguator — links use amber+underline,
// accent. Italic is the disambiguator — links use accent+underline,
// so without italic readers can't tell `\mathbb{R}` (math) from a
// hyperlinked word. Anything `texToUnicode` doesn't recognise is
// preserved verbatim, so unfamiliar commands just look like their
// raw LaTeX rather than vanishing.
parts.push(
<Text color={t.color.amber} italic key={parts.length}>
<Text color={t.color.accent} italic key={parts.length}>
{renderMath(texToUnicode(m[17] ?? m[18]!))}
</Text>
)
@ -402,11 +420,11 @@ function MdImpl({ compact, t, text }: MdProps) {
if (media) {
start('paragraph')
nodes.push(
<Text color={t.color.dim} key={key}>
<Text color={t.color.muted} key={key}>
{'▸ '}
<Link url={/^(?:\/|[a-z]:[\\/])/i.test(media) ? `file://${media}` : media}>
<Text color={t.color.amber} underline>
<Text color={t.color.accent} underline>
{media}
</Text>
</Link>
@ -453,7 +471,7 @@ function MdImpl({ compact, t, text }: MdProps) {
nodes.push(
<Box flexDirection="column" key={key} paddingLeft={2}>
{lang && !isDiff && <Text color={t.color.dim}>{'─ ' + lang}</Text>}
{lang && !isDiff && <Text color={t.color.muted}>{'─ ' + lang}</Text>}
{block.map((l, j) => {
if (highlighted) {
@ -479,7 +497,7 @@ function MdImpl({ compact, t, text }: MdProps) {
return (
<Text
backgroundColor={add ? t.color.diffAdded : del ? t.color.diffRemoved : undefined}
color={add ? t.color.diffAddedWord : del ? t.color.diffRemovedWord : hunk ? t.color.dim : undefined}
color={add ? t.color.diffAddedWord : del ? t.color.diffRemovedWord : hunk ? t.color.muted : undefined}
dimColor={isDiff && !add && !del && !hunk && l.startsWith(' ')}
key={j}
>
@ -513,7 +531,7 @@ function MdImpl({ compact, t, text }: MdProps) {
start('code')
nodes.push(
<Box flexDirection="column" key={key} paddingLeft={2}>
{inner ? <Text color={t.color.amber}>{renderMath(texToUnicode(inner))}</Text> : null}
{inner ? <Text color={t.color.accent}>{renderMath(texToUnicode(inner))}</Text> : null}
</Box>
)
i++
@ -560,7 +578,7 @@ function MdImpl({ compact, t, text }: MdProps) {
nodes.push(
<Box flexDirection="column" key={key} paddingLeft={2}>
{block.map((l, j) => (
<Text color={t.color.amber} key={j}>
<Text color={t.color.accent} key={j}>
{renderMath(texToUnicode(l))}
</Text>
))}
@ -576,7 +594,7 @@ function MdImpl({ compact, t, text }: MdProps) {
if (heading) {
start('heading')
nodes.push(
<Text bold color={t.color.amber} key={key}>
<Text bold color={t.color.accent} key={key}>
<MdInline t={t} text={heading} />
</Text>
)
@ -588,7 +606,7 @@ function MdImpl({ compact, t, text }: MdProps) {
if (i + 1 < lines.length && SETEXT_RE.test(lines[i + 1]!)) {
start('heading')
nodes.push(
<Text bold color={t.color.amber} key={key}>
<Text bold color={t.color.accent} key={key}>
<MdInline t={t} text={line.trim()} />
</Text>
)
@ -600,7 +618,7 @@ function MdImpl({ compact, t, text }: MdProps) {
if (HR_RE.test(line)) {
start('rule')
nodes.push(
<Text color={t.color.dim} key={key}>
<Text color={t.color.muted} key={key}>
{'─'.repeat(36)}
</Text>
)
@ -614,7 +632,7 @@ function MdImpl({ compact, t, text }: MdProps) {
if (footnote) {
start('list')
nodes.push(
<Text color={t.color.dim} key={key}>
<Text color={t.color.muted} key={key}>
[{footnote[1]}] <MdInline t={t} text={footnote[2] ?? ''} />
</Text>
)
@ -623,7 +641,7 @@ function MdImpl({ compact, t, text }: MdProps) {
while (i < lines.length && /^\s{2,}\S/.test(lines[i]!)) {
nodes.push(
<Box key={`${key}-cont-${i}`} paddingLeft={2}>
<Text color={t.color.dim}>
<Text color={t.color.muted}>
<MdInline t={t} text={lines[i]!.trim()} />
</Text>
</Box>
@ -652,7 +670,7 @@ function MdImpl({ compact, t, text }: MdProps) {
nodes.push(
<Text key={`${key}-def-${i}`}>
<Text color={t.color.dim}> · </Text>
<Text color={t.color.muted}> · </Text>
<MdInline t={t} text={def} />
</Text>
)
@ -672,7 +690,7 @@ function MdImpl({ compact, t, text }: MdProps) {
nodes.push(
<Text key={key}>
<Text color={t.color.dim}>
<Text color={t.color.muted}>
{' '.repeat(indentDepth(bullet[1]!) * 2)}
{marker}{' '}
</Text>
@ -691,7 +709,7 @@ function MdImpl({ compact, t, text }: MdProps) {
start('list')
nodes.push(
<Text key={key}>
<Text color={t.color.dim}>
<Text color={t.color.muted}>
{' '.repeat(indentDepth(numbered[1]!) * 2)}
{numbered[2]}.{' '}
</Text>
@ -719,7 +737,7 @@ function MdImpl({ compact, t, text }: MdProps) {
nodes.push(
<Box flexDirection="column" key={key}>
{quoteLines.map((ql, qi) => (
<Text color={t.color.dim} key={qi}>
<Text color={t.color.muted} key={qi}>
{' '.repeat(Math.max(0, ql.depth - 1) * 2)}
{'│ '}
<MdInline t={t} text={ql.text} />
@ -756,7 +774,7 @@ function MdImpl({ compact, t, text }: MdProps) {
if (summary) {
start('paragraph')
nodes.push(
<Text color={t.color.dim} key={key}>
<Text color={t.color.muted} key={key}>
{summary}
</Text>
)
@ -768,7 +786,7 @@ function MdImpl({ compact, t, text }: MdProps) {
if (/^<\/?[^>]+>$/.test(line.trim())) {
start('paragraph')
nodes.push(
<Text color={t.color.dim} key={key}>
<Text color={t.color.muted} key={key}>
{line.trim()}
</Text>
)

View file

@ -14,7 +14,7 @@ export function MaskedPrompt({ cols = 80, icon, label, onSubmit, sub, t }: Maske
{icon} {label}
</Text>
{sub && <Text color={t.color.dim}> {sub}</Text>}
{sub && <Text color={t.color.muted}> {sub}</Text>}
<Box>
<Text color={t.color.label}>{'> '}</Text>

View file

@ -80,13 +80,13 @@ export const MessageLine = memo(function MessageLine({
const preview = compactPreview(stripped, maxChars) || '(empty tool result)'
return (
<Box alignSelf="flex-start" borderColor={t.color.dim} borderStyle="round" marginLeft={3} paddingX={1}>
<Box alignSelf="flex-start" borderColor={t.color.muted} borderStyle="round" marginLeft={3} paddingX={1}>
{hasAnsi(msg.text) ? (
<Text wrap="truncate-end">
<Ansi>{msg.text}</Ansi>
</Text>
) : (
<Text color={t.color.dim} wrap="truncate-end">
<Text color={t.color.muted} wrap="truncate-end">
{preview}
</Text>
)}
@ -101,7 +101,7 @@ export const MessageLine = memo(function MessageLine({
const content = (() => {
if (msg.kind === 'slash') {
return <Text color={t.color.dim}>{msg.text}</Text>
return <Text color={t.color.muted}>{msg.text}</Text>
}
if (msg.role !== 'user' && hasAnsi(msg.text)) {
@ -125,7 +125,7 @@ export const MessageLine = memo(function MessageLine({
return (
<Text color={body}>
{head}
<Text color={t.color.dim} dimColor>
<Text color={t.color.muted} dimColor>
[long message]
</Text>
{rest.join('')}

View file

@ -146,7 +146,7 @@ export function ModelPicker({ gw, onCancel, onSelect, sessionId, t }: ModelPicke
})
if (loading) {
return <Text color={t.color.dim}>loading models</Text>
return <Text color={t.color.muted}>loading models</Text>
}
if (err) {
@ -161,7 +161,7 @@ export function ModelPicker({ gw, onCancel, onSelect, sessionId, t }: ModelPicke
if (!providers.length) {
return (
<Box flexDirection="column">
<Text color={t.color.dim}>no authenticated providers</Text>
<Text color={t.color.muted}>no authenticated providers</Text>
<OverlayHint t={t}>Esc/q cancel</OverlayHint>
</Box>
)
@ -176,21 +176,21 @@ export function ModelPicker({ gw, onCancel, onSelect, sessionId, t }: ModelPicke
return (
<Box flexDirection="column" width={width}>
<Text bold color={t.color.amber} wrap="truncate-end">
<Text bold color={t.color.accent} wrap="truncate-end">
Select provider (step 1/2)
</Text>
<Text color={t.color.dim} wrap="truncate-end">
<Text color={t.color.muted} wrap="truncate-end">
Full model IDs on the next step · Enter to continue
</Text>
<Text color={t.color.dim} wrap="truncate-end">
<Text color={t.color.muted} wrap="truncate-end">
Current: {currentModel || '(unknown)'}
</Text>
<Text color={t.color.label} wrap="truncate-end">
{provider?.warning ? `warning: ${provider.warning}` : ' '}
</Text>
<Text color={t.color.dim} wrap="truncate-end">
<Text color={t.color.muted} wrap="truncate-end">
{offset > 0 ? `${offset} more` : ' '}
</Text>
@ -201,7 +201,7 @@ export function ModelPicker({ gw, onCancel, onSelect, sessionId, t }: ModelPicke
return row ? (
<Text
bold={providerIdx === idx}
color={providerIdx === idx ? t.color.amber : t.color.dim}
color={providerIdx === idx ? t.color.accent : t.color.muted}
inverse={providerIdx === idx}
key={providers[idx]?.slug ?? `row-${idx}`}
wrap="truncate-end"
@ -210,17 +210,17 @@ export function ModelPicker({ gw, onCancel, onSelect, sessionId, t }: ModelPicke
{i + 1}. {row}
</Text>
) : (
<Text color={t.color.dim} key={`pad-${i}`} wrap="truncate-end">
<Text color={t.color.muted} key={`pad-${i}`} wrap="truncate-end">
{' '}
</Text>
)
})}
<Text color={t.color.dim} wrap="truncate-end">
<Text color={t.color.muted} wrap="truncate-end">
{offset + VISIBLE < rows.length ? `${rows.length - offset - VISIBLE} more` : ' '}
</Text>
<Text color={t.color.dim} wrap="truncate-end">
<Text color={t.color.muted} wrap="truncate-end">
persist: {persistGlobal ? 'global' : 'session'} · g toggle
</Text>
<OverlayHint t={t}>/ select · Enter choose · 1-9,0 quick · Esc/q cancel</OverlayHint>
@ -232,17 +232,17 @@ export function ModelPicker({ gw, onCancel, onSelect, sessionId, t }: ModelPicke
return (
<Box flexDirection="column" width={width}>
<Text bold color={t.color.amber} wrap="truncate-end">
<Text bold color={t.color.accent} wrap="truncate-end">
Select model (step 2/2)
</Text>
<Text color={t.color.dim} wrap="truncate-end">
<Text color={t.color.muted} wrap="truncate-end">
{names[providerIdx] || '(unknown provider)'} · Esc back
</Text>
<Text color={t.color.label} wrap="truncate-end">
{provider?.warning ? `warning: ${provider.warning}` : ' '}
</Text>
<Text color={t.color.dim} wrap="truncate-end">
<Text color={t.color.muted} wrap="truncate-end">
{offset > 0 ? `${offset} more` : ' '}
</Text>
@ -252,11 +252,11 @@ export function ModelPicker({ gw, onCancel, onSelect, sessionId, t }: ModelPicke
if (!row) {
return !models.length && i === 0 ? (
<Text color={t.color.dim} key="empty" wrap="truncate-end">
<Text color={t.color.muted} key="empty" wrap="truncate-end">
no models listed for this provider
</Text>
) : (
<Text color={t.color.dim} key={`pad-${i}`} wrap="truncate-end">
<Text color={t.color.muted} key={`pad-${i}`} wrap="truncate-end">
{' '}
</Text>
)
@ -267,7 +267,7 @@ export function ModelPicker({ gw, onCancel, onSelect, sessionId, t }: ModelPicke
return (
<Text
bold={modelIdx === idx}
color={modelIdx === idx ? t.color.amber : t.color.dim}
color={modelIdx === idx ? t.color.accent : t.color.muted}
inverse={modelIdx === idx}
key={`${provider?.slug ?? 'prov'}:${idx}:${row}`}
wrap="truncate-end"
@ -278,11 +278,11 @@ export function ModelPicker({ gw, onCancel, onSelect, sessionId, t }: ModelPicke
)
})}
<Text color={t.color.dim} wrap="truncate-end">
<Text color={t.color.muted} wrap="truncate-end">
{offset + VISIBLE < models.length ? `${models.length - offset - VISIBLE} more` : ' '}
</Text>
<Text color={t.color.dim} wrap="truncate-end">
<Text color={t.color.muted} wrap="truncate-end">
persist: {persistGlobal ? 'global' : 'session'} · g toggle
</Text>
<OverlayHint t={t}>

View file

@ -20,7 +20,7 @@ export function useOverlayKeys({ disabled = false, onBack, onClose }: OverlayKey
export function OverlayHint({ children, t }: OverlayHintProps) {
return (
<Text color={t.color.dim} wrap="truncate-end">
<Text color={t.color.muted} wrap="truncate-end">
{children}
</Text>
)

View file

@ -48,13 +48,13 @@ 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>
))}
{overflow > 0 ? (
<Text color={t.color.dim}>
<Text color={t.color.muted}>
+{overflow} more line{overflow === 1 ? '' : 's'} (full text above)
</Text>
) : null}
@ -64,14 +64,14 @@ export function ApprovalPrompt({ onChoice, req, t }: ApprovalPromptProps) {
{OPTS.map((o, i) => (
<Text key={o}>
<Text bold={sel === i} color={sel === i ? t.color.warn : t.color.dim} inverse={sel === i}>
<Text bold={sel === i} color={sel === i ? t.color.warn : t.color.muted} inverse={sel === i}>
{sel === i ? '▸ ' : ' '}
{i + 1}. {LABELS[o]}
</Text>
</Text>
))}
<Text color={t.color.dim}>/ select · Enter confirm · 1-4 quick pick · Ctrl+C deny</Text>
<Text color={t.color.muted}>/ select · Enter confirm · 1-4 quick pick · Ctrl+C deny</Text>
</Box>
)
}
@ -84,8 +84,8 @@ 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.accent}>ask</Text>
<Text color={t.color.text}> {req.question}</Text>
</Text>
)
@ -129,7 +129,7 @@ export function ClarifyPrompt({ cols = 80, onAnswer, onCancel, req, t }: Clarify
<TextInput columns={Math.max(20, cols - 6)} onChange={setCustom} onSubmit={onAnswer} value={custom} />
</Box>
<Text color={t.color.dim}>
<Text color={t.color.muted}>
Enter send · Esc {choices.length ? 'back' : 'cancel'} ·{' '}
{isMac ? 'Cmd+C copy · Cmd+V paste · Ctrl+C cancel' : 'Ctrl+C cancel'}
</Text>
@ -143,14 +143,14 @@ export function ClarifyPrompt({ cols = 80, onAnswer, onCancel, req, t }: Clarify
{[...choices, 'Other (type your answer)'].map((c, i) => (
<Text key={i}>
<Text bold={sel === i} color={sel === i ? t.color.label : t.color.dim} inverse={sel === i}>
<Text bold={sel === i} color={sel === i ? t.color.label : t.color.muted} inverse={sel === i}>
{sel === i ? '▸ ' : ' '}
{i + 1}. {c}
</Text>
</Text>
))}
<Text color={t.color.dim}>/ select · Enter confirm · 1-{choices.length} quick pick · Esc/Ctrl+C cancel</Text>
<Text color={t.color.muted}>/ select · Enter confirm · 1-{choices.length} quick pick · Esc/Ctrl+C cancel</Text>
</Box>
)
}
@ -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>
@ -207,12 +207,12 @@ export function ConfirmPrompt({ onCancel, onConfirm, req, t }: ConfirmPromptProp
{rows.map((row, i) => (
<Text key={row.label}>
<Text color={sel === i ? accent : t.color.dim}>{sel === i ? '▸ ' : ' '}</Text>
<Text color={sel === i ? row.color : t.color.dim}>{row.label}</Text>
<Text color={sel === i ? accent : t.color.muted}>{sel === i ? '▸ ' : ' '}</Text>
<Text color={sel === i ? row.color : t.color.muted}>{row.label}</Text>
</Text>
))}
<Text color={t.color.dim}>/ select · Enter confirm · Y/N quick · Esc cancel</Text>
<Text color={t.color.muted}>/ select · Enter confirm · Y/N quick · Esc cancel</Text>
</Box>
)
}

View file

@ -23,14 +23,14 @@ export function QueuedMessages({ cols, queueEditIdx, queued, t }: QueuedMessages
return (
<Box flexDirection="column" marginTop={1}>
<Text color={t.color.dim} dimColor>
<Text color={t.color.muted} dimColor>
{`queued (${queued.length})${
queueEditIdx !== null ? ` · editing ${queueEditIdx + 1} · Ctrl+X delete · Esc cancel` : ''
}`}
</Text>
{q.showLead && (
<Text color={t.color.dim} dimColor>
<Text color={t.color.muted} dimColor>
{' '}
</Text>
@ -41,14 +41,14 @@ export function QueuedMessages({ cols, queueEditIdx, queued, t }: QueuedMessages
const active = queueEditIdx === idx
return (
<Text color={active ? t.color.amber : t.color.dim} dimColor key={`${idx}-${item.slice(0, 16)}`}>
<Text color={active ? t.color.accent : t.color.muted} dimColor key={`${idx}-${item.slice(0, 16)}`}>
{active ? '▸' : ' '} {idx + 1}. {compactPreview(item, Math.max(16, cols - 10))}
</Text>
)
})}
{q.showTail && (
<Text color={t.color.dim} dimColor>
<Text color={t.color.muted} dimColor>
{' '}and {queued.length - q.end} more
</Text>
)}

View file

@ -80,7 +80,7 @@ export function SessionPicker({ gw, onCancel, onSelect, t }: SessionPickerProps)
})
if (loading) {
return <Text color={t.color.dim}>loading sessions</Text>
return <Text color={t.color.muted}>loading sessions</Text>
}
if (err) {
@ -95,7 +95,7 @@ export function SessionPicker({ gw, onCancel, onSelect, t }: SessionPickerProps)
if (!items.length) {
return (
<Box flexDirection="column">
<Text color={t.color.dim}>no previous sessions</Text>
<Text color={t.color.muted}>no previous sessions</Text>
<OverlayHint t={t}>Esc/q cancel</OverlayHint>
</Box>
)
@ -105,11 +105,11 @@ export function SessionPicker({ gw, onCancel, onSelect, t }: SessionPickerProps)
return (
<Box flexDirection="column" width={width}>
<Text bold color={t.color.amber}>
<Text bold color={t.color.accent}>
Resume Session
</Text>
{offset > 0 && <Text color={t.color.dim}> {offset} more</Text>}
{offset > 0 && <Text color={t.color.muted}> {offset} more</Text>}
{items.slice(offset, offset + VISIBLE).map((s, vi) => {
const i = offset + vi
@ -117,30 +117,30 @@ export function SessionPicker({ gw, onCancel, onSelect, t }: SessionPickerProps)
return (
<Box key={s.id}>
<Text bold={selected} color={selected ? t.color.amber : t.color.dim} inverse={selected}>
<Text bold={selected} color={selected ? t.color.accent : t.color.muted} inverse={selected}>
{selected ? '▸ ' : ' '}
</Text>
<Box width={30}>
<Text bold={selected} color={selected ? t.color.amber : t.color.dim} inverse={selected}>
<Text bold={selected} color={selected ? t.color.accent : t.color.muted} inverse={selected}>
{String(i + 1).padStart(2)}. [{s.id}]
</Text>
</Box>
<Box width={30}>
<Text bold={selected} color={selected ? t.color.amber : t.color.dim} inverse={selected}>
<Text bold={selected} color={selected ? t.color.accent : t.color.muted} inverse={selected}>
({s.message_count} msgs, {age(s.started_at)}, {s.source || 'tui'})
</Text>
</Box>
<Text bold={selected} color={selected ? t.color.amber : t.color.dim} inverse={selected} wrap="truncate-end">
<Text bold={selected} color={selected ? t.color.accent : t.color.muted} inverse={selected} wrap="truncate-end">
{s.title || s.preview || '(untitled)'}
</Text>
</Box>
)
})}
{offset + VISIBLE < items.length && <Text color={t.color.dim}> {items.length - offset - VISIBLE} more</Text>}
{offset + VISIBLE < items.length && <Text color={t.color.muted}> {items.length - offset - VISIBLE} more</Text>}
<OverlayHint t={t}>/ select · Enter resume · 1-9 quick · Esc/q cancel</OverlayHint>
</Box>
)

View file

@ -179,7 +179,7 @@ export function SkillsHub({ gw, onClose, t }: SkillsHubProps) {
})
if (loading) {
return <Text color={t.color.dim}>loading skills</Text>
return <Text color={t.color.muted}>loading skills</Text>
}
if (err && stage === 'category') {
@ -194,7 +194,7 @@ export function SkillsHub({ gw, onClose, t }: SkillsHubProps) {
if (!cats.length) {
return (
<Box flexDirection="column" width={width}>
<Text color={t.color.dim}>no skills available</Text>
<Text color={t.color.muted}>no skills available</Text>
<OverlayHint t={t}>Esc/q cancel</OverlayHint>
</Box>
)
@ -206,12 +206,12 @@ export function SkillsHub({ gw, onClose, t }: SkillsHubProps) {
return (
<Box flexDirection="column" width={width}>
<Text bold color={t.color.amber}>
<Text bold color={t.color.accent}>
Skills Hub
</Text>
<Text color={t.color.dim}>select a category</Text>
{offset > 0 && <Text color={t.color.dim}> {offset} more</Text>}
<Text color={t.color.muted}>select a category</Text>
{offset > 0 && <Text color={t.color.muted}> {offset} more</Text>}
{items.map((row, i) => {
const idx = offset + i
@ -219,7 +219,7 @@ export function SkillsHub({ gw, onClose, t }: SkillsHubProps) {
return (
<Text
bold={catIdx === idx}
color={catIdx === idx ? t.color.amber : t.color.dim}
color={catIdx === idx ? t.color.accent : t.color.muted}
inverse={catIdx === idx}
key={row}
wrap="truncate-end"
@ -230,7 +230,7 @@ export function SkillsHub({ gw, onClose, t }: SkillsHubProps) {
)
})}
{offset + VISIBLE < rows.length && <Text color={t.color.dim}> {rows.length - offset - VISIBLE} more</Text>}
{offset + VISIBLE < rows.length && <Text color={t.color.muted}> {rows.length - offset - VISIBLE} more</Text>}
<OverlayHint t={t}>/ select · Enter open · 1-9,0 quick · Esc/q cancel</OverlayHint>
</Box>
)
@ -241,13 +241,13 @@ export function SkillsHub({ gw, onClose, t }: SkillsHubProps) {
return (
<Box flexDirection="column" width={width}>
<Text bold color={t.color.amber}>
<Text bold color={t.color.accent}>
{selectedCat}
</Text>
<Text color={t.color.dim}>{skills.length} skill(s)</Text>
{!skills.length ? <Text color={t.color.dim}>no skills in this category</Text> : null}
{offset > 0 && <Text color={t.color.dim}> {offset} more</Text>}
<Text color={t.color.muted}>{skills.length} skill(s)</Text>
{!skills.length ? <Text color={t.color.muted}>no skills in this category</Text> : null}
{offset > 0 && <Text color={t.color.muted}> {offset} more</Text>}
{items.map((row, i) => {
const idx = offset + i
@ -255,7 +255,7 @@ export function SkillsHub({ gw, onClose, t }: SkillsHubProps) {
return (
<Text
bold={skillIdx === idx}
color={skillIdx === idx ? t.color.amber : t.color.dim}
color={skillIdx === idx ? t.color.accent : t.color.muted}
inverse={skillIdx === idx}
key={row}
wrap="truncate-end"
@ -267,7 +267,7 @@ export function SkillsHub({ gw, onClose, t }: SkillsHubProps) {
})}
{offset + VISIBLE < skills.length && (
<Text color={t.color.dim}> {skills.length - offset - VISIBLE} more</Text>
<Text color={t.color.muted}> {skills.length - offset - VISIBLE} more</Text>
)}
<OverlayHint t={t}>
{skills.length ? '↑/↓ select · Enter open · 1-9,0 quick · Esc back · q close' : 'Esc back · q close'}
@ -278,16 +278,16 @@ export function SkillsHub({ gw, onClose, t }: SkillsHubProps) {
return (
<Box flexDirection="column" width={width}>
<Text bold color={t.color.amber}>
<Text bold color={t.color.accent}>
{info?.name ?? skillName}
</Text>
<Text color={t.color.dim}>{info?.category ?? selectedCat}</Text>
{info?.description ? <Text color={t.color.cornsilk}>{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}
<Text color={t.color.muted}>{info?.category ?? selectedCat}</Text>
{info?.description ? <Text color={t.color.text}>{info.description}</Text> : null}
{info?.path ? <Text color={t.color.muted}>path: {info.path}</Text> : null}
{!info && !err ? <Text color={t.color.muted}>loading</Text> : null}
{err ? <Text color={t.color.label}>error: {err}</Text> : null}
{installing ? <Text color={t.color.amber}>installing</Text> : null}
{installing ? <Text color={t.color.accent}>installing</Text> : null}
<OverlayHint t={t}>i reinspect · x reinstall · Enter/Esc back · q close</OverlayHint>
</Box>

View file

@ -360,6 +360,10 @@ export function TextInput({
const nativeCursor = focus && termFocus && !selected && !!stdout?.isTTY
// Placeholder text is just a hint, not a selection — render it dim
// without inverse styling. In a TTY the hardware cursor parks at column
// 0 and visually marks the input start. Non-TTY surfaces still need the
// synthetic inverse first-char to draw a cursor at all.
const rendered = useMemo(() => {
if (!focus) {
return display || dim(placeholder)
@ -711,6 +715,14 @@ export function TextInput({
if (range && range.start === range.end) {
selRef.current = null
setSel(null)
return
}
const normalized = selRange()
if (isMac && normalized) {
void writeClipboardText(vRef.current.slice(normalized.start, normalized.end))
}
}

View file

@ -77,7 +77,7 @@ function TreeRow({
return (
<Box>
<NoSelect flexShrink={0} fromLeftEdge width={lead.length}>
<Text color={stemColor ?? t.color.dim} dim={stemDim}>
<Text color={stemColor ?? t.color.muted} dim={stemDim}>
{lead}
</Text>
</NoSelect>
@ -246,12 +246,12 @@ function Chevron({
title: string
tone?: 'dim' | 'error' | 'warn'
}) {
const color = tone === 'error' ? t.color.error : tone === 'warn' ? t.color.warn : t.color.dim
const color = tone === 'error' ? t.color.error : tone === 'warn' ? t.color.warn : t.color.muted
return (
<Box onClick={(e: any) => onClick(!!e?.shiftKey || !!e?.ctrlKey)}>
<Text color={color} dim={tone === 'dim'}>
<Text color={t.color.amber}>{open ? '▾ ' : '▸ '}</Text>
<Text color={t.color.accent}>{open ? '▾ ' : '▸ '}</Text>
{title}
{typeof count === 'number' ? ` (${count})` : ''}
{suffix ? (
@ -266,7 +266,7 @@ function Chevron({
}
function heatColor(node: SubagentNode, peak: number, theme: Theme): string | undefined {
const palette = [theme.color.bronze, theme.color.amber, theme.color.gold, theme.color.warn, theme.color.error]
const palette = [theme.color.border, theme.color.accent, theme.color.primary, theme.color.warn, theme.color.error]
const idx = hotnessBucket(node.aggregate.hotness, peak, palette.length)
// Below the median bucket we keep the default dim stem so cool branches
@ -394,7 +394,7 @@ function SubagentAccordion({
const hasTools = item.tools.length > 0
const noteRows = [...(summary ? [summary] : []), ...item.notes]
const hasNotes = noteRows.length > 0
const noteColor = statusTone === 'error' ? t.color.error : statusTone === 'warn' ? t.color.warn : t.color.dim
const noteColor = statusTone === 'error' ? t.color.error : statusTone === 'warn' ? t.color.warn : t.color.muted
const sections: {
header: ReactNode
@ -460,10 +460,10 @@ 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>
<Text color={t.color.accent}> </Text>
{line}
</>
}
@ -649,22 +649,22 @@ export const Thinking = memo(function Thinking({
{preview ? (
mode === 'full' ? (
lines.map((line, index) => (
<Text color={t.color.dim} key={index} wrap="wrap-trim">
<Text color={t.color.muted} key={index} wrap="wrap-trim">
{line || ' '}
{index === lines.length - 1 ? (
<StreamCursor color={t.color.dim} streaming={streaming} visible={active} />
<StreamCursor color={t.color.muted} streaming={streaming} visible={active} />
) : null}
</Text>
))
) : (
<Text color={t.color.dim} wrap="truncate-end">
<Text color={t.color.muted} wrap="truncate-end">
{preview}
<StreamCursor color={t.color.dim} streaming={streaming} visible={active} />
<StreamCursor color={t.color.muted} streaming={streaming} visible={active} />
</Text>
)
) : (
<Text color={t.color.dim}>
<StreamCursor color={t.color.dim} streaming={streaming} visible={active} />
<Text color={t.color.muted}>
<StreamCursor color={t.color.muted} streaming={streaming} visible={active} />
</Text>
)}
</Box>
@ -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}`,
@ -801,7 +801,7 @@ export const ToolTrail = memo(function ToolTrail({
if (parsed.detail) {
pushDetail({
color: parsed.mark === '✗' ? t.color.error : t.color.dim,
color: parsed.mark === '✗' ? t.color.error : t.color.muted,
content: parsed.detail,
dimColor: parsed.mark !== '✗',
key: `tr-${i}-d`
@ -815,9 +815,9 @@ 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` }],
details: [{ color: t.color.muted, content: 'drafting...', dimColor: true, key: `tr-${i}-d` }],
key: `tr-${i}`,
label
})
@ -827,12 +827,12 @@ export const ToolTrail = memo(function ToolTrail({
if (line === 'analyzing tool output…') {
pushDetail({
color: t.color.dim,
color: t.color.muted,
dimColor: true,
key: `tr-${i}`,
content: groups.length ? (
<>
<Spinner color={t.color.amber} variant="think" /> {line}
<Spinner color={t.color.accent} variant="think" /> {line}
</>
) : (
line
@ -842,20 +842,20 @@ export const ToolTrail = memo(function ToolTrail({
continue
}
meta.push({ color: t.color.dim, content: line, dimColor: true, key: `tr-${i}` })
meta.push({ color: t.color.muted, content: line, dimColor: true, key: `tr-${i}` })
}
for (const tool of tools) {
const label = formatToolCall(tool.name, tool.context || '')
groups.push({
color: t.color.cornsilk,
color: t.color.text,
key: tool.id,
label,
details: [],
content: (
<>
<Spinner color={t.color.amber} variant="tool" /> {label}
<Spinner color={t.color.accent} variant="tool" /> {label}
{tool.startedAt ? ` (${fmtElapsed(now - tool.startedAt)})` : ''}
</>
)
@ -864,7 +864,7 @@ export const ToolTrail = memo(function ToolTrail({
for (const item of activity.slice(-4)) {
const glyph = item.tone === 'error' ? '✗' : item.tone === 'warn' ? '!' : '·'
const color = item.tone === 'error' ? t.color.error : item.tone === 'warn' ? t.color.warn : t.color.dim
const color = item.tone === 'error' ? t.color.error : item.tone === 'warn' ? t.color.warn : t.color.muted
meta.push({ color, content: `${glyph} ${item.text}`, dimColor: item.tone === 'info', key: `a-${item.id}` })
}
@ -998,14 +998,14 @@ export const ToolTrail = memo(function ToolTrail({
}
}}
>
<Text color={t.color.dim} dim={!thinkingLive}>
<Text color={t.color.amber}>{openThinking ? '▾ ' : '▸ '}</Text>
<Text color={t.color.muted} dim={!thinkingLive}>
<Text color={t.color.accent}>{openThinking ? '▾ ' : '▸ '}</Text>
{thinkingLive ? (
<Text bold color={t.color.cornsilk}>
<Text bold color={t.color.text}>
Thinking
</Text>
) : (
<Text color={t.color.dim} dim>
<Text color={t.color.muted} dim>
Thinking
</Text>
)}
@ -1068,7 +1068,7 @@ export const ToolTrail = memo(function ToolTrail({
color={group.color}
content={
<>
<Text color={t.color.amber}> </Text>
<Text color={t.color.accent}> </Text>
{toolLabel(group)}
</>
}
@ -1182,7 +1182,7 @@ export const ToolTrail = memo(function ToolTrail({
color={t.color.statusFg}
content={
<>
<Text color={t.color.amber}>Σ </Text>
<Text color={t.color.accent}>Σ </Text>
{totalTokensLabel}
</>
}
@ -1192,7 +1192,7 @@ export const ToolTrail = memo(function ToolTrail({
) : null}
{outcome ? (
<Box marginTop={1}>
<Text color={t.color.dim} dim>
<Text color={t.color.muted} dim>
· {outcome}
</Text>
</Box>

View file

@ -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.muted
}
export const TodoPanel = memo(function TodoPanel({
@ -56,16 +56,16 @@ export const TodoPanel = memo(function TodoPanel({
return (
<Box flexDirection="column" marginBottom={1}>
<Box onClick={handleToggle}>
<Text color={t.color.dim}>
<Text color={t.color.amber}>{effectiveCollapsed ? '▸ ' : '▾ '}</Text>
<Text bold color={t.color.cornsilk}>
<Text color={t.color.muted}>
<Text color={t.color.accent}>{effectiveCollapsed ? '▸ ' : '▾ '}</Text>
<Text bold color={t.color.text}>
Todo
</Text>{' '}
<Text color={t.color.statusFg} dim>
({done}/{todos.length})
</Text>
{incomplete && pending > 0 && (
<Text color={t.color.dim} dim>
<Text color={t.color.muted} dim>
{' '}
· incomplete · {pending} still {pending === 1 ? 'pending' : 'pending/in_progress'}
</Text>