import { feature } from 'bun:bundle' import * as React from 'react' import { Box, Text } from '../ink.js' import type { ContextData } from '../utils/analyzeContext.js' import { generateContextSuggestions } from '../utils/contextSuggestions.js' import { getDisplayPath } from '../utils/file.js' import { formatTokens } from '../utils/format.js' import { getSourceDisplayName, type SettingSource, } from '../utils/settings/constants.js' import { plural } from '../utils/stringUtils.js' import { ContextSuggestions } from './ContextSuggestions.js' const RESERVED_CATEGORY_NAME = 'Autocompact buffer' /** * One-liner for the legend header showing what context-collapse has done. * Returns null when nothing's summarized/staged so we don't add visual * noise in the common case. This is the one place a user can see that * their context was rewritten — the placeholders are isMeta * and don't appear in the conversation view. */ function CollapseStatus(): React.ReactNode { if (feature('CONTEXT_COLLAPSE')) { /* eslint-disable @typescript-eslint/no-require-imports */ const { getStats, isContextCollapseEnabled } = require('../services/contextCollapse/index.js') as typeof import('../services/contextCollapse/index.js') /* eslint-enable @typescript-eslint/no-require-imports */ if (!isContextCollapseEnabled()) return null const s = getStats() const { health: h } = s const parts: string[] = [] if (s.collapsedSpans > 0) { parts.push( `${s.collapsedSpans} ${plural(s.collapsedSpans, 'span')} summarized (${s.collapsedMessages} msgs)`, ) } if (s.stagedSpans > 0) parts.push(`${s.stagedSpans} staged`) const summary = parts.length > 0 ? parts.join(', ') : h.totalSpawns > 0 ? `${h.totalSpawns} ${plural(h.totalSpawns, 'spawn')}, nothing staged yet` : 'waiting for first trigger' let line2: React.ReactNode = null if (h.totalErrors > 0) { line2 = ( Collapse errors: {h.totalErrors}/{h.totalSpawns} spawns failed {h.lastError ? ` (last: ${h.lastError.slice(0, 60)})` : ''} ) } else if (h.emptySpawnWarningEmitted) { line2 = ( Collapse idle: {h.totalEmptySpawns} consecutive empty runs ) } return ( <> Context strategy: collapse ({summary}) {line2} ) } return null } // Order for displaying source groups: Project > User > Managed > Plugin > Built-in const SOURCE_DISPLAY_ORDER = [ 'Project', 'User', 'Managed', 'Plugin', 'Built-in', ] /** Group items by source type for display, sorted by tokens descending within each group */ function groupBySource< T extends { source: SettingSource | 'plugin' | 'built-in'; tokens: number }, >(items: T[]): Map { const groups = new Map() for (const item of items) { const key = getSourceDisplayName(item.source) const existing = groups.get(key) || [] existing.push(item) groups.set(key, existing) } // Sort each group by tokens descending for (const [key, group] of groups.entries()) { groups.set( key, group.sort((a, b) => b.tokens - a.tokens), ) } // Return groups in consistent order const orderedGroups = new Map() for (const source of SOURCE_DISPLAY_ORDER) { const group = groups.get(source) if (group) { orderedGroups.set(source, group) } } return orderedGroups } interface Props { data: ContextData } export function ContextVisualization({ data }: Props): React.ReactNode { const { categories, totalTokens, rawMaxTokens, percentage, gridRows, model, memoryFiles, mcpTools, deferredBuiltinTools = [], systemTools, systemPromptSections, agents, skills, messageBreakdown, } = data // Filter out categories with 0 tokens for the legend, and exclude Free space, Autocompact buffer, and deferred const visibleCategories = categories.filter( cat => cat.tokens > 0 && cat.name !== 'Free space' && cat.name !== RESERVED_CATEGORY_NAME && !cat.isDeferred, ) // Check if MCP tools are deferred (loaded on-demand via tool search) const hasDeferredMcpTools = categories.some( cat => cat.isDeferred && cat.name.includes('MCP'), ) // Check if builtin tools are deferred const hasDeferredBuiltinTools = deferredBuiltinTools.length > 0 const autocompactCategory = categories.find( cat => cat.name === RESERVED_CATEGORY_NAME, ) return ( Context Usage {/* Fixed size grid */} {gridRows.map((row, rowIndex) => ( {row.map((square, colIndex) => { if (square.categoryName === 'Free space') { return ( {'⛶ '} ) } if (square.categoryName === RESERVED_CATEGORY_NAME) { return ( {'⛝ '} ) } return ( {square.squareFullness >= 0.7 ? '⛁ ' : '⛀ '} ) })} ))} {/* Legend to the right */} {model} · {formatTokens(totalTokens)}/{formatTokens(rawMaxTokens)}{' '} tokens ({percentage}%) Estimated usage by category {visibleCategories.map((cat, index) => { const tokenDisplay = formatTokens(cat.tokens) // Show "N/A" for deferred categories since they don't count toward context const percentDisplay = cat.isDeferred ? 'N/A' : `${((cat.tokens / rawMaxTokens) * 100).toFixed(1)}%` const isReserved = cat.name === RESERVED_CATEGORY_NAME const displayName = cat.name // Deferred categories don't appear in grid, so show blank instead of symbol const symbol = cat.isDeferred ? ' ' : isReserved ? '⛝' : '⛁' return ( {symbol} {displayName}: {tokenDisplay} tokens ({percentDisplay}) ) })} {(categories.find(c => c.name === 'Free space')?.tokens ?? 0) > 0 && ( Free space: {formatTokens( categories.find(c => c.name === 'Free space')?.tokens || 0, )}{' '} ( {( ((categories.find(c => c.name === 'Free space')?.tokens || 0) / rawMaxTokens) * 100 ).toFixed(1)} %) )} {autocompactCategory && autocompactCategory.tokens > 0 && ( {autocompactCategory.name}: {formatTokens(autocompactCategory.tokens)} tokens ( {((autocompactCategory.tokens / rawMaxTokens) * 100).toFixed(1)} %) )} {mcpTools.length > 0 && ( MCP tools {' '} · /mcp{hasDeferredMcpTools ? ' (loaded on-demand)' : ''} {/* Show loaded tools first */} {mcpTools.some(t => t.isLoaded) && ( Loaded {mcpTools .filter(t => t.isLoaded) .map((tool, i) => ( └ {tool.name}: {formatTokens(tool.tokens)} tokens ))} )} {/* Show available (deferred) tools */} {hasDeferredMcpTools && mcpTools.some(t => !t.isLoaded) && ( Available {mcpTools .filter(t => !t.isLoaded) .map((tool, i) => ( └ {tool.name} ))} )} {/* Show all tools normally when not deferred */} {!hasDeferredMcpTools && mcpTools.map((tool, i) => ( └ {tool.name}: {formatTokens(tool.tokens)} tokens ))} )} {/* Show builtin tools: always-loaded + deferred (ant-only) */} {((systemTools && systemTools.length > 0) || hasDeferredBuiltinTools) && process.env.USER_TYPE === 'ant' && ( [ANT-ONLY] System tools {hasDeferredBuiltinTools && ( (some loaded on-demand) )} {/* Always-loaded + deferred-but-loaded tools */} Loaded {systemTools?.map((tool, i) => ( └ {tool.name}: {formatTokens(tool.tokens)} tokens ))} {deferredBuiltinTools .filter(t => t.isLoaded) .map((tool, i) => ( └ {tool.name}: {formatTokens(tool.tokens)} tokens ))} {/* Deferred (not yet loaded) tools */} {hasDeferredBuiltinTools && deferredBuiltinTools.some(t => !t.isLoaded) && ( Available {deferredBuiltinTools .filter(t => !t.isLoaded) .map((tool, i) => ( └ {tool.name} ))} )} )} {systemPromptSections && systemPromptSections.length > 0 && process.env.USER_TYPE === 'ant' && ( [ANT-ONLY] System prompt sections {systemPromptSections.map((section, i) => ( └ {section.name}: {formatTokens(section.tokens)} tokens ))} )} {agents.length > 0 && ( Custom agents · /agents {Array.from(groupBySource(agents).entries()).map( ([sourceDisplay, sourceAgents]) => ( {sourceDisplay} {sourceAgents.map((agent, i) => ( └ {agent.agentType}: {formatTokens(agent.tokens)} tokens ))} ), )} )} {memoryFiles.length > 0 && ( Memory files · /memory {memoryFiles.map((file, i) => ( └ {getDisplayPath(file.path)}: {formatTokens(file.tokens)} tokens ))} )} {skills && skills.tokens > 0 && ( Skills · /skills {Array.from(groupBySource(skills.skillFrontmatter).entries()).map( ([sourceDisplay, sourceSkills]) => ( {sourceDisplay} {sourceSkills.map((skill, i) => ( └ {skill.name}: {formatTokens(skill.tokens)} tokens ))} ), )} )} {messageBreakdown && process.env.USER_TYPE === 'ant' && ( [ANT-ONLY] Message breakdown Tool calls: {formatTokens(messageBreakdown.toolCallTokens)} tokens Tool results: {formatTokens(messageBreakdown.toolResultTokens)} tokens Attachments: {formatTokens(messageBreakdown.attachmentTokens)} tokens Assistant messages (non-tool): {formatTokens(messageBreakdown.assistantMessageTokens)} tokens User messages (non-tool-result): {formatTokens(messageBreakdown.userMessageTokens)} tokens {messageBreakdown.toolCallsByType.length > 0 && ( [ANT-ONLY] Top tools {messageBreakdown.toolCallsByType.slice(0, 5).map((tool, i) => ( └ {tool.name}: calls {formatTokens(tool.callTokens)}, results{' '} {formatTokens(tool.resultTokens)} ))} )} {messageBreakdown.attachmentsByType.length > 0 && ( [ANT-ONLY] Top attachments {messageBreakdown.attachmentsByType .slice(0, 5) .map((attachment, i) => ( └ {attachment.name}: {formatTokens(attachment.tokens)} tokens ))} )} )} ) }