From f8417820424e96791602693c1f7387f6baa1fa15 Mon Sep 17 00:00:00 2001 From: Ariane Emory Date: Sun, 21 Dec 2025 19:12:02 -0500 Subject: [PATCH] Fix TUI text truncation during streaming (#5006) Replace reconcile() with produce() for TextPart updates during streaming to prevent race conditions where text-end events overwrite accumulated text from text-delta events. - Use produce() for incremental text updates instead of full object replacement - Preserve streaming context and prevent mid-sentence truncation - Maintain backward compatibility for non-text parts with fallback to reconcile() - Fix race condition in sync.tsx that caused intermittent truncation --- .../opencode/src/cli/cmd/tui/context/sync.tsx | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx index 2528a499896..8bd10783e38 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sync.tsx @@ -215,6 +215,32 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ } const result = Binary.search(parts, event.properties.part.id, (p) => p.id) if (result.found) { + // For TextParts during streaming, use produce to merge deltas safely + // This prevents race conditions where text-end overwrites accumulated text + const currentPart = parts[result.index] + if (currentPart.type === "text" && event.properties.part.type === "text") { + const incomingTextPart = event.properties.part as Extract + const currentTextPart = currentPart as Extract + + // If this is a streaming update (has text field), use produce for safe merge + if (incomingTextPart.text !== undefined) { + setStore( + "part", + event.properties.part.messageID, + produce((draft) => { + const part = draft[result.index] as Extract + // Update text content but preserve other metadata + part.text = incomingTextPart.text + // Update timing only if end time is provided (text-end event) + if (incomingTextPart.time?.end) { + part.time = incomingTextPart.time + } + }) + ) + break + } + } + // Fall back to reconcile for non-text parts or non-streaming updates setStore("part", event.properties.part.messageID, result.index, reconcile(event.properties.part)) break }