diff --git a/cli/src/claude/claudeLocalLauncher.test.ts b/cli/src/claude/claudeLocalLauncher.test.ts index fedf6a0a3..a4e855aab 100644 --- a/cli/src/claude/claudeLocalLauncher.test.ts +++ b/cli/src/claude/claudeLocalLauncher.test.ts @@ -105,4 +105,32 @@ describe('claudeLocalLauncher message filtering', () => { expect(sentMessages).toHaveLength(2) }) + + it('filters out isMeta messages (e.g. skill injections)', async () => { + const { session, sentMessages } = createSessionStub() + await claudeLocalLauncher(session as never) + + harness.scannerOnMessage!({ + type: 'user', + isMeta: true, + uuid: '1', + message: { content: [{ type: 'text', text: '# Skill content...' }] } + }) + + expect(sentMessages).toHaveLength(0) + }) + + it('filters out isCompactSummary messages', async () => { + const { session, sentMessages } = createSessionStub() + await claudeLocalLauncher(session as never) + + harness.scannerOnMessage!({ + type: 'assistant', + isCompactSummary: true, + uuid: '1', + message: { content: 'compacted context' } + }) + + expect(sentMessages).toHaveLength(0) + }) }) diff --git a/cli/src/claude/claudeLocalLauncher.ts b/cli/src/claude/claudeLocalLauncher.ts index 5bada013e..291569cda 100644 --- a/cli/src/claude/claudeLocalLauncher.ts +++ b/cli/src/claude/claudeLocalLauncher.ts @@ -15,6 +15,11 @@ export async function claudeLocalLauncher(session: Session): Promise<'switch' | if (message.type === 'summary') { return } + // Filter out internal meta messages (e.g. skill injections) and + // compact summaries to avoid them appearing in the web UI + if (message.isMeta || message.isCompactSummary) { + 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)) { diff --git a/cli/src/claude/utils/OutgoingMessageQueue.test.ts b/cli/src/claude/utils/OutgoingMessageQueue.test.ts new file mode 100644 index 000000000..9530bf338 --- /dev/null +++ b/cli/src/claude/utils/OutgoingMessageQueue.test.ts @@ -0,0 +1,51 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import { OutgoingMessageQueue } from './OutgoingMessageQueue' + +describe('OutgoingMessageQueue message filtering', () => { + let sent: Array> + let queue: OutgoingMessageQueue + + beforeEach(() => { + sent = [] + queue = new OutgoingMessageQueue((msg) => { sent.push(msg) }) + }) + + afterEach(() => { + queue.destroy() + }) + + it('sends normal messages', async () => { + queue.enqueue({ type: 'assistant', uuid: '1' }) + queue.enqueue({ type: 'user', uuid: '2' }) + await queue.flush() + + expect(sent).toHaveLength(2) + }) + + it('filters out system messages', async () => { + queue.enqueue({ type: 'system', subtype: 'init', uuid: '1' }) + queue.enqueue({ type: 'assistant', uuid: '2' }) + await queue.flush() + + expect(sent).toHaveLength(1) + expect(sent[0]).toMatchObject({ type: 'assistant' }) + }) + + it('filters out isMeta messages', async () => { + queue.enqueue({ type: 'user', isMeta: true, uuid: '1' }) + queue.enqueue({ type: 'assistant', uuid: '2' }) + await queue.flush() + + expect(sent).toHaveLength(1) + expect(sent[0]).toMatchObject({ type: 'assistant' }) + }) + + it('filters out isCompactSummary messages', async () => { + queue.enqueue({ type: 'assistant', isCompactSummary: true, uuid: '1' }) + queue.enqueue({ type: 'user', uuid: '2' }) + await queue.flush() + + expect(sent).toHaveLength(1) + expect(sent[0]).toMatchObject({ type: 'user' }) + }) +}) diff --git a/cli/src/claude/utils/OutgoingMessageQueue.ts b/cli/src/claude/utils/OutgoingMessageQueue.ts index 6d214c657..a3b1736e6 100644 --- a/cli/src/claude/utils/OutgoingMessageQueue.ts +++ b/cli/src/claude/utils/OutgoingMessageQueue.ts @@ -121,7 +121,7 @@ export class OutgoingMessageQueue { // Send if not already sent if (!item.sent) { - if (item.logMessage.type !== 'system') { + if (item.logMessage.type !== 'system' && !item.logMessage.isMeta && !item.logMessage.isCompactSummary) { this.sendFunction(item.logMessage); } item.sent = true;