From 73a910bb62530503de89656c17458a41c94581f2 Mon Sep 17 00:00:00 2001 From: xintaofei Date: Fri, 17 Apr 2026 09:12:54 +0800 Subject: [PATCH] fix(ui): clean streaming Agent result output by unwrapping JSON and stripping task_id prefix Refactor cleanAgentOutput to a sequential non-recursive pipeline: unwrap JSON containers first, then strip task_id session lines, then extract content. Also apply cleaning to in-progress agent output during streaming, not just completed results. --- src/contexts/conversation-runtime-context.tsx | 43 +++++++++++-------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/src/contexts/conversation-runtime-context.tsx b/src/contexts/conversation-runtime-context.tsx index 8b24d35..cee9925 100644 --- a/src/contexts/conversation-runtime-context.tsx +++ b/src/contexts/conversation-runtime-context.tsx @@ -199,13 +199,14 @@ function getJoinedChunks(chunks: readonly string[]): string { */ function cleanAgentOutput(output: string | null): string | null { if (!output) return null - const trimmed = output.trim() - if (!trimmed) return null + let text = output.trim() + if (!text) return null + // Step 1: Unwrap JSON containers (no recursion — single-level unwrap) // JSON array of content blocks: [{"type":"text","text":"..."},...] - if (trimmed.startsWith("[")) { + if (text.startsWith("[")) { try { - const arr = JSON.parse(trimmed) + const arr = JSON.parse(text) if (Array.isArray(arr)) { const texts: string[] = [] for (const item of arr) { @@ -217,37 +218,43 @@ function cleanAgentOutput(output: string | null): string | null { texts.push(item.text) } } - if (texts.length > 0) return texts.join("\n") + if (texts.length > 0) text = texts.join("\n") } } catch { /* not valid JSON */ } - } - - // JSON object with common result fields - if (trimmed.startsWith("{")) { + } else if (text.startsWith("{")) { + // JSON object with common result fields try { - const obj = JSON.parse(trimmed) as Record + const obj = JSON.parse(text) as Record for (const key of ["result", "output", "text", "content", "completed"]) { - if (typeof obj[key] === "string") return obj[key] as string + if (typeof obj[key] === "string") { + text = (obj[key] as string).trim() + break + } } } catch { /* not valid JSON */ } } - // XML wrapper (OpenCode) - const tagStart = trimmed.indexOf("") + // Step 2: Strip leading session / task_id lines that some agents prepend + // before the actual result (e.g. "task_id: ses_xxx (for resuming ...)"). + text = text.replace(/^task_id:\s*\S+[^\n]*\n+/, "").trim() + if (!text) return null + + // Step 3: Extract from XML wrapper (OpenCode) + const tagStart = text.indexOf("") if (tagStart !== -1) { const contentStart = tagStart + "".length - const contentEnd = trimmed.indexOf("", contentStart) - const extracted = trimmed + const contentEnd = text.indexOf("", contentStart) + const extracted = text .substring(contentStart, contentEnd === -1 ? undefined : contentEnd) .trim() if (extracted) return extracted } - return output + return text } function buildStreamingTurnsFromLiveMessage( @@ -441,7 +448,9 @@ function buildStreamingTurnsFromLiveMessage( currentBlocks.push({ type: "tool_result", tool_use_id: block.info.tool_call_id, - output_preview: resolvedOutput ?? null, + output_preview: isAgent + ? cleanAgentOutput(resolvedOutput) + : (resolvedOutput ?? null), is_error: false, ...(agentStats ? { agent_stats: agentStats } : {}), })