From 745f1e07541acb22e11dd5bb6d3f6051fb22100a Mon Sep 17 00:00:00 2001 From: Haoqing Wang <1506751656@qq.com> Date: Fri, 27 Mar 2026 15:30:47 +0800 Subject: [PATCH 1/3] fix(web): suppress Task tool prompt text from leaking into chat When Claude calls the Task/Agent tool, it often writes the prompt text as a regular text block before the tool_use block in the same message. This causes the prompt to appear twice: once as a standalone message and once inside the tool card. Skip text blocks in agent messages that contain a Task tool_use, since the prompt is already visible in the tool card. --- web/src/chat/reducerTimeline.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/web/src/chat/reducerTimeline.ts b/web/src/chat/reducerTimeline.ts index 962737d27..044ce69b2 100644 --- a/web/src/chat/reducerTimeline.ts +++ b/web/src/chat/reducerTimeline.ts @@ -73,9 +73,18 @@ export function reduceTimeline( } if (msg.role === 'agent') { + // Check if this message contains a Task tool_use. If so, skip text blocks — + // Claude often writes the prompt as text before the tool_use, causing it to + // appear both as a standalone message AND inside the tool card. + const hasTaskToolUse = msg.content.some( + (c) => c.type === 'tool-call' && c.name === 'Task' + ) + for (let idx = 0; idx < msg.content.length; idx += 1) { const c = msg.content[idx] if (c.type === 'text') { + if (hasTaskToolUse) continue + if (isCliOutputText(c.text, msg.meta)) { blocks.push(createCliOutputBlock({ id: `${msg.id}:${idx}`, From 2205e0492054347ce9149a257e8934881bcb5416 Mon Sep 17 00:00:00 2001 From: Haoqing Wang <1506751656@qq.com> Date: Fri, 27 Mar 2026 17:42:55 +0800 Subject: [PATCH 2/3] fix(web): filter system-injected messages from rendering as user messages Task notifications, command caveats, and system reminders are logged as type:'user' in the JSONL but are not text the human actually typed. The CLI fix (279f758) changed the socket role to 'agent', but the web normalizer still saw data.type='user' and created user-role messages. Filter these out in normalizeUserOutput by checking for known XML tag prefixes (, , , etc.) --- web/src/chat/normalizeAgent.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/web/src/chat/normalizeAgent.ts b/web/src/chat/normalizeAgent.ts index 4096e7588..6e4ed40ed 100644 --- a/web/src/chat/normalizeAgent.ts +++ b/web/src/chat/normalizeAgent.ts @@ -109,6 +109,20 @@ 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('') || + trimmed.startsWith('') || + trimmed.startsWith('') + ) { + return null + } + } + if (isSidechain && typeof messageContent === 'string') { return { id: messageId, From e6953ae6ab72c7b10b2fa06821b46afe183df814 Mon Sep 17 00:00:00 2001 From: Haoqing Wang <1506751656@qq.com> Date: Mon, 30 Mar 2026 14:11:14 +0800 Subject: [PATCH 3/3] fix(web): preserve CLI output cards and non-prompt text in Task messages - normalizeAgent: stop filtering and tags which are needed by the CLI output renderer for command cards - reducerTimeline: only skip text blocks that exactly match the Task tool prompt instead of dropping all text blocks in the message --- web/src/chat/normalizeAgent.ts | 2 -- web/src/chat/reducerTimeline.ts | 20 +++++++++++++++----- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/web/src/chat/normalizeAgent.ts b/web/src/chat/normalizeAgent.ts index 6e4ed40ed..18886e989 100644 --- a/web/src/chat/normalizeAgent.ts +++ b/web/src/chat/normalizeAgent.ts @@ -115,8 +115,6 @@ function normalizeUserOutput( const trimmed = messageContent.trimStart() if ( trimmed.startsWith('') || - trimmed.startsWith('') || - trimmed.startsWith('') || trimmed.startsWith('') ) { return null diff --git a/web/src/chat/reducerTimeline.ts b/web/src/chat/reducerTimeline.ts index 044ce69b2..1d40714e1 100644 --- a/web/src/chat/reducerTimeline.ts +++ b/web/src/chat/reducerTimeline.ts @@ -73,17 +73,27 @@ export function reduceTimeline( } if (msg.role === 'agent') { - // Check if this message contains a Task tool_use. If so, skip text blocks — - // Claude often writes the prompt as text before the tool_use, causing it to - // appear both as a standalone message AND inside the tool card. - const hasTaskToolUse = msg.content.some( + // When the message contains a Task tool_use, Claude often writes the + // prompt as a text block before the tool_use block. We only want to + // suppress that exact prompt text — not every text block in the message. + const taskToolCall = msg.content.find( (c) => c.type === 'tool-call' && c.name === 'Task' ) + const taskPromptText: string | null = (() => { + if (!taskToolCall || taskToolCall.type !== 'tool-call') return null + const input = taskToolCall.input + if (typeof input === 'object' && input !== null && 'prompt' in input) { + const p = (input as { prompt: unknown }).prompt + if (typeof p === 'string') return p + } + return null + })() for (let idx = 0; idx < msg.content.length; idx += 1) { const c = msg.content[idx] if (c.type === 'text') { - if (hasTaskToolUse) continue + // Skip text blocks that are just the Task tool prompt (already shown in tool card) + if (taskPromptText && c.text.trim() === taskPromptText.trim()) continue if (isCliOutputText(c.text, msg.meta)) { blocks.push(createCliOutputBlock({