source dump of claude code
at main 325 lines 11 kB view raw
1import { feature } from 'bun:bundle' 2import { microcompactMessages } from '../../services/compact/microCompact.js' 3import type { AppState } from '../../state/AppStateStore.js' 4import type { Tools, ToolUseContext } from '../../Tool.js' 5import type { AgentDefinitionsResult } from '../../tools/AgentTool/loadAgentsDir.js' 6import type { Message } from '../../types/message.js' 7import { 8 analyzeContextUsage, 9 type ContextData, 10} from '../../utils/analyzeContext.js' 11import { formatTokens } from '../../utils/format.js' 12import { getMessagesAfterCompactBoundary } from '../../utils/messages.js' 13import { getSourceDisplayName } from '../../utils/settings/constants.js' 14import { plural } from '../../utils/stringUtils.js' 15 16/** 17 * Shared data-collection path for `/context` (slash command) and the SDK 18 * `get_context_usage` control request. Mirrors query.ts's pre-API transforms 19 * (compact boundary, projectView, microcompact) so the token count reflects 20 * what the model actually sees. 21 */ 22type CollectContextDataInput = { 23 messages: Message[] 24 getAppState: () => AppState 25 options: { 26 mainLoopModel: string 27 tools: Tools 28 agentDefinitions: AgentDefinitionsResult 29 customSystemPrompt?: string 30 appendSystemPrompt?: string 31 } 32} 33 34export async function collectContextData( 35 context: CollectContextDataInput, 36): Promise<ContextData> { 37 const { 38 messages, 39 getAppState, 40 options: { 41 mainLoopModel, 42 tools, 43 agentDefinitions, 44 customSystemPrompt, 45 appendSystemPrompt, 46 }, 47 } = context 48 49 let apiView = getMessagesAfterCompactBoundary(messages) 50 if (feature('CONTEXT_COLLAPSE')) { 51 /* eslint-disable @typescript-eslint/no-require-imports */ 52 const { projectView } = 53 require('../../services/contextCollapse/operations.js') as typeof import('../../services/contextCollapse/operations.js') 54 /* eslint-enable @typescript-eslint/no-require-imports */ 55 apiView = projectView(apiView) 56 } 57 58 const { messages: compactedMessages } = await microcompactMessages(apiView) 59 const appState = getAppState() 60 61 return analyzeContextUsage( 62 compactedMessages, 63 mainLoopModel, 64 async () => appState.toolPermissionContext, 65 tools, 66 agentDefinitions, 67 undefined, // terminalWidth 68 // analyzeContextUsage only reads options.{customSystemPrompt,appendSystemPrompt} 69 // but its signature declares the full Pick<ToolUseContext, 'options'>. 70 { options: { customSystemPrompt, appendSystemPrompt } } as Pick< 71 ToolUseContext, 72 'options' 73 >, 74 undefined, // mainThreadAgentDefinition 75 apiView, // original messages for API usage extraction 76 ) 77} 78 79export async function call( 80 _args: string, 81 context: ToolUseContext, 82): Promise<{ type: 'text'; value: string }> { 83 const data = await collectContextData(context) 84 return { 85 type: 'text' as const, 86 value: formatContextAsMarkdownTable(data), 87 } 88} 89 90function formatContextAsMarkdownTable(data: ContextData): string { 91 const { 92 categories, 93 totalTokens, 94 rawMaxTokens, 95 percentage, 96 model, 97 memoryFiles, 98 mcpTools, 99 agents, 100 skills, 101 messageBreakdown, 102 systemTools, 103 systemPromptSections, 104 } = data 105 106 let output = `## Context Usage\n\n` 107 output += `**Model:** ${model} \n` 108 output += `**Tokens:** ${formatTokens(totalTokens)} / ${formatTokens(rawMaxTokens)} (${percentage}%)\n` 109 110 // Context-collapse status. Always show when the runtime gate is on — 111 // the user needs to know which strategy is managing their context 112 // even before anything has fired. 113 if (feature('CONTEXT_COLLAPSE')) { 114 /* eslint-disable @typescript-eslint/no-require-imports */ 115 const { getStats, isContextCollapseEnabled } = 116 require('../../services/contextCollapse/index.js') as typeof import('../../services/contextCollapse/index.js') 117 /* eslint-enable @typescript-eslint/no-require-imports */ 118 if (isContextCollapseEnabled()) { 119 const s = getStats() 120 const { health: h } = s 121 122 const parts = [] 123 if (s.collapsedSpans > 0) { 124 parts.push( 125 `${s.collapsedSpans} ${plural(s.collapsedSpans, 'span')} summarized (${s.collapsedMessages} messages)`, 126 ) 127 } 128 if (s.stagedSpans > 0) parts.push(`${s.stagedSpans} staged`) 129 const summary = 130 parts.length > 0 131 ? parts.join(', ') 132 : h.totalSpawns > 0 133 ? `${h.totalSpawns} ${plural(h.totalSpawns, 'spawn')}, nothing staged yet` 134 : 'waiting for first trigger' 135 output += `**Context strategy:** collapse (${summary})\n` 136 137 if (h.totalErrors > 0) { 138 output += `**Collapse errors:** ${h.totalErrors}/${h.totalSpawns} spawns failed` 139 if (h.lastError) { 140 output += ` (last: ${h.lastError.slice(0, 80)})` 141 } 142 output += '\n' 143 } else if (h.emptySpawnWarningEmitted) { 144 output += `**Collapse idle:** ${h.totalEmptySpawns} consecutive empty runs\n` 145 } 146 } 147 } 148 output += '\n' 149 150 // Main categories table 151 const visibleCategories = categories.filter( 152 cat => 153 cat.tokens > 0 && 154 cat.name !== 'Free space' && 155 cat.name !== 'Autocompact buffer', 156 ) 157 158 if (visibleCategories.length > 0) { 159 output += `### Estimated usage by category\n\n` 160 output += `| Category | Tokens | Percentage |\n` 161 output += `|----------|--------|------------|\n` 162 163 for (const cat of visibleCategories) { 164 const percentDisplay = ((cat.tokens / rawMaxTokens) * 100).toFixed(1) 165 output += `| ${cat.name} | ${formatTokens(cat.tokens)} | ${percentDisplay}% |\n` 166 } 167 168 const freeSpaceCategory = categories.find(c => c.name === 'Free space') 169 if (freeSpaceCategory && freeSpaceCategory.tokens > 0) { 170 const percentDisplay = ( 171 (freeSpaceCategory.tokens / rawMaxTokens) * 172 100 173 ).toFixed(1) 174 output += `| Free space | ${formatTokens(freeSpaceCategory.tokens)} | ${percentDisplay}% |\n` 175 } 176 177 const autocompactCategory = categories.find( 178 c => c.name === 'Autocompact buffer', 179 ) 180 if (autocompactCategory && autocompactCategory.tokens > 0) { 181 const percentDisplay = ( 182 (autocompactCategory.tokens / rawMaxTokens) * 183 100 184 ).toFixed(1) 185 output += `| Autocompact buffer | ${formatTokens(autocompactCategory.tokens)} | ${percentDisplay}% |\n` 186 } 187 188 output += `\n` 189 } 190 191 // MCP tools 192 if (mcpTools.length > 0) { 193 output += `### MCP Tools\n\n` 194 output += `| Tool | Server | Tokens |\n` 195 output += `|------|--------|--------|\n` 196 for (const tool of mcpTools) { 197 output += `| ${tool.name} | ${tool.serverName} | ${formatTokens(tool.tokens)} |\n` 198 } 199 output += `\n` 200 } 201 202 // System tools (ant-only) 203 if ( 204 systemTools && 205 systemTools.length > 0 && 206 process.env.USER_TYPE === 'ant' 207 ) { 208 output += `### [ANT-ONLY] System Tools\n\n` 209 output += `| Tool | Tokens |\n` 210 output += `|------|--------|\n` 211 for (const tool of systemTools) { 212 output += `| ${tool.name} | ${formatTokens(tool.tokens)} |\n` 213 } 214 output += `\n` 215 } 216 217 // System prompt sections (ant-only) 218 if ( 219 systemPromptSections && 220 systemPromptSections.length > 0 && 221 process.env.USER_TYPE === 'ant' 222 ) { 223 output += `### [ANT-ONLY] System Prompt Sections\n\n` 224 output += `| Section | Tokens |\n` 225 output += `|---------|--------|\n` 226 for (const section of systemPromptSections) { 227 output += `| ${section.name} | ${formatTokens(section.tokens)} |\n` 228 } 229 output += `\n` 230 } 231 232 // Custom agents 233 if (agents.length > 0) { 234 output += `### Custom Agents\n\n` 235 output += `| Agent Type | Source | Tokens |\n` 236 output += `|------------|--------|--------|\n` 237 for (const agent of agents) { 238 let sourceDisplay: string 239 switch (agent.source) { 240 case 'projectSettings': 241 sourceDisplay = 'Project' 242 break 243 case 'userSettings': 244 sourceDisplay = 'User' 245 break 246 case 'localSettings': 247 sourceDisplay = 'Local' 248 break 249 case 'flagSettings': 250 sourceDisplay = 'Flag' 251 break 252 case 'policySettings': 253 sourceDisplay = 'Policy' 254 break 255 case 'plugin': 256 sourceDisplay = 'Plugin' 257 break 258 case 'built-in': 259 sourceDisplay = 'Built-in' 260 break 261 default: 262 sourceDisplay = String(agent.source) 263 } 264 output += `| ${agent.agentType} | ${sourceDisplay} | ${formatTokens(agent.tokens)} |\n` 265 } 266 output += `\n` 267 } 268 269 // Memory files 270 if (memoryFiles.length > 0) { 271 output += `### Memory Files\n\n` 272 output += `| Type | Path | Tokens |\n` 273 output += `|------|------|--------|\n` 274 for (const file of memoryFiles) { 275 output += `| ${file.type} | ${file.path} | ${formatTokens(file.tokens)} |\n` 276 } 277 output += `\n` 278 } 279 280 // Skills 281 if (skills && skills.tokens > 0 && skills.skillFrontmatter.length > 0) { 282 output += `### Skills\n\n` 283 output += `| Skill | Source | Tokens |\n` 284 output += `|-------|--------|--------|\n` 285 for (const skill of skills.skillFrontmatter) { 286 output += `| ${skill.name} | ${getSourceDisplayName(skill.source)} | ${formatTokens(skill.tokens)} |\n` 287 } 288 output += `\n` 289 } 290 291 // Message breakdown (ant-only) 292 if (messageBreakdown && process.env.USER_TYPE === 'ant') { 293 output += `### [ANT-ONLY] Message Breakdown\n\n` 294 output += `| Category | Tokens |\n` 295 output += `|----------|--------|\n` 296 output += `| Tool calls | ${formatTokens(messageBreakdown.toolCallTokens)} |\n` 297 output += `| Tool results | ${formatTokens(messageBreakdown.toolResultTokens)} |\n` 298 output += `| Attachments | ${formatTokens(messageBreakdown.attachmentTokens)} |\n` 299 output += `| Assistant messages (non-tool) | ${formatTokens(messageBreakdown.assistantMessageTokens)} |\n` 300 output += `| User messages (non-tool-result) | ${formatTokens(messageBreakdown.userMessageTokens)} |\n` 301 output += `\n` 302 303 if (messageBreakdown.toolCallsByType.length > 0) { 304 output += `#### Top Tools\n\n` 305 output += `| Tool | Call Tokens | Result Tokens |\n` 306 output += `|------|-------------|---------------|\n` 307 for (const tool of messageBreakdown.toolCallsByType) { 308 output += `| ${tool.name} | ${formatTokens(tool.callTokens)} | ${formatTokens(tool.resultTokens)} |\n` 309 } 310 output += `\n` 311 } 312 313 if (messageBreakdown.attachmentsByType.length > 0) { 314 output += `#### Top Attachments\n\n` 315 output += `| Attachment | Tokens |\n` 316 output += `|------------|--------|\n` 317 for (const attachment of messageBreakdown.attachmentsByType) { 318 output += `| ${attachment.name} | ${formatTokens(attachment.tokens)} |\n` 319 } 320 output += `\n` 321 } 322 } 323 324 return output 325}