mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
Desktop context inspection could send get_context_usage while a prewarmed CLI process existed but before the SDK socket was connected. That first control request sat in the pending outbound queue and could time out, leaving the composer spinner or a misleading empty-state context display. The server now waits for the SDK socket before sending control requests, and the UI renders the actual initial context snapshot instead of masking empty active-session data. Constraint: CLI /context and desktop get_context_usage must stay on the shared collectContextData/analyzeContextUsage path Rejected: Hide zero-token active snapshots in the component | that masks backend timing bugs and prevents showing real initial system/tool context Confidence: high Scope-risk: narrow Directive: Do not queue control requests before the SDK socket is connected; let user messages keep the pending outbound behavior Tested: bun test src/server/__tests__/conversations.test.ts --test-name-pattern "not queue control|context-only|prewarmed empty session|structured session inspection" Tested: cd desktop && bun run test -- pages.test.tsx --test-name-pattern "EmptySession shows draft context|first-paint spinner|empty live session|live context usage|runtime model" Tested: bun run check:server Tested: cd desktop && bun run lint Tested: real dev API prewarmed empty session returned 31,190 / 262,144 (12%) on first context-only inspection and 16ms on repeat Tested: browser at http://127.0.0.1:5174/ showed 上下文用量 12% for an empty active session
319 lines
9.9 KiB
TypeScript
319 lines
9.9 KiB
TypeScript
const args = process.argv.slice(2)
|
|
|
|
function getArg(name: string): string | undefined {
|
|
const index = args.indexOf(name)
|
|
return index >= 0 ? args[index + 1] : undefined
|
|
}
|
|
|
|
function emit(ws: WebSocket, payload: Record<string, unknown>) {
|
|
ws.send(JSON.stringify(payload) + '\n')
|
|
}
|
|
|
|
function delay(ms: number) {
|
|
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
}
|
|
|
|
function extractUserText(message: any): string {
|
|
const content = message?.message?.content
|
|
if (!Array.isArray(content)) return ''
|
|
return content
|
|
.filter((block: any) => block?.type === 'text' && typeof block.text === 'string')
|
|
.map((block: any) => block.text)
|
|
.join(' ')
|
|
}
|
|
|
|
const sdkUrl = getArg('--sdk-url')
|
|
const sessionId = getArg('--session-id') || crypto.randomUUID()
|
|
const initMode = process.env.MOCK_SDK_INIT_MODE || 'on_open'
|
|
const initDelayMs = Number(process.env.MOCK_SDK_INIT_DELAY_MS || '0')
|
|
const streamDelayMs = Number(process.env.MOCK_SDK_STREAM_DELAY_MS || '0')
|
|
const exitAfterOpenMs = Number(process.env.MOCK_SDK_EXIT_AFTER_OPEN_MS || '0')
|
|
const exitAfterFirstUserMs = Number(process.env.MOCK_SDK_EXIT_AFTER_FIRST_USER_MS || '0')
|
|
const mcpStatusDelayMs = Number(process.env.MOCK_SDK_MCP_STATUS_DELAY_MS || '0')
|
|
const startupStdout = process.env.MOCK_SDK_STARTUP_STDOUT || ''
|
|
const exitBeforeSdkMs = Number(process.env.MOCK_SDK_EXIT_BEFORE_SDK_MS || '0')
|
|
let initSent = false
|
|
let firstUserExitScheduled = false
|
|
|
|
if (!sdkUrl) {
|
|
console.error('Missing --sdk-url')
|
|
process.exit(1)
|
|
}
|
|
|
|
if (startupStdout) {
|
|
console.log(startupStdout)
|
|
}
|
|
|
|
if (exitBeforeSdkMs > 0) {
|
|
setTimeout(() => process.exit(1), exitBeforeSdkMs)
|
|
}
|
|
|
|
const ws = new WebSocket(sdkUrl)
|
|
|
|
function sendInit() {
|
|
if (initSent) return
|
|
initSent = true
|
|
emit(ws, {
|
|
type: 'system',
|
|
subtype: 'init',
|
|
model: 'mock-opus',
|
|
slash_commands: [{ name: 'help', description: 'Show help' }],
|
|
session_id: sessionId,
|
|
})
|
|
}
|
|
|
|
ws.addEventListener('open', () => {
|
|
if (initMode !== 'on_first_user') {
|
|
if (initDelayMs > 0) {
|
|
setTimeout(sendInit, initDelayMs)
|
|
} else {
|
|
sendInit()
|
|
}
|
|
}
|
|
if (exitAfterOpenMs > 0) {
|
|
setTimeout(() => process.exit(1), exitAfterOpenMs)
|
|
}
|
|
})
|
|
|
|
ws.addEventListener('message', (event) => {
|
|
const payload = typeof event.data === 'string' ? event.data : String(event.data)
|
|
const lines = payload.split('\n').map(line => line.trim()).filter(Boolean)
|
|
|
|
void (async () => {
|
|
for (const line of lines) {
|
|
const parsed = JSON.parse(line)
|
|
|
|
if (parsed.type === 'user') {
|
|
sendInit()
|
|
if (exitAfterFirstUserMs > 0 && !firstUserExitScheduled) {
|
|
firstUserExitScheduled = true
|
|
setTimeout(() => process.exit(1), exitAfterFirstUserMs)
|
|
continue
|
|
}
|
|
const text = extractUserText(parsed)
|
|
const slashCommand = text.trim()
|
|
if (slashCommand === '/cost') {
|
|
emit(ws, {
|
|
type: 'system',
|
|
subtype: 'local_command_output',
|
|
content: 'Total cost: $0.0000\nTotal duration: 0s',
|
|
session_id: sessionId,
|
|
})
|
|
emit(ws, {
|
|
type: 'result',
|
|
subtype: 'success',
|
|
is_error: false,
|
|
result: 'Total cost: $0.0000',
|
|
usage: { input_tokens: 0, output_tokens: 0 },
|
|
session_id: sessionId,
|
|
})
|
|
continue
|
|
}
|
|
if (slashCommand === '/context') {
|
|
emit(ws, {
|
|
type: 'system',
|
|
subtype: 'local_command_output',
|
|
content: '## Context Usage\n\n| Type | Tokens |\n| --- | ---: |\n| System prompt | 123 |',
|
|
session_id: sessionId,
|
|
})
|
|
emit(ws, {
|
|
type: 'result',
|
|
subtype: 'success',
|
|
is_error: false,
|
|
result: 'Context usage',
|
|
usage: { input_tokens: 0, output_tokens: 0 },
|
|
session_id: sessionId,
|
|
})
|
|
continue
|
|
}
|
|
if (text.includes('trigger api error')) {
|
|
emit(ws, {
|
|
type: 'assistant',
|
|
error: 'invalid_request',
|
|
isApiErrorMessage: true,
|
|
message: {
|
|
role: 'assistant',
|
|
content: [{ type: 'text', text: 'Prompt is too long' }],
|
|
},
|
|
session_id: sessionId,
|
|
})
|
|
if (text.includes('then exit')) {
|
|
setTimeout(() => process.exit(1), 10)
|
|
continue
|
|
}
|
|
emit(ws, {
|
|
type: 'result',
|
|
subtype: 'success',
|
|
is_error: true,
|
|
result: 'Prompt is too long',
|
|
usage: { input_tokens: 0, output_tokens: 0 },
|
|
session_id: sessionId,
|
|
})
|
|
continue
|
|
}
|
|
emit(ws, {
|
|
type: 'stream_event',
|
|
event: { type: 'message_start' },
|
|
session_id: sessionId,
|
|
})
|
|
emit(ws, {
|
|
type: 'stream_event',
|
|
event: {
|
|
type: 'content_block_start',
|
|
index: 0,
|
|
content_block: { type: 'text', text: '' },
|
|
},
|
|
session_id: sessionId,
|
|
})
|
|
emit(ws, {
|
|
type: 'stream_event',
|
|
event: {
|
|
type: 'content_block_delta',
|
|
index: 0,
|
|
delta: { type: 'thinking_delta', thinking: 'Mock thinking...' },
|
|
},
|
|
session_id: sessionId,
|
|
})
|
|
if (streamDelayMs > 0) await delay(streamDelayMs)
|
|
emit(ws, {
|
|
type: 'stream_event',
|
|
event: {
|
|
type: 'content_block_delta',
|
|
index: 0,
|
|
delta: { type: 'text_delta', text: `Echo: ${text}` },
|
|
},
|
|
session_id: sessionId,
|
|
})
|
|
if (streamDelayMs > 0) await delay(streamDelayMs)
|
|
emit(ws, {
|
|
type: 'stream_event',
|
|
event: { type: 'content_block_stop', index: 0 },
|
|
session_id: sessionId,
|
|
})
|
|
emit(ws, {
|
|
type: 'result',
|
|
subtype: 'success',
|
|
is_error: false,
|
|
result: `Echo: ${text}`,
|
|
usage: { input_tokens: 3, output_tokens: 2 },
|
|
session_id: sessionId,
|
|
})
|
|
}
|
|
|
|
if (parsed.type === 'control_request' && parsed.request?.subtype === 'interrupt') {
|
|
emit(ws, {
|
|
type: 'result',
|
|
subtype: 'success',
|
|
is_error: false,
|
|
result: 'Interrupted',
|
|
usage: { input_tokens: 0, output_tokens: 0 },
|
|
session_id: sessionId,
|
|
})
|
|
}
|
|
|
|
if (parsed.type === 'control_request' && parsed.request?.subtype === 'get_session_usage') {
|
|
emit(ws, {
|
|
type: 'control_response',
|
|
response: {
|
|
subtype: 'success',
|
|
request_id: parsed.request_id,
|
|
response: {
|
|
totalCostUSD: 0.1234,
|
|
costDisplay: '$0.1234',
|
|
hasUnknownModelCost: false,
|
|
totalAPIDuration: 4,
|
|
totalDuration: 43,
|
|
totalLinesAdded: 0,
|
|
totalLinesRemoved: 0,
|
|
totalInputTokens: 27000,
|
|
totalOutputTokens: 41,
|
|
totalCacheReadInputTokens: 0,
|
|
totalCacheCreationInputTokens: 0,
|
|
totalWebSearchRequests: 0,
|
|
models: [{
|
|
model: 'mock-opus',
|
|
displayName: 'mock-opus',
|
|
inputTokens: 27000,
|
|
outputTokens: 41,
|
|
cacheReadInputTokens: 0,
|
|
cacheCreationInputTokens: 0,
|
|
webSearchRequests: 0,
|
|
costUSD: 0.1234,
|
|
costDisplay: '$0.1234',
|
|
contextWindow: 200000,
|
|
maxOutputTokens: 8192,
|
|
}],
|
|
},
|
|
},
|
|
session_id: sessionId,
|
|
})
|
|
}
|
|
|
|
if (parsed.type === 'control_request' && parsed.request?.subtype === 'get_context_usage') {
|
|
emit(ws, {
|
|
type: 'control_response',
|
|
response: {
|
|
subtype: 'success',
|
|
request_id: parsed.request_id,
|
|
response: {
|
|
categories: [
|
|
{ name: 'System prompt', tokens: 6800, color: '#8a8a8a' },
|
|
{ name: 'MCP tools', tokens: 5900, color: '#06b6d4' },
|
|
{ name: 'Messages', tokens: 2400, color: '#7c3aed' },
|
|
{ name: 'Free space', tokens: 132000, color: '#a1a1aa' },
|
|
],
|
|
totalTokens: 27000,
|
|
maxTokens: 200000,
|
|
rawMaxTokens: 200000,
|
|
percentage: 13,
|
|
gridRows: Array.from({ length: 10 }, (_, row) =>
|
|
Array.from({ length: 10 }, (_, col) => {
|
|
const index = row * 10 + col
|
|
return {
|
|
color: index < 13 ? '#06b6d4' : '#a1a1aa',
|
|
isFilled: index < 13,
|
|
categoryName: index < 13 ? 'Used' : 'Free space',
|
|
tokens: 2000,
|
|
percentage: 1,
|
|
squareFullness: index < 13 ? 1 : 0,
|
|
}
|
|
}),
|
|
),
|
|
model: 'mock-opus',
|
|
estimateOnly: parsed.request.estimateOnly === true,
|
|
memoryFiles: [],
|
|
mcpTools: [{ name: 'mock_tool', serverName: 'mock', tokens: 144, isLoaded: true }],
|
|
agents: [],
|
|
skills: { totalSkills: 1, includedSkills: 1, tokens: 3000, skillFrontmatter: [] },
|
|
isAutoCompactEnabled: true,
|
|
apiUsage: null,
|
|
},
|
|
},
|
|
session_id: sessionId,
|
|
})
|
|
}
|
|
|
|
if (parsed.type === 'control_request' && parsed.request?.subtype === 'mcp_status') {
|
|
if (mcpStatusDelayMs > 0) {
|
|
await delay(mcpStatusDelayMs)
|
|
}
|
|
emit(ws, {
|
|
type: 'control_response',
|
|
response: {
|
|
subtype: 'success',
|
|
request_id: parsed.request_id,
|
|
response: {
|
|
mcpServers: [{ name: 'mock', status: 'connected' }],
|
|
},
|
|
},
|
|
session_id: sessionId,
|
|
})
|
|
}
|
|
}
|
|
})()
|
|
})
|
|
|
|
ws.addEventListener('close', () => {
|
|
process.exit(0)
|
|
})
|