From b7862388fa5d13eeca92499e8e243e5b158abca7 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Mon, 25 May 2026 14:06:19 +0000 Subject: [PATCH] Studio: render Codex parallel-calls synthesis AFTER the per-tab blocks End-to-end probe of a 2-way parallel-calls Codex turn revealed the visible assistant message looked like: [Codex tab 1/2] [Codex tab 2/2] --- Synthesis --- The synthesis came first (no label), then the tabs, then a trailing "--- Synthesis ---" line with nothing below it. The header comment in chat-adapter.ts said the intent was "[tabs] ... [synthesis]" but the implementation prepended cumulativeText (which carries the synthesis deltas the backend emits after codex_gather) before the tabs. Fix: split renderCodexBuffer into renderCodexTabsBlock (pure per-tab rendering, no trailing divider) and reorder renderFullContent so when Codex fan-out is active the output is: [Codex tab 1/N] [Codex tab N/N] --- Synthesis --- The non-Codex case still returns cumulativeText unchanged. --- .../src/features/chat/api/chat-adapter.ts | 33 +++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/studio/frontend/src/features/chat/api/chat-adapter.ts b/studio/frontend/src/features/chat/api/chat-adapter.ts index 8cd6b903d6..5a71049591 100644 --- a/studio/frontend/src/features/chat/api/chat-adapter.ts +++ b/studio/frontend/src/features/chat/api/chat-adapter.ts @@ -1143,8 +1143,8 @@ export function createOpenAIStreamAdapter(): ChatModelAdapter { let codexTotalTabs = 0; let codexGatherEmitted = false; - function renderCodexBuffer(): string { - if (codexTabBuffers.size === 0 && !codexGatherEmitted) return ""; + function renderCodexTabsBlock(): string { + if (codexTabBuffers.size === 0) return ""; const lines: string[] = []; const ids = [...codexTabBuffers.keys()].sort((a, b) => a - b); for (const id of ids) { @@ -1159,21 +1159,28 @@ export function createOpenAIStreamAdapter(): ChatModelAdapter { lines.push("\n"); } } - if (codexGatherEmitted) { - lines.push("\n--- Synthesis ---\n"); - } return lines.join(""); } - // All chat-content yields go through this so the Codex per-tab - // output is always concatenated with the normal SSE text. The - // synthesis is emitted by the backend BOTH as a `codex_gather` - // tool event AND as a normal content delta; rendering both - // would duplicate it. Render the tabs above (header / tab text) - // separately from cumulativeText (which carries the synthesis - // content delta) so the user sees `[tabs] ... [synthesis]`. + // Codex parallel-calls fan-out renders the labeled tab outputs + // first, then a "--- Synthesis ---" divider, then the synthesis + // text the backend streams as plain content deltas after the + // `codex_gather` event. Earlier the synthesis came BEFORE the + // tabs (since cumulativeText was prepended) which left the + // trailing "--- Synthesis ---" line orphaned at the bottom with + // no synthesis text under it, confusing users. When there is no + // Codex fan-out (single-tab Codex turn or any other provider) + // the function falls back to the plain cumulativeText. function renderFullContent(): string { - return cumulativeText + renderCodexBuffer(); + const tabsBlock = renderCodexTabsBlock(); + if (!tabsBlock && !codexGatherEmitted) { + return cumulativeText; + } + const parts: string[] = []; + if (tabsBlock) parts.push(tabsBlock); + if (codexGatherEmitted) parts.push("\n\n--- Synthesis ---\n\n"); + if (cumulativeText) parts.push(cumulativeText); + return parts.join(""); } // Tracks whether we are currently inside a `` block opened by // a `delta.reasoning_content` chunk. Kimi (kimi-k2.6, kimi-k2-thinking)