From 6159d3811c4db903faca86df2474c12dcb128dda Mon Sep 17 00:00:00 2001 From: Haoqing Wang <1506751656@qq.com> Date: Thu, 2 Apr 2026 01:08:11 +0800 Subject: [PATCH 1/6] fix(web): filter system-injected XML tags from rendering as raw text Claude Code injects internal messages (, , , ) as user-role messages. The web UI was rendering these as raw XML text visible to users. - Parse and display as agent-event with summary text - Silently drop , , - Add tests covering all injection prefixes and edge cases --- web/src/chat/reducerTimeline.test.ts | 92 ++++++++++++++++++++++++++++ web/src/chat/reducerTimeline.ts | 39 ++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 web/src/chat/reducerTimeline.test.ts diff --git a/web/src/chat/reducerTimeline.test.ts b/web/src/chat/reducerTimeline.test.ts new file mode 100644 index 000000000..53ba61529 --- /dev/null +++ b/web/src/chat/reducerTimeline.test.ts @@ -0,0 +1,92 @@ +import { describe, expect, it } from 'vitest' +import { reduceTimeline } from './reducerTimeline' +import type { TracedMessage } from './tracer' + +function makeContext() { + return { + permissionsById: new Map(), + groups: new Map(), + consumedGroupIds: new Set(), + titleChangesByToolUseId: new Map(), + emittedTitleChangeToolUseIds: new Set() + } +} + +function makeUserMessage(text: string, overrides?: Partial): TracedMessage { + return { + id: 'msg-1', + localId: null, + createdAt: 1_700_000_000_000, + role: 'user', + content: { type: 'text', text }, + isSidechain: false, + ...overrides + } as TracedMessage +} + +describe('reduceTimeline – system injection filtering', () => { + it('converts with summary to agent-event', () => { + const text = ` abc killed Background command "Download benchmarks" was stopped ` + const { blocks } = reduceTimeline([makeUserMessage(text)], makeContext()) + + expect(blocks).toHaveLength(1) + expect(blocks[0].kind).toBe('agent-event') + if (blocks[0].kind === 'agent-event') { + expect(blocks[0].event).toEqual({ + type: 'message', + message: 'Background command "Download benchmarks" was stopped' + }) + } + }) + + it('silently drops without summary', () => { + const text = ` abc killed ` + const { blocks } = reduceTimeline([makeUserMessage(text)], makeContext()) + + expect(blocks).toHaveLength(0) + }) + + it('silently drops with empty summary', () => { + const text = ` completed ` + const { blocks } = reduceTimeline([makeUserMessage(text)], makeContext()) + + expect(blocks).toHaveLength(0) + }) + + it('handles with leading whitespace', () => { + const text = ` \n Task done ` + const { blocks } = reduceTimeline([makeUserMessage(text)], makeContext()) + + expect(blocks).toHaveLength(1) + expect(blocks[0].kind).toBe('agent-event') + }) + + it('hides messages', () => { + const text = `\nSome internal reminder\n` + const { blocks } = reduceTimeline([makeUserMessage(text)], makeContext()) + + expect(blocks).toHaveLength(0) + }) + + it('hides messages', () => { + const text = `commit` + const { blocks } = reduceTimeline([makeUserMessage(text)], makeContext()) + + expect(blocks).toHaveLength(0) + }) + + it('hides messages', () => { + const text = `some caveat` + const { blocks } = reduceTimeline([makeUserMessage(text)], makeContext()) + + expect(blocks).toHaveLength(0) + }) + + it('passes through normal user text as user-text block', () => { + const text = 'Hello, this is a normal message' + const { blocks } = reduceTimeline([makeUserMessage(text)], makeContext()) + + expect(blocks).toHaveLength(1) + expect(blocks[0].kind).toBe('user-text') + }) +}) diff --git a/web/src/chat/reducerTimeline.ts b/web/src/chat/reducerTimeline.ts index 1d40714e1..226f209d5 100644 --- a/web/src/chat/reducerTimeline.ts +++ b/web/src/chat/reducerTimeline.ts @@ -4,6 +4,31 @@ import { createCliOutputBlock, isCliOutputText, mergeCliOutputBlocks } from '@/c import { parseMessageAsEvent } from '@/chat/reducerEvents' import { ensureToolBlock, extractTitleFromChangeTitleInput, isChangeTitleToolName, type PermissionEntry } from '@/chat/reducerTools' +/** + * XML tags that Claude Code injects as user-role messages. + * These must be filtered out or converted before rendering in the web UI. + * Mirrors SYSTEM_INJECTION_PREFIXES in cli/src/api/apiSession.ts. + */ +const SYSTEM_INJECTION_PREFIXES = [ + '', + '', + '', + '', +] + +function isSystemInjectedMessage(text: string): boolean { + const trimmed = text.trimStart() + return SYSTEM_INJECTION_PREFIXES.some(p => trimmed.startsWith(p)) +} + +function parseTaskNotificationSummary(text: string): string | null { + const trimmed = text.trimStart() + if (!trimmed.startsWith('')) return null + const summary = trimmed.match(/([\s\S]*?)<\/summary>/)?.[1]?.trim() + // Return null for missing/empty summary so isSystemInjectedMessage silently drops it + return summary || null +} + export function reduceTimeline( messages: TracedMessage[], context: { @@ -47,6 +72,20 @@ export function reduceTimeline( } if (msg.role === 'user') { + const taskSummary = parseTaskNotificationSummary(msg.content.text) + if (taskSummary) { + blocks.push({ + kind: 'agent-event', + id: msg.id, + createdAt: msg.createdAt, + event: { type: 'message', message: taskSummary }, + meta: msg.meta + }) + continue + } + if (isSystemInjectedMessage(msg.content.text)) { + continue + } if (isCliOutputText(msg.content.text, msg.meta)) { blocks.push(createCliOutputBlock({ id: msg.id, From 7068dccd9f8d4a1607bb269b98a5483ee2a59836 Mon Sep 17 00:00:00 2001 From: Haoqing Wang <1506751656@qq.com> Date: Thu, 2 Apr 2026 01:23:41 +0800 Subject: [PATCH 2/6] fix(web): scope system injection filtering to Claude sessions only Address review feedback: the XML tag filtering was applied at the generic timeline layer, which could incorrectly hide legitimate user messages in Codex/Gemini sessions. - Add isClaudeSession flag threaded from Session.metadata.claudeSessionId - Only filter system-injected tags when isClaudeSession is true - Add tests verifying non-Claude sessions pass through all messages --- web/src/chat/reducer.ts | 5 +++-- web/src/chat/reducerTimeline.test.ts | 21 ++++++++++++++++++-- web/src/chat/reducerTimeline.ts | 29 +++++++++++++++------------- web/src/components/SessionChat.tsx | 6 ++++-- 4 files changed, 42 insertions(+), 19 deletions(-) diff --git a/web/src/chat/reducer.ts b/web/src/chat/reducer.ts index 798499c67..36aac8041 100644 --- a/web/src/chat/reducer.ts +++ b/web/src/chat/reducer.ts @@ -21,7 +21,8 @@ export type LatestUsage = { export function reduceChatBlocks( normalized: NormalizedMessage[], - agentState: AgentState | null | undefined + agentState: AgentState | null | undefined, + options?: { isClaudeSession?: boolean } ): { blocks: ChatBlock[]; hasReadyEvent: boolean; latestUsage: LatestUsage | null } { const permissionsById = getPermissions(agentState) const toolIdsInMessages = collectToolIdsFromMessages(normalized) @@ -43,7 +44,7 @@ export function reduceChatBlocks( const consumedGroupIds = new Set() const emittedTitleChangeToolUseIds = new Set() - const reducerContext = { permissionsById, groups, consumedGroupIds, titleChangesByToolUseId, emittedTitleChangeToolUseIds } + const reducerContext = { permissionsById, groups, consumedGroupIds, titleChangesByToolUseId, emittedTitleChangeToolUseIds, isClaudeSession: options?.isClaudeSession } const rootResult = reduceTimeline(root, reducerContext) let hasReadyEvent = rootResult.hasReadyEvent diff --git a/web/src/chat/reducerTimeline.test.ts b/web/src/chat/reducerTimeline.test.ts index 53ba61529..33e570f87 100644 --- a/web/src/chat/reducerTimeline.test.ts +++ b/web/src/chat/reducerTimeline.test.ts @@ -2,13 +2,14 @@ import { describe, expect, it } from 'vitest' import { reduceTimeline } from './reducerTimeline' import type { TracedMessage } from './tracer' -function makeContext() { +function makeContext(overrides?: { isClaudeSession?: boolean }) { return { permissionsById: new Map(), groups: new Map(), consumedGroupIds: new Set(), titleChangesByToolUseId: new Map(), - emittedTitleChangeToolUseIds: new Set() + emittedTitleChangeToolUseIds: new Set(), + isClaudeSession: overrides?.isClaudeSession ?? true } } @@ -89,4 +90,20 @@ describe('reduceTimeline – system injection filtering', () => { expect(blocks).toHaveLength(1) expect(blocks[0].kind).toBe('user-text') }) + + it('does not filter system tags in non-Claude sessions', () => { + const text = ` Some task ` + const { blocks } = reduceTimeline([makeUserMessage(text)], makeContext({ isClaudeSession: false })) + + expect(blocks).toHaveLength(1) + expect(blocks[0].kind).toBe('user-text') + }) + + it('does not filter in non-Claude sessions', () => { + const text = `Some reminder` + const { blocks } = reduceTimeline([makeUserMessage(text)], makeContext({ isClaudeSession: false })) + + expect(blocks).toHaveLength(1) + expect(blocks[0].kind).toBe('user-text') + }) }) diff --git a/web/src/chat/reducerTimeline.ts b/web/src/chat/reducerTimeline.ts index 226f209d5..061464c83 100644 --- a/web/src/chat/reducerTimeline.ts +++ b/web/src/chat/reducerTimeline.ts @@ -37,6 +37,7 @@ export function reduceTimeline( consumedGroupIds: Set titleChangesByToolUseId: Map emittedTitleChangeToolUseIds: Set + isClaudeSession?: boolean } ): { blocks: ChatBlock[]; toolBlocksById: Map; hasReadyEvent: boolean } { const blocks: ChatBlock[] = [] @@ -72,19 +73,21 @@ export function reduceTimeline( } if (msg.role === 'user') { - const taskSummary = parseTaskNotificationSummary(msg.content.text) - if (taskSummary) { - blocks.push({ - kind: 'agent-event', - id: msg.id, - createdAt: msg.createdAt, - event: { type: 'message', message: taskSummary }, - meta: msg.meta - }) - continue - } - if (isSystemInjectedMessage(msg.content.text)) { - continue + if (context.isClaudeSession) { + const taskSummary = parseTaskNotificationSummary(msg.content.text) + if (taskSummary) { + blocks.push({ + kind: 'agent-event', + id: msg.id, + createdAt: msg.createdAt, + event: { type: 'message', message: taskSummary }, + meta: msg.meta + }) + continue + } + if (isSystemInjectedMessage(msg.content.text)) { + continue + } } if (isCliOutputText(msg.content.text, msg.meta)) { blocks.push(createCliOutputBlock({ diff --git a/web/src/components/SessionChat.tsx b/web/src/components/SessionChat.tsx index 841286245..e50369540 100644 --- a/web/src/components/SessionChat.tsx +++ b/web/src/components/SessionChat.tsx @@ -198,9 +198,11 @@ export function SessionChat(props: { return normalized }, [props.messages]) + const isClaudeSession = Boolean(props.session.metadata?.claudeSessionId) + const reduced = useMemo( - () => reduceChatBlocks(normalizedMessages, props.session.agentState), - [normalizedMessages, props.session.agentState] + () => reduceChatBlocks(normalizedMessages, props.session.agentState, { isClaudeSession }), + [normalizedMessages, props.session.agentState, isClaudeSession] ) const reconciled = useMemo( () => reconcileChatBlocks(reduced.blocks, blocksByIdRef.current), From 880dee2aedaa2f9b54e82d97c3f99c8ee089e379 Mon Sep 17 00:00:00 2001 From: Haoqing Wang <1506751656@qq.com> Date: Thu, 2 Apr 2026 11:24:27 +0800 Subject: [PATCH 3/6] fix(web): treat all string user output as sidechain to prevent prompt leaks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restores the fix from 3cf96ab that was accidentally reverted in 2205e04. In normalizeUserOutput(), string-content user messages arriving through the agent output path are never real user input (real user text goes through normalizeUserRecord). Previously, non-sidechain string messages were emitted as role:'user', causing subagent prompts and system-injected messages to render as user text in the web UI. Now all string-content user messages in this path are: - with summary → converted to role:'event' - Everything else → marked as sidechain (matched to parent Task tool call by the tracer, or harmlessly skipped by the reducer) This provides a root-level fix that prevents ANY string user message from the agent output path from leaking as visible user text. --- web/src/chat/normalize.test.ts | 91 ++++++++++++++++++++++++++++++++++ web/src/chat/normalizeAgent.ts | 30 +++++++++-- 2 files changed, 117 insertions(+), 4 deletions(-) diff --git a/web/src/chat/normalize.test.ts b/web/src/chat/normalize.test.ts index f6f38a0dd..64600669c 100644 --- a/web/src/chat/normalize.test.ts +++ b/web/src/chat/normalize.test.ts @@ -104,4 +104,95 @@ describe('normalizeDecryptedMessage', () => { } expect(firstBlock.text).toContain('"foo": "bar"') }) + + it('converts user output to event', () => { + const message = makeMessage({ + role: 'agent', + content: { + type: 'output', + data: { + type: 'user', + message: { content: ' Background command stopped ' } + } + } + }) + + const normalized = normalizeDecryptedMessage(message) + + expect(normalized).toMatchObject({ + id: 'msg-1', + role: 'event', + isSidechain: false, + content: { type: 'message', message: 'Background command stopped' } + }) + }) + + it('treats without summary as sidechain (dropped by reducer)', () => { + const message = makeMessage({ + role: 'agent', + content: { + type: 'output', + data: { + type: 'user', + uuid: 'u3', + message: { content: ' killed ' } + } + } + }) + + const normalized = normalizeDecryptedMessage(message) + + expect(normalized).toMatchObject({ + role: 'agent', + isSidechain: true, + }) + }) + + it('treats non-sidechain string user output as sidechain', () => { + const message = makeMessage({ + role: 'agent', + content: { + type: 'output', + data: { + type: 'user', + isSidechain: false, + uuid: 'u1', + message: { content: 'This is a subagent prompt' } + } + } + }) + + const normalized = normalizeDecryptedMessage(message) + + expect(normalized).toMatchObject({ + role: 'agent', + isSidechain: true, + }) + if (normalized?.role !== 'agent') throw new Error('Expected agent') + expect(normalized.content[0]).toMatchObject({ + type: 'sidechain', + prompt: 'This is a subagent prompt' + }) + }) + + it('treats user output as sidechain (dropped by reducer)', () => { + const message = makeMessage({ + role: 'agent', + content: { + type: 'output', + data: { + type: 'user', + uuid: 'u2', + message: { content: 'Some internal reminder' } + } + } + }) + + const normalized = normalizeDecryptedMessage(message) + + expect(normalized).toMatchObject({ + role: 'agent', + isSidechain: true, + }) + }) }) diff --git a/web/src/chat/normalizeAgent.ts b/web/src/chat/normalizeAgent.ts index 18886e989..d9c7677f8 100644 --- a/web/src/chat/normalizeAgent.ts +++ b/web/src/chat/normalizeAgent.ts @@ -132,15 +132,37 @@ function normalizeUserOutput( } } + // Handle system-injected messages that arrive as type:'user' through + // the agent output path. Real user text goes through normalizeUserRecord. if (typeof messageContent === 'string') { + // Convert to a visible event + const trimmed = messageContent.trimStart() + if (trimmed.startsWith('')) { + const summary = trimmed.match(/([\s\S]*?)<\/summary>/)?.[1]?.trim() + if (summary) { + return { + id: messageId, + localId, + createdAt, + role: 'event', + content: { type: 'message', message: summary }, + isSidechain: false, + meta + } + } + } + + // All other string-content user messages in this path are + // system-injected (subagent prompts, system reminders, etc.). + // Treat as sidechain so the tracer can match it to a parent Task + // tool call; unmatched ones are harmlessly skipped by the reducer. return { id: messageId, localId, createdAt, - role: 'user', - isSidechain: false, - content: { type: 'text', text: messageContent }, - meta + role: 'agent', + isSidechain: true, + content: [{ type: 'sidechain', uuid, prompt: messageContent }] } } From a364286b39f78dbaf353b309f9e9d7852ec48a33 Mon Sep 17 00:00:00 2001 From: Haoqing Wang <1506751656@qq.com> Date: Thu, 2 Apr 2026 11:30:57 +0800 Subject: [PATCH 4/6] ci: retrigger CI From 5101e903ca44750eacdd1bb900c399121dbc131b Mon Sep 17 00:00:00 2001 From: Haoqing Wang <1506751656@qq.com> Date: Thu, 2 Apr 2026 11:36:14 +0800 Subject: [PATCH 5/6] fix(web): remove superseded return-null filter from upstream PR #372 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The upstream `return null` filter for and (from PR #372) is now superseded by the comprehensive sidechain upgrade logic. Remove it to avoid short-circuiting the new task-notification → event conversion. --- web/src/chat/normalizeAgent.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/web/src/chat/normalizeAgent.ts b/web/src/chat/normalizeAgent.ts index d9c7677f8..c9b50f8be 100644 --- a/web/src/chat/normalizeAgent.ts +++ b/web/src/chat/normalizeAgent.ts @@ -109,18 +109,6 @@ function normalizeUserOutput( const messageContent = message.content - // Skip system-injected messages that were logged as type:'user' but are - // not text the human actually typed (task notifications, command caveats, etc.) - if (typeof messageContent === 'string') { - const trimmed = messageContent.trimStart() - if ( - trimmed.startsWith('') || - trimmed.startsWith('') - ) { - return null - } - } - if (isSidechain && typeof messageContent === 'string') { return { id: messageId, From fde5d8362b6b67b826f2e8cae735bc27ffbfabc7 Mon Sep 17 00:00:00 2001 From: Haoqing Wang <1506751656@qq.com> Date: Thu, 2 Apr 2026 11:42:44 +0800 Subject: [PATCH 6/6] refactor(web): remove reducer-side system injection filtering System-injected messages are now fully handled in normalizeUserOutput() (normalize layer), so the redundant filtering in reduceTimeline() is no longer needed. Removing it also eliminates the risk of accidentally hiding legitimate user messages that happen to start with XML tags. - Remove SYSTEM_INJECTION_PREFIXES, isSystemInjectedMessage, parseTaskNotificationSummary from reducerTimeline.ts - Remove isClaudeSession plumbing from reducer.ts and SessionChat.tsx - Simplify reducerTimeline.test.ts to only test pass-through behavior --- web/src/chat/reducer.ts | 5 +- web/src/chat/reducerTimeline.test.ts | 80 +++------------------------- web/src/chat/reducerTimeline.ts | 42 --------------- web/src/components/SessionChat.tsx | 6 +-- 4 files changed, 11 insertions(+), 122 deletions(-) diff --git a/web/src/chat/reducer.ts b/web/src/chat/reducer.ts index 36aac8041..798499c67 100644 --- a/web/src/chat/reducer.ts +++ b/web/src/chat/reducer.ts @@ -21,8 +21,7 @@ export type LatestUsage = { export function reduceChatBlocks( normalized: NormalizedMessage[], - agentState: AgentState | null | undefined, - options?: { isClaudeSession?: boolean } + agentState: AgentState | null | undefined ): { blocks: ChatBlock[]; hasReadyEvent: boolean; latestUsage: LatestUsage | null } { const permissionsById = getPermissions(agentState) const toolIdsInMessages = collectToolIdsFromMessages(normalized) @@ -44,7 +43,7 @@ export function reduceChatBlocks( const consumedGroupIds = new Set() const emittedTitleChangeToolUseIds = new Set() - const reducerContext = { permissionsById, groups, consumedGroupIds, titleChangesByToolUseId, emittedTitleChangeToolUseIds, isClaudeSession: options?.isClaudeSession } + const reducerContext = { permissionsById, groups, consumedGroupIds, titleChangesByToolUseId, emittedTitleChangeToolUseIds } const rootResult = reduceTimeline(root, reducerContext) let hasReadyEvent = rootResult.hasReadyEvent diff --git a/web/src/chat/reducerTimeline.test.ts b/web/src/chat/reducerTimeline.test.ts index 33e570f87..e572ed452 100644 --- a/web/src/chat/reducerTimeline.test.ts +++ b/web/src/chat/reducerTimeline.test.ts @@ -2,14 +2,13 @@ import { describe, expect, it } from 'vitest' import { reduceTimeline } from './reducerTimeline' import type { TracedMessage } from './tracer' -function makeContext(overrides?: { isClaudeSession?: boolean }) { +function makeContext() { return { permissionsById: new Map(), groups: new Map(), consumedGroupIds: new Set(), titleChangesByToolUseId: new Map(), - emittedTitleChangeToolUseIds: new Set(), - isClaudeSession: overrides?.isClaudeSession ?? true + emittedTitleChangeToolUseIds: new Set() } } @@ -25,65 +24,8 @@ function makeUserMessage(text: string, overrides?: Partial): Trac } as TracedMessage } -describe('reduceTimeline – system injection filtering', () => { - it('converts with summary to agent-event', () => { - const text = ` abc killed Background command "Download benchmarks" was stopped ` - const { blocks } = reduceTimeline([makeUserMessage(text)], makeContext()) - - expect(blocks).toHaveLength(1) - expect(blocks[0].kind).toBe('agent-event') - if (blocks[0].kind === 'agent-event') { - expect(blocks[0].event).toEqual({ - type: 'message', - message: 'Background command "Download benchmarks" was stopped' - }) - } - }) - - it('silently drops without summary', () => { - const text = ` abc killed ` - const { blocks } = reduceTimeline([makeUserMessage(text)], makeContext()) - - expect(blocks).toHaveLength(0) - }) - - it('silently drops with empty summary', () => { - const text = ` completed ` - const { blocks } = reduceTimeline([makeUserMessage(text)], makeContext()) - - expect(blocks).toHaveLength(0) - }) - - it('handles with leading whitespace', () => { - const text = ` \n Task done ` - const { blocks } = reduceTimeline([makeUserMessage(text)], makeContext()) - - expect(blocks).toHaveLength(1) - expect(blocks[0].kind).toBe('agent-event') - }) - - it('hides messages', () => { - const text = `\nSome internal reminder\n` - const { blocks } = reduceTimeline([makeUserMessage(text)], makeContext()) - - expect(blocks).toHaveLength(0) - }) - - it('hides messages', () => { - const text = `commit` - const { blocks } = reduceTimeline([makeUserMessage(text)], makeContext()) - - expect(blocks).toHaveLength(0) - }) - - it('hides messages', () => { - const text = `some caveat` - const { blocks } = reduceTimeline([makeUserMessage(text)], makeContext()) - - expect(blocks).toHaveLength(0) - }) - - it('passes through normal user text as user-text block', () => { +describe('reduceTimeline', () => { + it('renders user text as user-text block', () => { const text = 'Hello, this is a normal message' const { blocks } = reduceTimeline([makeUserMessage(text)], makeContext()) @@ -91,17 +33,9 @@ describe('reduceTimeline – system injection filtering', () => { expect(blocks[0].kind).toBe('user-text') }) - it('does not filter system tags in non-Claude sessions', () => { - const text = ` Some task ` - const { blocks } = reduceTimeline([makeUserMessage(text)], makeContext({ isClaudeSession: false })) - - expect(blocks).toHaveLength(1) - expect(blocks[0].kind).toBe('user-text') - }) - - it('does not filter in non-Claude sessions', () => { - const text = `Some reminder` - const { blocks } = reduceTimeline([makeUserMessage(text)], makeContext({ isClaudeSession: false })) + it('does not filter XML-like user text (filtering is in normalize layer)', () => { + const text = ' Some task ' + const { blocks } = reduceTimeline([makeUserMessage(text)], makeContext()) expect(blocks).toHaveLength(1) expect(blocks[0].kind).toBe('user-text') diff --git a/web/src/chat/reducerTimeline.ts b/web/src/chat/reducerTimeline.ts index 061464c83..1d40714e1 100644 --- a/web/src/chat/reducerTimeline.ts +++ b/web/src/chat/reducerTimeline.ts @@ -4,31 +4,6 @@ import { createCliOutputBlock, isCliOutputText, mergeCliOutputBlocks } from '@/c import { parseMessageAsEvent } from '@/chat/reducerEvents' import { ensureToolBlock, extractTitleFromChangeTitleInput, isChangeTitleToolName, type PermissionEntry } from '@/chat/reducerTools' -/** - * XML tags that Claude Code injects as user-role messages. - * These must be filtered out or converted before rendering in the web UI. - * Mirrors SYSTEM_INJECTION_PREFIXES in cli/src/api/apiSession.ts. - */ -const SYSTEM_INJECTION_PREFIXES = [ - '', - '', - '', - '', -] - -function isSystemInjectedMessage(text: string): boolean { - const trimmed = text.trimStart() - return SYSTEM_INJECTION_PREFIXES.some(p => trimmed.startsWith(p)) -} - -function parseTaskNotificationSummary(text: string): string | null { - const trimmed = text.trimStart() - if (!trimmed.startsWith('')) return null - const summary = trimmed.match(/([\s\S]*?)<\/summary>/)?.[1]?.trim() - // Return null for missing/empty summary so isSystemInjectedMessage silently drops it - return summary || null -} - export function reduceTimeline( messages: TracedMessage[], context: { @@ -37,7 +12,6 @@ export function reduceTimeline( consumedGroupIds: Set titleChangesByToolUseId: Map emittedTitleChangeToolUseIds: Set - isClaudeSession?: boolean } ): { blocks: ChatBlock[]; toolBlocksById: Map; hasReadyEvent: boolean } { const blocks: ChatBlock[] = [] @@ -73,22 +47,6 @@ export function reduceTimeline( } if (msg.role === 'user') { - if (context.isClaudeSession) { - const taskSummary = parseTaskNotificationSummary(msg.content.text) - if (taskSummary) { - blocks.push({ - kind: 'agent-event', - id: msg.id, - createdAt: msg.createdAt, - event: { type: 'message', message: taskSummary }, - meta: msg.meta - }) - continue - } - if (isSystemInjectedMessage(msg.content.text)) { - continue - } - } if (isCliOutputText(msg.content.text, msg.meta)) { blocks.push(createCliOutputBlock({ id: msg.id, diff --git a/web/src/components/SessionChat.tsx b/web/src/components/SessionChat.tsx index e50369540..841286245 100644 --- a/web/src/components/SessionChat.tsx +++ b/web/src/components/SessionChat.tsx @@ -198,11 +198,9 @@ export function SessionChat(props: { return normalized }, [props.messages]) - const isClaudeSession = Boolean(props.session.metadata?.claudeSessionId) - const reduced = useMemo( - () => reduceChatBlocks(normalizedMessages, props.session.agentState, { isClaudeSession }), - [normalizedMessages, props.session.agentState, isClaudeSession] + () => reduceChatBlocks(normalizedMessages, props.session.agentState), + [normalizedMessages, props.session.agentState] ) const reconciled = useMemo( () => reconcileChatBlocks(reduced.blocks, blocksByIdRef.current),