chore: initialize recovered claude workspace
This commit is contained in:
325
src/commands/context/context-noninteractive.ts
Normal file
325
src/commands/context/context-noninteractive.ts
Normal file
@@ -0,0 +1,325 @@
|
||||
import { feature } from 'bun:bundle'
|
||||
import { microcompactMessages } from '../../services/compact/microCompact.js'
|
||||
import type { AppState } from '../../state/AppStateStore.js'
|
||||
import type { Tools, ToolUseContext } from '../../Tool.js'
|
||||
import type { AgentDefinitionsResult } from '../../tools/AgentTool/loadAgentsDir.js'
|
||||
import type { Message } from '../../types/message.js'
|
||||
import {
|
||||
analyzeContextUsage,
|
||||
type ContextData,
|
||||
} from '../../utils/analyzeContext.js'
|
||||
import { formatTokens } from '../../utils/format.js'
|
||||
import { getMessagesAfterCompactBoundary } from '../../utils/messages.js'
|
||||
import { getSourceDisplayName } from '../../utils/settings/constants.js'
|
||||
import { plural } from '../../utils/stringUtils.js'
|
||||
|
||||
/**
|
||||
* Shared data-collection path for `/context` (slash command) and the SDK
|
||||
* `get_context_usage` control request. Mirrors query.ts's pre-API transforms
|
||||
* (compact boundary, projectView, microcompact) so the token count reflects
|
||||
* what the model actually sees.
|
||||
*/
|
||||
type CollectContextDataInput = {
|
||||
messages: Message[]
|
||||
getAppState: () => AppState
|
||||
options: {
|
||||
mainLoopModel: string
|
||||
tools: Tools
|
||||
agentDefinitions: AgentDefinitionsResult
|
||||
customSystemPrompt?: string
|
||||
appendSystemPrompt?: string
|
||||
}
|
||||
}
|
||||
|
||||
export async function collectContextData(
|
||||
context: CollectContextDataInput,
|
||||
): Promise<ContextData> {
|
||||
const {
|
||||
messages,
|
||||
getAppState,
|
||||
options: {
|
||||
mainLoopModel,
|
||||
tools,
|
||||
agentDefinitions,
|
||||
customSystemPrompt,
|
||||
appendSystemPrompt,
|
||||
},
|
||||
} = context
|
||||
|
||||
let apiView = getMessagesAfterCompactBoundary(messages)
|
||||
if (feature('CONTEXT_COLLAPSE')) {
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
const { projectView } =
|
||||
require('../../services/contextCollapse/operations.js') as typeof import('../../services/contextCollapse/operations.js')
|
||||
/* eslint-enable @typescript-eslint/no-require-imports */
|
||||
apiView = projectView(apiView)
|
||||
}
|
||||
|
||||
const { messages: compactedMessages } = await microcompactMessages(apiView)
|
||||
const appState = getAppState()
|
||||
|
||||
return analyzeContextUsage(
|
||||
compactedMessages,
|
||||
mainLoopModel,
|
||||
async () => appState.toolPermissionContext,
|
||||
tools,
|
||||
agentDefinitions,
|
||||
undefined, // terminalWidth
|
||||
// analyzeContextUsage only reads options.{customSystemPrompt,appendSystemPrompt}
|
||||
// but its signature declares the full Pick<ToolUseContext, 'options'>.
|
||||
{ options: { customSystemPrompt, appendSystemPrompt } } as Pick<
|
||||
ToolUseContext,
|
||||
'options'
|
||||
>,
|
||||
undefined, // mainThreadAgentDefinition
|
||||
apiView, // original messages for API usage extraction
|
||||
)
|
||||
}
|
||||
|
||||
export async function call(
|
||||
_args: string,
|
||||
context: ToolUseContext,
|
||||
): Promise<{ type: 'text'; value: string }> {
|
||||
const data = await collectContextData(context)
|
||||
return {
|
||||
type: 'text' as const,
|
||||
value: formatContextAsMarkdownTable(data),
|
||||
}
|
||||
}
|
||||
|
||||
function formatContextAsMarkdownTable(data: ContextData): string {
|
||||
const {
|
||||
categories,
|
||||
totalTokens,
|
||||
rawMaxTokens,
|
||||
percentage,
|
||||
model,
|
||||
memoryFiles,
|
||||
mcpTools,
|
||||
agents,
|
||||
skills,
|
||||
messageBreakdown,
|
||||
systemTools,
|
||||
systemPromptSections,
|
||||
} = data
|
||||
|
||||
let output = `## Context Usage\n\n`
|
||||
output += `**Model:** ${model} \n`
|
||||
output += `**Tokens:** ${formatTokens(totalTokens)} / ${formatTokens(rawMaxTokens)} (${percentage}%)\n`
|
||||
|
||||
// Context-collapse status. Always show when the runtime gate is on —
|
||||
// the user needs to know which strategy is managing their context
|
||||
// even before anything has fired.
|
||||
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()) {
|
||||
const s = getStats()
|
||||
const { health: h } = s
|
||||
|
||||
const parts = []
|
||||
if (s.collapsedSpans > 0) {
|
||||
parts.push(
|
||||
`${s.collapsedSpans} ${plural(s.collapsedSpans, 'span')} summarized (${s.collapsedMessages} messages)`,
|
||||
)
|
||||
}
|
||||
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'
|
||||
output += `**Context strategy:** collapse (${summary})\n`
|
||||
|
||||
if (h.totalErrors > 0) {
|
||||
output += `**Collapse errors:** ${h.totalErrors}/${h.totalSpawns} spawns failed`
|
||||
if (h.lastError) {
|
||||
output += ` (last: ${h.lastError.slice(0, 80)})`
|
||||
}
|
||||
output += '\n'
|
||||
} else if (h.emptySpawnWarningEmitted) {
|
||||
output += `**Collapse idle:** ${h.totalEmptySpawns} consecutive empty runs\n`
|
||||
}
|
||||
}
|
||||
}
|
||||
output += '\n'
|
||||
|
||||
// Main categories table
|
||||
const visibleCategories = categories.filter(
|
||||
cat =>
|
||||
cat.tokens > 0 &&
|
||||
cat.name !== 'Free space' &&
|
||||
cat.name !== 'Autocompact buffer',
|
||||
)
|
||||
|
||||
if (visibleCategories.length > 0) {
|
||||
output += `### Estimated usage by category\n\n`
|
||||
output += `| Category | Tokens | Percentage |\n`
|
||||
output += `|----------|--------|------------|\n`
|
||||
|
||||
for (const cat of visibleCategories) {
|
||||
const percentDisplay = ((cat.tokens / rawMaxTokens) * 100).toFixed(1)
|
||||
output += `| ${cat.name} | ${formatTokens(cat.tokens)} | ${percentDisplay}% |\n`
|
||||
}
|
||||
|
||||
const freeSpaceCategory = categories.find(c => c.name === 'Free space')
|
||||
if (freeSpaceCategory && freeSpaceCategory.tokens > 0) {
|
||||
const percentDisplay = (
|
||||
(freeSpaceCategory.tokens / rawMaxTokens) *
|
||||
100
|
||||
).toFixed(1)
|
||||
output += `| Free space | ${formatTokens(freeSpaceCategory.tokens)} | ${percentDisplay}% |\n`
|
||||
}
|
||||
|
||||
const autocompactCategory = categories.find(
|
||||
c => c.name === 'Autocompact buffer',
|
||||
)
|
||||
if (autocompactCategory && autocompactCategory.tokens > 0) {
|
||||
const percentDisplay = (
|
||||
(autocompactCategory.tokens / rawMaxTokens) *
|
||||
100
|
||||
).toFixed(1)
|
||||
output += `| Autocompact buffer | ${formatTokens(autocompactCategory.tokens)} | ${percentDisplay}% |\n`
|
||||
}
|
||||
|
||||
output += `\n`
|
||||
}
|
||||
|
||||
// MCP tools
|
||||
if (mcpTools.length > 0) {
|
||||
output += `### MCP Tools\n\n`
|
||||
output += `| Tool | Server | Tokens |\n`
|
||||
output += `|------|--------|--------|\n`
|
||||
for (const tool of mcpTools) {
|
||||
output += `| ${tool.name} | ${tool.serverName} | ${formatTokens(tool.tokens)} |\n`
|
||||
}
|
||||
output += `\n`
|
||||
}
|
||||
|
||||
// System tools (ant-only)
|
||||
if (
|
||||
systemTools &&
|
||||
systemTools.length > 0 &&
|
||||
process.env.USER_TYPE === 'ant'
|
||||
) {
|
||||
output += `### [ANT-ONLY] System Tools\n\n`
|
||||
output += `| Tool | Tokens |\n`
|
||||
output += `|------|--------|\n`
|
||||
for (const tool of systemTools) {
|
||||
output += `| ${tool.name} | ${formatTokens(tool.tokens)} |\n`
|
||||
}
|
||||
output += `\n`
|
||||
}
|
||||
|
||||
// System prompt sections (ant-only)
|
||||
if (
|
||||
systemPromptSections &&
|
||||
systemPromptSections.length > 0 &&
|
||||
process.env.USER_TYPE === 'ant'
|
||||
) {
|
||||
output += `### [ANT-ONLY] System Prompt Sections\n\n`
|
||||
output += `| Section | Tokens |\n`
|
||||
output += `|---------|--------|\n`
|
||||
for (const section of systemPromptSections) {
|
||||
output += `| ${section.name} | ${formatTokens(section.tokens)} |\n`
|
||||
}
|
||||
output += `\n`
|
||||
}
|
||||
|
||||
// Custom agents
|
||||
if (agents.length > 0) {
|
||||
output += `### Custom Agents\n\n`
|
||||
output += `| Agent Type | Source | Tokens |\n`
|
||||
output += `|------------|--------|--------|\n`
|
||||
for (const agent of agents) {
|
||||
let sourceDisplay: string
|
||||
switch (agent.source) {
|
||||
case 'projectSettings':
|
||||
sourceDisplay = 'Project'
|
||||
break
|
||||
case 'userSettings':
|
||||
sourceDisplay = 'User'
|
||||
break
|
||||
case 'localSettings':
|
||||
sourceDisplay = 'Local'
|
||||
break
|
||||
case 'flagSettings':
|
||||
sourceDisplay = 'Flag'
|
||||
break
|
||||
case 'policySettings':
|
||||
sourceDisplay = 'Policy'
|
||||
break
|
||||
case 'plugin':
|
||||
sourceDisplay = 'Plugin'
|
||||
break
|
||||
case 'built-in':
|
||||
sourceDisplay = 'Built-in'
|
||||
break
|
||||
default:
|
||||
sourceDisplay = String(agent.source)
|
||||
}
|
||||
output += `| ${agent.agentType} | ${sourceDisplay} | ${formatTokens(agent.tokens)} |\n`
|
||||
}
|
||||
output += `\n`
|
||||
}
|
||||
|
||||
// Memory files
|
||||
if (memoryFiles.length > 0) {
|
||||
output += `### Memory Files\n\n`
|
||||
output += `| Type | Path | Tokens |\n`
|
||||
output += `|------|------|--------|\n`
|
||||
for (const file of memoryFiles) {
|
||||
output += `| ${file.type} | ${file.path} | ${formatTokens(file.tokens)} |\n`
|
||||
}
|
||||
output += `\n`
|
||||
}
|
||||
|
||||
// Skills
|
||||
if (skills && skills.tokens > 0 && skills.skillFrontmatter.length > 0) {
|
||||
output += `### Skills\n\n`
|
||||
output += `| Skill | Source | Tokens |\n`
|
||||
output += `|-------|--------|--------|\n`
|
||||
for (const skill of skills.skillFrontmatter) {
|
||||
output += `| ${skill.name} | ${getSourceDisplayName(skill.source)} | ${formatTokens(skill.tokens)} |\n`
|
||||
}
|
||||
output += `\n`
|
||||
}
|
||||
|
||||
// Message breakdown (ant-only)
|
||||
if (messageBreakdown && process.env.USER_TYPE === 'ant') {
|
||||
output += `### [ANT-ONLY] Message Breakdown\n\n`
|
||||
output += `| Category | Tokens |\n`
|
||||
output += `|----------|--------|\n`
|
||||
output += `| Tool calls | ${formatTokens(messageBreakdown.toolCallTokens)} |\n`
|
||||
output += `| Tool results | ${formatTokens(messageBreakdown.toolResultTokens)} |\n`
|
||||
output += `| Attachments | ${formatTokens(messageBreakdown.attachmentTokens)} |\n`
|
||||
output += `| Assistant messages (non-tool) | ${formatTokens(messageBreakdown.assistantMessageTokens)} |\n`
|
||||
output += `| User messages (non-tool-result) | ${formatTokens(messageBreakdown.userMessageTokens)} |\n`
|
||||
output += `\n`
|
||||
|
||||
if (messageBreakdown.toolCallsByType.length > 0) {
|
||||
output += `#### Top Tools\n\n`
|
||||
output += `| Tool | Call Tokens | Result Tokens |\n`
|
||||
output += `|------|-------------|---------------|\n`
|
||||
for (const tool of messageBreakdown.toolCallsByType) {
|
||||
output += `| ${tool.name} | ${formatTokens(tool.callTokens)} | ${formatTokens(tool.resultTokens)} |\n`
|
||||
}
|
||||
output += `\n`
|
||||
}
|
||||
|
||||
if (messageBreakdown.attachmentsByType.length > 0) {
|
||||
output += `#### Top Attachments\n\n`
|
||||
output += `| Attachment | Tokens |\n`
|
||||
output += `|------------|--------|\n`
|
||||
for (const attachment of messageBreakdown.attachmentsByType) {
|
||||
output += `| ${attachment.name} | ${formatTokens(attachment.tokens)} |\n`
|
||||
}
|
||||
output += `\n`
|
||||
}
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
Reference in New Issue
Block a user