fix(cli): filter invisible system messages in local mode (#351)

This commit is contained in:
Haoqing Wang
2026-03-24 19:21:17 +08:00
committed by GitHub
parent 24834fbef4
commit 30265fdc25
2 changed files with 118 additions and 3 deletions
+108
View File
@@ -0,0 +1,108 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
const harness = vi.hoisted(() => ({
launches: [] as Array<Record<string, unknown>>,
scannerOnMessage: null as ((message: Record<string, unknown>) => void) | null
}))
vi.mock('./claudeLocal', () => ({
claudeLocal: async (opts: Record<string, unknown>) => {
harness.launches.push(opts)
}
}))
vi.mock('./utils/sessionScanner', () => ({
createSessionScanner: async (opts: { onMessage: (message: Record<string, unknown>) => void }) => {
harness.scannerOnMessage = opts.onMessage
return {
cleanup: async () => {},
onNewSession: () => {}
}
}
}))
vi.mock('@/modules/common/launcher/BaseLocalLauncher', () => ({
BaseLocalLauncher: class {
constructor(private readonly opts: { launch: (signal: AbortSignal) => Promise<void> }) {}
async run(): Promise<'exit'> {
await this.opts.launch(new AbortController().signal)
return 'exit'
}
}
}))
import { claudeLocalLauncher } from './claudeLocalLauncher'
function createSessionStub() {
const sentMessages: Array<Record<string, unknown>> = []
return {
session: {
sessionId: 'test-session',
path: '/tmp/test',
startedBy: 'terminal' as const,
startingMode: 'local' as const,
claudeEnvVars: {},
claudeArgs: [],
mcpServers: [],
allowedTools: [],
hookSettingsPath: null,
queue: { size: () => 0, reset: () => {}, setOnMessage: () => {} },
client: {
sendClaudeSessionMessage: (msg: Record<string, unknown>) => { sentMessages.push(msg) },
rpcHandlerManager: { registerHandler: () => {} }
},
addSessionFoundCallback: () => {},
removeSessionFoundCallback: () => {},
consumeOneTimeFlags: () => {},
recordLocalLaunchFailure: () => {}
},
sentMessages
}
}
describe('claudeLocalLauncher message filtering', () => {
afterEach(() => {
harness.launches = []
harness.scannerOnMessage = null
})
it('filters out summary messages', async () => {
const { session, sentMessages } = createSessionStub()
await claudeLocalLauncher(session as never)
harness.scannerOnMessage!({ type: 'summary', leafUuid: '1' })
expect(sentMessages).toHaveLength(0)
})
it('filters out invisible system messages', async () => {
const { session, sentMessages } = createSessionStub()
await claudeLocalLauncher(session as never)
harness.scannerOnMessage!({ type: 'system', subtype: 'init', uuid: '1' })
harness.scannerOnMessage!({ type: 'system', subtype: 'stop_hook_summary', uuid: '2' })
harness.scannerOnMessage!({ type: 'system', uuid: '3' })
expect(sentMessages).toHaveLength(0)
})
it('forwards visible system messages', async () => {
const { session, sentMessages } = createSessionStub()
await claudeLocalLauncher(session as never)
harness.scannerOnMessage!({ type: 'system', subtype: 'api_error', uuid: '1' })
harness.scannerOnMessage!({ type: 'system', subtype: 'turn_duration', uuid: '2' })
expect(sentMessages).toHaveLength(2)
})
it('forwards normal conversation messages', async () => {
const { session, sentMessages } = createSessionStub()
await claudeLocalLauncher(session as never)
harness.scannerOnMessage!({ type: 'user', uuid: '1' })
harness.scannerOnMessage!({ type: 'assistant', uuid: '2' })
expect(sentMessages).toHaveLength(2)
})
})
+10 -3
View File
@@ -1,6 +1,7 @@
import { claudeLocal } from "./claudeLocal";
import { Session } from "./session";
import { createSessionScanner } from "./utils/sessionScanner";
import { isClaudeChatVisibleMessage } from "./utils/chatVisibility";
import { BaseLocalLauncher } from "@/modules/common/launcher/BaseLocalLauncher";
export async function claudeLocalLauncher(session: Session): Promise<'switch' | 'exit'> {
@@ -9,11 +10,17 @@ export async function claudeLocalLauncher(session: Session): Promise<'switch' |
const scanner = await createSessionScanner({
sessionId: session.sessionId,
workingDirectory: session.path,
onMessage: (message) => {
onMessage: (message) => {
// Block SDK summary messages - we generate our own
if (message.type !== 'summary') {
session.client.sendClaudeSessionMessage(message)
if (message.type === 'summary') {
return
}
// Filter out invisible system messages (e.g. init, stop_hook_summary)
// to avoid them showing as raw JSON in the web UI
if (!isClaudeChatVisibleMessage(message)) {
return
}
session.client.sendClaudeSessionMessage(message)
}
});