From 546238a92abd12adebcfbdaa6d52ada8be3c3cb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chindri=C8=99=20Mihai=20Alexandru?= <12643176+chindris-mihai-alexandru@users.noreply.github.com> Date: Mon, 16 Feb 2026 20:01:45 +0200 Subject: [PATCH] perf(tui): reduce stream redraw churn and idle reconnect load --- .../opencode/src/cli/cmd/tui/context/sdk.tsx | 50 ++++++++++++------- .../opencode/src/cli/cmd/tui/context/sync.tsx | 3 +- .../src/cli/cmd/tui/routes/session/index.tsx | 13 ++--- 3 files changed, 39 insertions(+), 27 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/context/sdk.tsx b/packages/opencode/src/cli/cmd/tui/context/sdk.tsx index 7fa7e05c3d2..8f66dce95fc 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sdk.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sdk.tsx @@ -31,14 +31,16 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({ let queue: Event[] = [] let timer: Timer | undefined - let last = 0 + + const STREAM_BATCH_MS = 32 + const EVENT_BATCH_MS = 12 + const MAX_QUEUE_BEFORE_FLUSH = 100 const flush = () => { if (queue.length === 0) return const events = queue queue = [] timer = undefined - last = Date.now() // Batch all event emissions so all store updates result in a single render batch(() => { for (const event of events) { @@ -49,16 +51,16 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({ const handleEvent = (event: Event) => { queue.push(event) - const elapsed = Date.now() - last - if (timer) return - // If we just flushed recently (within 16ms), batch this with future events - // Otherwise, process immediately to avoid latency - if (elapsed < 16) { - timer = setTimeout(flush, 16) + if (queue.length >= MAX_QUEUE_BEFORE_FLUSH) { + if (timer) clearTimeout(timer) + flush() return } - flush() + + if (timer) return + const wait = event.type === "message.part.updated" ? STREAM_BATCH_MS : EVENT_BATCH_MS + timer = setTimeout(flush, wait) } onMount(async () => { @@ -70,17 +72,29 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({ } // Fall back to SSE + let retry = 250 while (true) { if (abort.signal.aborted) break - const events = await sdk.event.subscribe( - {}, - { - signal: abort.signal, - }, - ) - - for await (const event of events.stream) { - handleEvent(event) + try { + const events = await sdk.event.subscribe( + {}, + { + signal: abort.signal, + }, + ) + + retry = 250 + for await (const event of events.stream) { + handleEvent(event) + } + + if (!abort.signal.aborted) { + await Bun.sleep(100) + } + } catch { + if (abort.signal.aborted) break + await Bun.sleep(retry) + retry = Math.min(retry * 2, 2_000) } // Flush any remaining events diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx index eb8ed2d9bba..bb0fb7e40b6 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sync.tsx @@ -286,7 +286,8 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ } const result = Binary.search(parts, event.properties.part.id, (p) => p.id) if (result.found) { - setStore("part", event.properties.part.messageID, result.index, reconcile(event.properties.part)) + if (parts[result.index] === event.properties.part) break + setStore("part", event.properties.part.messageID, result.index, event.properties.part) break } setStore( diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 5aba1a56a54..26ce9ed950d 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -1354,17 +1354,14 @@ function ReasoningPart(props: { last: boolean; part: ReasoningPart; message: Ass function TextPart(props: { last: boolean; part: TextPart; message: AssistantMessage }) { const ctx = use() const { theme, syntax } = useTheme() + const content = createMemo(() => props.part.text) + return ( - + 0}> - +