import { feature } from 'bun:bundle'; import * as React from 'react'; import { Box, Text } from '@anthropic/ink'; 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( 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, cacheHitRate, cacheThreshold, } = 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}%) {cacheHitRate !== undefined && cacheThreshold !== undefined && ( Cache hit rate: {cacheHitRate.toFixed(0)}% {cacheHitRate < cacheThreshold ? ` (below ${cacheThreshold}% threshold)` : ''} )} 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 ))} )} )} ); }