| import { Box, NoSelect, Text } from '@hermes/ink' |
| import { memo, type ReactNode, useEffect, useMemo, useState } from 'react' |
| import spinners, { type BrailleSpinnerName } from 'unicode-animations' |
|
|
| import { THINKING_COT_MAX } from '../config/limits.js' |
| import { |
| buildSubagentTree, |
| fmtCost, |
| fmtTokens, |
| formatSummary as formatSpawnSummary, |
| hotnessBucket, |
| peakHotness, |
| sparkline, |
| treeTotals, |
| widthByDepth |
| } from '../lib/subagentTree.js' |
| import { |
| compactPreview, |
| estimateTokensRough, |
| fmtK, |
| formatToolCall, |
| parseToolTrailResultLine, |
| pick, |
| thinkingPreview, |
| toolTrailLabel |
| } from '../lib/text.js' |
| import type { Theme } from '../theme.js' |
| import type { ActiveTool, ActivityItem, DetailsMode, SubagentNode, SubagentProgress, ThinkingMode } from '../types.js' |
|
|
| const THINK: BrailleSpinnerName[] = ['helix', 'breathe', 'orbit', 'dna', 'waverows', 'snake', 'pulse'] |
| const TOOL: BrailleSpinnerName[] = ['cascade', 'scan', 'diagswipe', 'fillsweep', 'rain', 'columns', 'sparkle'] |
|
|
| const fmtElapsed = (ms: number) => { |
| const sec = Math.max(0, ms) / 1000 |
|
|
| return sec < 10 ? `${sec.toFixed(1)}s` : `${Math.round(sec)}s` |
| } |
|
|
| type TreeBranch = 'mid' | 'last' |
| type TreeRails = readonly boolean[] |
|
|
| const nextTreeRails = (rails: TreeRails, branch: TreeBranch) => [...rails, branch === 'mid'] |
|
|
| const treeLead = (rails: TreeRails, branch: TreeBranch) => |
| `${rails.map(on => (on ? '│ ' : ' ')).join('')}${branch === 'mid' ? '├─ ' : '└─ '}` |
|
|
| |
|
|
| function TreeRow({ |
| branch, |
| children, |
| rails = [], |
| stemColor, |
| stemDim = true, |
| t |
| }: { |
| branch: TreeBranch |
| children: ReactNode |
| rails?: TreeRails |
| stemColor?: string |
| stemDim?: boolean |
| t: Theme |
| }) { |
| const lead = treeLead(rails, branch) |
|
|
| return ( |
| <Box> |
| <NoSelect flexShrink={0} fromLeftEdge width={lead.length}> |
| <Text color={stemColor ?? t.color.dim} dim={stemDim}> |
| {lead} |
| </Text> |
| </NoSelect> |
| <Box flexDirection="column" flexGrow={1}> |
| {children} |
| </Box> |
| </Box> |
| ) |
| } |
|
|
| function TreeTextRow({ |
| branch, |
| color, |
| content, |
| dimColor, |
| rails = [], |
| t, |
| wrap = 'wrap-trim' |
| }: { |
| branch: TreeBranch |
| color: string |
| content: ReactNode |
| dimColor?: boolean |
| rails?: TreeRails |
| t: Theme |
| wrap?: 'truncate-end' | 'wrap' | 'wrap-trim' |
| }) { |
| const text = dimColor ? ( |
| <Text color={color} dim wrap={wrap}> |
| {content} |
| </Text> |
| ) : ( |
| <Text color={color} wrap={wrap}> |
| {content} |
| </Text> |
| ) |
|
|
| return ( |
| <TreeRow branch={branch} rails={rails} t={t}> |
| {text} |
| </TreeRow> |
| ) |
| } |
|
|
| function TreeNode({ |
| branch, |
| children, |
| header, |
| open, |
| rails = [], |
| stemColor, |
| stemDim, |
| t |
| }: { |
| branch: TreeBranch |
| children?: (rails: boolean[]) => ReactNode |
| header: ReactNode |
| open: boolean |
| rails?: TreeRails |
| stemColor?: string |
| stemDim?: boolean |
| t: Theme |
| }) { |
| return ( |
| <Box flexDirection="column"> |
| <TreeRow branch={branch} rails={rails} stemColor={stemColor} stemDim={stemDim} t={t}> |
| {header} |
| </TreeRow> |
| {open ? children?.(nextTreeRails(rails, branch)) : null} |
| </Box> |
| ) |
| } |
|
|
| export function Spinner({ color, variant = 'think' }: { color: string; variant?: 'think' | 'tool' }) { |
| const spin = useMemo(() => { |
| const raw = spinners[pick(variant === 'tool' ? TOOL : THINK)] |
|
|
| return { ...raw, frames: raw.frames.map(f => [...f][0] ?? '⠀') } |
| }, [variant]) |
|
|
| const [frame, setFrame] = useState(0) |
|
|
| useEffect(() => { |
| setFrame(0) |
| }, [spin]) |
|
|
| useEffect(() => { |
| const id = setInterval(() => setFrame(f => (f + 1) % spin.frames.length), spin.interval) |
|
|
| return () => clearInterval(id) |
| }, [spin]) |
|
|
| return <Text color={color}>{spin.frames[frame]}</Text> |
| } |
|
|
| interface DetailRow { |
| color: string |
| content: ReactNode |
| dimColor?: boolean |
| key: string |
| } |
|
|
| function Detail({ |
| branch = 'last', |
| color, |
| content, |
| dimColor, |
| rails = [], |
| t |
| }: DetailRow & { branch?: TreeBranch; rails?: TreeRails; t: Theme }) { |
| return <TreeTextRow branch={branch} color={color} content={content} dimColor={dimColor} rails={rails} t={t} /> |
| } |
|
|
| function StreamCursor({ |
| color, |
| dimColor, |
| streaming = false, |
| visible = false |
| }: { |
| color: string |
| dimColor?: boolean |
| streaming?: boolean |
| visible?: boolean |
| }) { |
| const [on, setOn] = useState(true) |
|
|
| useEffect(() => { |
| if (!visible || !streaming) { |
| setOn(true) |
|
|
| return |
| } |
|
|
| const id = setInterval(() => setOn(v => !v), 420) |
|
|
| return () => clearInterval(id) |
| }, [streaming, visible]) |
|
|
| if (!visible) { |
| return null |
| } |
|
|
| return dimColor ? ( |
| <Text color={color} dim> |
| {streaming && on ? '▍' : ' '} |
| </Text> |
| ) : ( |
| <Text color={color}>{streaming && on ? '▍' : ' '}</Text> |
| ) |
| } |
|
|
| function Chevron({ |
| count, |
| onClick, |
| open, |
| suffix, |
| t, |
| title, |
| tone = 'dim' |
| }: { |
| count?: number |
| onClick: (deep?: boolean) => void |
| open: boolean |
| suffix?: string |
| t: Theme |
| title: string |
| tone?: 'dim' | 'error' | 'warn' |
| }) { |
| const color = tone === 'error' ? t.color.error : tone === 'warn' ? t.color.warn : t.color.dim |
|
|
| return ( |
| <Box onClick={(e: any) => onClick(!!e?.shiftKey || !!e?.ctrlKey)}> |
| <Text color={color} dim={tone === 'dim'}> |
| <Text color={t.color.amber}>{open ? '▾ ' : '▸ '}</Text> |
| {title} |
| {typeof count === 'number' ? ` (${count})` : ''} |
| {suffix ? ( |
| <Text color={t.color.statusFg} dim> |
| {' '} |
| {suffix} |
| </Text> |
| ) : null} |
| </Text> |
| </Box> |
| ) |
| } |
|
|
| 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 idx = hotnessBucket(node.aggregate.hotness, peak, palette.length) |
|
|
| |
| |
| if (idx < 2) { |
| return undefined |
| } |
|
|
| return palette[idx] |
| } |
|
|
| function SubagentAccordion({ |
| branch, |
| expanded, |
| node, |
| peak, |
| rails = [], |
| t |
| }: { |
| branch: TreeBranch |
| expanded: boolean |
| node: SubagentNode |
| peak: number |
| rails?: TreeRails |
| t: Theme |
| }) { |
| const [open, setOpen] = useState(expanded) |
| const [deep, setDeep] = useState(expanded) |
| const [openThinking, setOpenThinking] = useState(expanded) |
| const [openTools, setOpenTools] = useState(expanded) |
| const [openNotes, setOpenNotes] = useState(expanded) |
| const [openKids, setOpenKids] = useState(expanded) |
|
|
| useEffect(() => { |
| if (!expanded) { |
| return |
| } |
|
|
| setOpen(true) |
| setDeep(true) |
| setOpenThinking(true) |
| setOpenTools(true) |
| setOpenNotes(true) |
| setOpenKids(true) |
| }, [expanded]) |
|
|
| const expandAll = () => { |
| setOpen(true) |
| setDeep(true) |
| setOpenThinking(true) |
| setOpenTools(true) |
| setOpenNotes(true) |
| setOpenKids(true) |
| } |
|
|
| const item = node.item |
| const children = node.children |
| const aggregate = node.aggregate |
|
|
| const statusTone: 'dim' | 'error' | 'warn' = |
| item.status === 'failed' ? 'error' : item.status === 'interrupted' ? 'warn' : 'dim' |
|
|
| const prefix = item.taskCount > 1 ? `[${item.index + 1}/${item.taskCount}] ` : '' |
| const goalLabel = item.goal || `Subagent ${item.index + 1}` |
| const title = `${prefix}${open ? goalLabel : compactPreview(goalLabel, 60)}` |
| const summary = compactPreview((item.summary || '').replace(/\s+/g, ' ').trim(), 72) |
|
|
| |
| |
| const statusLabel = item.status === 'queued' ? 'queued' : item.status === 'running' ? 'running' : String(item.status) |
|
|
| const rollupBits: string[] = [statusLabel] |
|
|
| if (item.durationSeconds) { |
| rollupBits.push(fmtElapsed(item.durationSeconds * 1000)) |
| } |
|
|
| const localTools = item.toolCount ?? 0 |
| const subtreeTools = aggregate.totalTools - localTools |
|
|
| if (localTools > 0) { |
| rollupBits.push(`${localTools} tool${localTools === 1 ? '' : 's'}`) |
| } |
|
|
| const localTokens = (item.inputTokens ?? 0) + (item.outputTokens ?? 0) |
|
|
| if (localTokens > 0) { |
| rollupBits.push(`${fmtTokens(localTokens)} tok`) |
| } |
|
|
| const localCost = item.costUsd ?? 0 |
|
|
| if (localCost > 0) { |
| rollupBits.push(fmtCost(localCost)) |
| } |
|
|
| const filesLocal = (item.filesWritten?.length ?? 0) + (item.filesRead?.length ?? 0) |
|
|
| if (filesLocal > 0) { |
| rollupBits.push(`⎘${filesLocal}`) |
| } |
|
|
| if (children.length > 0) { |
| rollupBits.push(`${aggregate.descendantCount}↓`) |
|
|
| if (subtreeTools > 0) { |
| rollupBits.push(`+${subtreeTools}t sub`) |
| } |
|
|
| const subCost = aggregate.costUsd - localCost |
|
|
| if (subCost >= 0.01) { |
| rollupBits.push(`+${fmtCost(subCost)} sub`) |
| } |
|
|
| if (aggregate.activeCount > 0 && item.status !== 'running') { |
| rollupBits.push(`⚡${aggregate.activeCount}`) |
| } |
| } |
|
|
| const suffix = rollupBits.join(' · ') |
|
|
| const thinkingText = item.thinking.join('\n') |
| const hasThinking = Boolean(thinkingText) |
| const hasTools = item.tools.length > 0 |
| const noteRows = [...(summary ? [summary] : []), ...item.notes] |
| const hasNotes = noteRows.length > 0 |
| const showChildren = expanded || deep |
| const noteColor = statusTone === 'error' ? t.color.error : statusTone === 'warn' ? t.color.warn : t.color.dim |
|
|
| const sections: { |
| header: ReactNode |
| key: string |
| open: boolean |
| render: (rails: boolean[]) => ReactNode |
| }[] = [] |
|
|
| if (hasThinking) { |
| sections.push({ |
| header: ( |
| <Chevron |
| count={item.thinking.length} |
| onClick={shift => { |
| if (shift) { |
| expandAll() |
| } else { |
| setOpenThinking(v => !v) |
| } |
| }} |
| open={showChildren || openThinking} |
| t={t} |
| title="Thinking" |
| /> |
| ), |
| key: 'thinking', |
| open: showChildren || openThinking, |
| render: childRails => ( |
| <Thinking |
| active={item.status === 'running'} |
| branch="last" |
| mode="full" |
| rails={childRails} |
| reasoning={thinkingText} |
| streaming={item.status === 'running'} |
| t={t} |
| /> |
| ) |
| }) |
| } |
|
|
| if (hasTools) { |
| sections.push({ |
| header: ( |
| <Chevron |
| count={item.tools.length} |
| onClick={shift => { |
| if (shift) { |
| expandAll() |
| } else { |
| setOpenTools(v => !v) |
| } |
| }} |
| open={showChildren || openTools} |
| t={t} |
| title="Tool calls" |
| /> |
| ), |
| key: 'tools', |
| open: showChildren || openTools, |
| render: childRails => ( |
| <Box flexDirection="column"> |
| {item.tools.map((line, index) => ( |
| <TreeTextRow |
| branch={index === item.tools.length - 1 ? 'last' : 'mid'} |
| color={t.color.cornsilk} |
| content={ |
| <> |
| <Text color={t.color.amber}>● </Text> |
| {line} |
| </> |
| } |
| key={`${item.id}-tool-${index}`} |
| rails={childRails} |
| t={t} |
| /> |
| ))} |
| </Box> |
| ) |
| }) |
| } |
|
|
| if (hasNotes) { |
| sections.push({ |
| header: ( |
| <Chevron |
| count={noteRows.length} |
| onClick={shift => { |
| if (shift) { |
| expandAll() |
| } else { |
| setOpenNotes(v => !v) |
| } |
| }} |
| open={showChildren || openNotes} |
| t={t} |
| title="Progress" |
| tone={statusTone} |
| /> |
| ), |
| key: 'notes', |
| open: showChildren || openNotes, |
| render: childRails => ( |
| <Box flexDirection="column"> |
| {noteRows.map((line, index) => ( |
| <TreeTextRow |
| branch={index === noteRows.length - 1 ? 'last' : 'mid'} |
| color={noteColor} |
| content={line} |
| dimColor={statusTone === 'dim'} |
| key={`${item.id}-note-${index}`} |
| rails={childRails} |
| t={t} |
| /> |
| ))} |
| </Box> |
| ) |
| }) |
| } |
|
|
| if (children.length > 0) { |
| |
| |
| sections.push({ |
| header: ( |
| <Chevron |
| count={children.length} |
| onClick={shift => { |
| if (shift) { |
| expandAll() |
| } else { |
| setOpenKids(v => !v) |
| } |
| }} |
| open={showChildren || openKids} |
| suffix={`d${item.depth + 1} · ${aggregate.descendantCount} total`} |
| t={t} |
| title="Spawned" |
| /> |
| ), |
| key: 'subagents', |
| open: showChildren || openKids, |
| render: childRails => ( |
| <Box flexDirection="column"> |
| {children.map((child, i) => ( |
| <SubagentAccordion |
| branch={i === children.length - 1 ? 'last' : 'mid'} |
| expanded={expanded || deep} |
| key={child.item.id} |
| node={child} |
| peak={peak} |
| rails={childRails} |
| t={t} |
| /> |
| ))} |
| </Box> |
| ) |
| }) |
| } |
|
|
| |
| |
| const stem = heatColor(node, peak, t) |
|
|
| return ( |
| <TreeNode |
| branch={branch} |
| header={ |
| <Chevron |
| onClick={shift => { |
| if (shift) { |
| expandAll() |
| |
| return |
| } |
| |
| setOpen(v => { |
| if (!v) { |
| setDeep(false) |
| } |
| |
| return !v |
| }) |
| }} |
| open={open} |
| suffix={suffix} |
| t={t} |
| title={title} |
| tone={statusTone} |
| /> |
| } |
| open={open} |
| rails={rails} |
| stemColor={stem} |
| stemDim={stem == null} |
| t={t} |
| > |
| {childRails => ( |
| <Box flexDirection="column"> |
| {sections.map((section, index) => ( |
| <TreeNode |
| branch={index === sections.length - 1 ? 'last' : 'mid'} |
| header={section.header} |
| key={`${item.id}-${section.key}`} |
| open={section.open} |
| rails={childRails} |
| t={t} |
| > |
| {section.render} |
| </TreeNode> |
| ))} |
| </Box> |
| )} |
| </TreeNode> |
| ) |
| } |
|
|
| |
|
|
| export const Thinking = memo(function Thinking({ |
| active = false, |
| branch = 'last', |
| mode = 'truncated', |
| rails = [], |
| reasoning, |
| streaming = false, |
| t |
| }: { |
| active?: boolean |
| branch?: TreeBranch |
| mode?: ThinkingMode |
| rails?: TreeRails |
| reasoning: string |
| streaming?: boolean |
| t: Theme |
| }) { |
| const preview = useMemo(() => thinkingPreview(reasoning, mode, THINKING_COT_MAX), [mode, reasoning]) |
| const lines = useMemo(() => preview.split('\n').map(line => line.replace(/\t/g, ' ')), [preview]) |
|
|
| if (!preview && !active) { |
| return null |
| } |
|
|
| return ( |
| <TreeRow branch={branch} rails={rails} t={t}> |
| <Box flexDirection="column" flexGrow={1}> |
| {preview ? ( |
| mode === 'full' ? ( |
| lines.map((line, index) => ( |
| <Text color={t.color.dim} dim key={index} wrap="wrap-trim"> |
| {line || ' '} |
| {index === lines.length - 1 ? ( |
| <StreamCursor color={t.color.dim} dimColor streaming={streaming} visible={active} /> |
| ) : null} |
| </Text> |
| )) |
| ) : ( |
| <Text color={t.color.dim} dim wrap="truncate-end"> |
| {preview} |
| <StreamCursor color={t.color.dim} dimColor streaming={streaming} visible={active} /> |
| </Text> |
| ) |
| ) : ( |
| <Text color={t.color.dim} dim> |
| <StreamCursor color={t.color.dim} dimColor streaming={streaming} visible={active} /> |
| </Text> |
| )} |
| </Box> |
| </TreeRow> |
| ) |
| }) |
|
|
| |
|
|
| interface Group { |
| color: string |
| content: ReactNode |
| details: DetailRow[] |
| key: string |
| label: string |
| } |
|
|
| export const ToolTrail = memo(function ToolTrail({ |
| busy = false, |
| detailsMode = 'collapsed', |
| outcome = '', |
| reasoningActive = false, |
| reasoning = '', |
| reasoningTokens, |
| reasoningStreaming = false, |
| subagents = [], |
| t, |
| tools = [], |
| toolTokens, |
| trail = [], |
| activity = [] |
| }: { |
| busy?: boolean |
| detailsMode?: DetailsMode |
| outcome?: string |
| reasoningActive?: boolean |
| reasoning?: string |
| reasoningTokens?: number |
| reasoningStreaming?: boolean |
| subagents?: SubagentProgress[] |
| t: Theme |
| tools?: ActiveTool[] |
| toolTokens?: number |
| trail?: string[] |
| activity?: ActivityItem[] |
| }) { |
| const [now, setNow] = useState(() => Date.now()) |
| const [openThinking, setOpenThinking] = useState(false) |
| const [openTools, setOpenTools] = useState(false) |
| const [openSubagents, setOpenSubagents] = useState(false) |
| const [deepSubagents, setDeepSubagents] = useState(false) |
| const [openMeta, setOpenMeta] = useState(false) |
|
|
| useEffect(() => { |
| if (!tools.length || (detailsMode === 'collapsed' && !openTools)) { |
| return |
| } |
|
|
| const id = setInterval(() => setNow(Date.now()), 500) |
|
|
| return () => clearInterval(id) |
| }, [detailsMode, openTools, tools.length]) |
|
|
| useEffect(() => { |
| if (detailsMode === 'expanded') { |
| setOpenThinking(true) |
| setOpenTools(true) |
| setOpenSubagents(true) |
| setOpenMeta(true) |
| } |
|
|
| if (detailsMode === 'hidden') { |
| setOpenThinking(false) |
| setOpenTools(false) |
| setOpenSubagents(false) |
| setOpenMeta(false) |
| } |
| }, [detailsMode]) |
|
|
| const cot = useMemo(() => thinkingPreview(reasoning, 'full', THINKING_COT_MAX), [reasoning]) |
|
|
| |
| |
| |
| const spawnTree = useMemo(() => buildSubagentTree(subagents), [subagents]) |
| const spawnPeak = useMemo(() => peakHotness(spawnTree), [spawnTree]) |
| const spawnTotals = useMemo(() => treeTotals(spawnTree), [spawnTree]) |
| const spawnWidths = useMemo(() => widthByDepth(spawnTree), [spawnTree]) |
| const spawnSpark = useMemo(() => sparkline(spawnWidths), [spawnWidths]) |
| const spawnSummaryLabel = useMemo(() => formatSpawnSummary(spawnTotals), [spawnTotals]) |
|
|
| if ( |
| !busy && |
| !trail.length && |
| !tools.length && |
| !subagents.length && |
| !activity.length && |
| !cot && |
| !reasoningActive && |
| !outcome |
| ) { |
| return null |
| } |
|
|
| |
|
|
| const groups: Group[] = [] |
| const meta: DetailRow[] = [] |
| const pushDetail = (row: DetailRow) => (groups.at(-1)?.details ?? meta).push(row) |
|
|
| for (const [i, line] of trail.entries()) { |
| const parsed = parseToolTrailResultLine(line) |
|
|
| if (parsed) { |
| groups.push({ |
| color: parsed.mark === '✗' ? t.color.error : t.color.cornsilk, |
| content: parsed.detail ? parsed.call : `${parsed.call} ${parsed.mark}`, |
| details: [], |
| key: `tr-${i}`, |
| label: parsed.call |
| }) |
|
|
| if (parsed.detail) { |
| pushDetail({ |
| color: parsed.mark === '✗' ? t.color.error : t.color.dim, |
| content: parsed.detail, |
| dimColor: parsed.mark !== '✗', |
| key: `tr-${i}-d` |
| }) |
| } |
|
|
| continue |
| } |
|
|
| if (line.startsWith('drafting ')) { |
| const label = toolTrailLabel(line.slice(9).replace(/…$/, '').trim()) |
|
|
| groups.push({ |
| color: t.color.cornsilk, |
| content: label, |
| details: [{ color: t.color.dim, content: 'drafting...', dimColor: true, key: `tr-${i}-d` }], |
| key: `tr-${i}`, |
| label |
| }) |
|
|
| continue |
| } |
|
|
| if (line === 'analyzing tool output…') { |
| pushDetail({ |
| color: t.color.dim, |
| dimColor: true, |
| key: `tr-${i}`, |
| content: groups.length ? ( |
| <> |
| <Spinner color={t.color.amber} variant="think" /> {line} |
| </> |
| ) : ( |
| line |
| ) |
| }) |
|
|
| continue |
| } |
|
|
| meta.push({ color: t.color.dim, 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, |
| key: tool.id, |
| label, |
| details: [], |
| content: ( |
| <> |
| <Spinner color={t.color.amber} variant="tool" /> {label} |
| {tool.startedAt ? ` (${fmtElapsed(now - tool.startedAt)})` : ''} |
| </> |
| ) |
| }) |
| } |
|
|
| 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 |
| meta.push({ color, content: `${glyph} ${item.text}`, dimColor: item.tone === 'info', key: `a-${item.id}` }) |
| } |
|
|
| |
|
|
| const hasTools = groups.length > 0 |
| const hasSubagents = subagents.length > 0 |
| const hasMeta = meta.length > 0 |
| const hasThinking = !!cot || reasoningActive || busy |
| const thinkingLive = reasoningActive || reasoningStreaming |
|
|
| const tokenCount = |
| reasoningTokens && reasoningTokens > 0 ? reasoningTokens : reasoning ? estimateTokensRough(reasoning) : 0 |
|
|
| const toolTokenCount = toolTokens ?? 0 |
| const totalTokenCount = tokenCount + toolTokenCount |
| const thinkingTokensLabel = tokenCount > 0 ? `~${fmtK(tokenCount)} tokens` : null |
|
|
| const toolTokensLabel = toolTokens !== undefined && toolTokens > 0 ? `~${fmtK(toolTokens)} tokens` : undefined |
|
|
| const totalTokensLabel = tokenCount > 0 && toolTokenCount > 0 ? `~${fmtK(totalTokenCount)} total` : null |
| const delegateGroups = groups.filter(g => g.label.startsWith('Delegate Task')) |
| const inlineDelegateKey = hasSubagents && delegateGroups.length === 1 ? delegateGroups[0]!.key : null |
|
|
| |
|
|
| if (detailsMode === 'hidden') { |
| const alerts = activity.filter(i => i.tone !== 'info').slice(-2) |
|
|
| return alerts.length ? ( |
| <Box flexDirection="column"> |
| {alerts.map(i => ( |
| <Text color={i.tone === 'error' ? t.color.error : t.color.warn} key={`ha-${i.id}`}> |
| {i.tone === 'error' ? '✗' : '!'} {i.text} |
| </Text> |
| ))} |
| </Box> |
| ) : null |
| } |
|
|
| |
|
|
| const expandAll = () => { |
| setOpenThinking(true) |
| setOpenTools(true) |
| setOpenSubagents(true) |
| setDeepSubagents(true) |
| setOpenMeta(true) |
| } |
|
|
| const metaTone: 'dim' | 'error' | 'warn' = activity.some(i => i.tone === 'error') |
| ? 'error' |
| : activity.some(i => i.tone === 'warn') |
| ? 'warn' |
| : 'dim' |
|
|
| const renderSubagentList = (rails: boolean[]) => ( |
| <Box flexDirection="column"> |
| {spawnTree.map((node, index) => ( |
| <SubagentAccordion |
| branch={index === spawnTree.length - 1 ? 'last' : 'mid'} |
| expanded={detailsMode === 'expanded' || deepSubagents} |
| key={node.item.id} |
| node={node} |
| peak={spawnPeak} |
| rails={rails} |
| t={t} |
| /> |
| ))} |
| </Box> |
| ) |
|
|
| const sections: { |
| header: ReactNode |
| key: string |
| open: boolean |
| render: (rails: boolean[]) => ReactNode |
| }[] = [] |
|
|
| if (hasThinking) { |
| sections.push({ |
| header: ( |
| <Box |
| onClick={(e: any) => { |
| if (e?.shiftKey || e?.ctrlKey) { |
| expandAll() |
| } else { |
| setOpenThinking(v => !v) |
| } |
| }} |
| > |
| <Text color={t.color.dim} dim={!thinkingLive}> |
| <Text color={t.color.amber}>{detailsMode === 'expanded' || openThinking ? '▾ ' : '▸ '}</Text> |
| {thinkingLive ? ( |
| <Text bold color={t.color.cornsilk}> |
| Thinking |
| </Text> |
| ) : ( |
| <Text color={t.color.dim} dim> |
| Thinking |
| </Text> |
| )} |
| {thinkingTokensLabel ? ( |
| <Text color={t.color.statusFg} dim> |
| {' '} |
| {thinkingTokensLabel} |
| </Text> |
| ) : null} |
| </Text> |
| </Box> |
| ), |
| key: 'thinking', |
| open: detailsMode === 'expanded' || openThinking, |
| render: rails => ( |
| <Thinking |
| active={reasoningActive} |
| branch="last" |
| mode="full" |
| rails={rails} |
| reasoning={busy ? reasoning : cot} |
| streaming={busy && reasoningStreaming} |
| t={t} |
| /> |
| ) |
| }) |
| } |
|
|
| if (hasTools) { |
| sections.push({ |
| header: ( |
| <Chevron |
| count={groups.length} |
| onClick={shift => { |
| if (shift) { |
| expandAll() |
| } else { |
| setOpenTools(v => !v) |
| } |
| }} |
| open={detailsMode === 'expanded' || openTools} |
| suffix={toolTokensLabel} |
| t={t} |
| title="Tool calls" |
| /> |
| ), |
| key: 'tools', |
| open: detailsMode === 'expanded' || openTools, |
| render: rails => ( |
| <Box flexDirection="column"> |
| {groups.map((group, index) => { |
| const branch: TreeBranch = index === groups.length - 1 ? 'last' : 'mid' |
| const childRails = nextTreeRails(rails, branch) |
| const hasInlineSubagents = inlineDelegateKey === group.key |
| |
| return ( |
| <Box flexDirection="column" key={group.key}> |
| <TreeTextRow |
| branch={branch} |
| color={group.color} |
| content={ |
| <> |
| <Text color={t.color.amber}>● </Text> |
| {group.content} |
| </> |
| } |
| rails={rails} |
| t={t} |
| /> |
| {group.details.map((detail, detailIndex) => ( |
| <Detail |
| {...detail} |
| branch={detailIndex === group.details.length - 1 && !hasInlineSubagents ? 'last' : 'mid'} |
| key={detail.key} |
| rails={childRails} |
| t={t} |
| /> |
| ))} |
| {hasInlineSubagents ? renderSubagentList(childRails) : null} |
| </Box> |
| ) |
| })} |
| </Box> |
| ) |
| }) |
| } |
|
|
| if (hasSubagents && !inlineDelegateKey) { |
| |
| |
| const suffix = spawnSpark ? `${spawnSummaryLabel} ${spawnSpark} (/agents)` : `${spawnSummaryLabel} (/agents)` |
|
|
| sections.push({ |
| header: ( |
| <Chevron |
| count={spawnTotals.descendantCount} |
| onClick={shift => { |
| if (shift) { |
| expandAll() |
| setDeepSubagents(true) |
| } else { |
| setOpenSubagents(v => !v) |
| setDeepSubagents(false) |
| } |
| }} |
| open={detailsMode === 'expanded' || openSubagents} |
| suffix={suffix} |
| t={t} |
| title="Spawn tree" |
| /> |
| ), |
| key: 'subagents', |
| open: detailsMode === 'expanded' || openSubagents, |
| render: renderSubagentList |
| }) |
| } |
|
|
| if (hasMeta) { |
| sections.push({ |
| header: ( |
| <Chevron |
| count={meta.length} |
| onClick={shift => { |
| if (shift) { |
| expandAll() |
| } else { |
| setOpenMeta(v => !v) |
| } |
| }} |
| open={detailsMode === 'expanded' || openMeta} |
| t={t} |
| title="Activity" |
| tone={metaTone} |
| /> |
| ), |
| key: 'meta', |
| open: detailsMode === 'expanded' || openMeta, |
| render: rails => ( |
| <Box flexDirection="column"> |
| {meta.map((row, index) => ( |
| <TreeTextRow |
| branch={index === meta.length - 1 ? 'last' : 'mid'} |
| color={row.color} |
| content={row.content} |
| dimColor={row.dimColor} |
| key={row.key} |
| rails={rails} |
| t={t} |
| /> |
| ))} |
| </Box> |
| ) |
| }) |
| } |
|
|
| const topCount = sections.length + (totalTokensLabel ? 1 : 0) |
|
|
| return ( |
| <Box flexDirection="column"> |
| {sections.map((section, index) => ( |
| <TreeNode |
| branch={index === topCount - 1 ? 'last' : 'mid'} |
| header={section.header} |
| key={section.key} |
| open={section.open} |
| t={t} |
| > |
| {section.render} |
| </TreeNode> |
| ))} |
| {totalTokensLabel ? ( |
| <TreeTextRow |
| branch="last" |
| color={t.color.statusFg} |
| content={ |
| <> |
| <Text color={t.color.amber}>Σ </Text> |
| {totalTokensLabel} |
| </> |
| } |
| dimColor |
| t={t} |
| /> |
| ) : null} |
| {outcome ? ( |
| <Box marginTop={1}> |
| <Text color={t.color.dim} dim> |
| · {outcome} |
| </Text> |
| </Box> |
| ) : null} |
| </Box> |
| ) |
| }) |
|
|