mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
The desktop app now keeps the composer stable while turns are active, reduces low-signal tool noise in the transcript, restores project context under the composer after session creation, and relies on the CLI's own permission requests instead of injecting broader desktop-side Bash asks. This also brings in the supporting desktop app source tree and the server routes/session metadata needed for git info, filesystem browsing, session resume, slash commands, and SDK-backed permission bridging so the UI can operate as a coherent feature instead of a partial patch. Constraint: Desktop transcript needs to stay usable during long multi-tool sessions without hiding file-change diffs Constraint: Permission prompts must mirror CLI behavior closely enough that read-only commands do not get desktop-only prompts Rejected: Keep rendering Read/Bash bodies inline | too noisy and unlike the intended transcript model Rejected: Commit only the touched desktop files | would leave the newly introduced desktop app incomplete in git history Confidence: medium Scope-risk: broad Reversibility: messy Directive: Treat non-writing tools as summary-first transcript events; do not re-expand them by default without validating the UX against long sessions Tested: cd desktop && bun run lint Tested: cd desktop && bun run test -- --run Tested: bun test src/server/__tests__/conversations.test.ts Not-tested: Manual visual regression against the exact screenshots in a live desktop session Not-tested: Full root TypeScript check (repository still has unrelated extracted-native parse failures)
113 lines
2.9 KiB
TypeScript
113 lines
2.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 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()
|
|
|
|
if (!sdkUrl) {
|
|
console.error('Missing --sdk-url')
|
|
process.exit(1)
|
|
}
|
|
|
|
const ws = new WebSocket(sdkUrl)
|
|
|
|
ws.addEventListener('open', () => {
|
|
emit(ws, {
|
|
type: 'system',
|
|
subtype: 'init',
|
|
model: 'mock-opus',
|
|
slash_commands: [{ name: 'help', description: 'Show help' }],
|
|
session_id: sessionId,
|
|
})
|
|
})
|
|
|
|
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)
|
|
|
|
for (const line of lines) {
|
|
const parsed = JSON.parse(line)
|
|
|
|
if (parsed.type === 'user') {
|
|
const text = extractUserText(parsed)
|
|
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,
|
|
})
|
|
emit(ws, {
|
|
type: 'stream_event',
|
|
event: {
|
|
type: 'content_block_delta',
|
|
index: 0,
|
|
delta: { type: 'text_delta', text: `Echo: ${text}` },
|
|
},
|
|
session_id: sessionId,
|
|
})
|
|
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,
|
|
})
|
|
}
|
|
}
|
|
})
|
|
|
|
ws.addEventListener('close', () => {
|
|
process.exit(0)
|
|
})
|