fix(desktop): reduce streaming render jitter (webview coalescing + height-cache hygiene) - #8292
Closed
Lxiny-zy wants to merge 2 commits into
Closed
fix(desktop): reduce streaming render jitter (webview coalescing + height-cache hygiene)#8292Lxiny-zy wants to merge 2 commits into
Lxiny-zy wants to merge 2 commits into
Conversation
Lxiny-zy
force-pushed
the
fix/transcript-stream-render-jitter
branch
from
August 11, 2026 03:02
eadf22d to
de20c17
Compare
…annel The agent emits one event per model chunk (hundreds per second on fast streams). Every event crosses the Wails IPC bridge and queues behind the single webview event channel, which blocks runtime.EventsEmit when backed up and turns burst latency into visible input lag and jitter: by the time the frontend's per-frame batch drains, a backlog of stale deltas renders as a jump instead of a stream. webviewStreamCoalescer merges consecutive same-kind text/reasoning deltas for the same tab+submission+runtimeEpoch into one wire event per 16ms window (one 60Hz frame - the frontend already coalesces a second time per frame, so the merged rate never exceeds the repaint rate). Event order and the reasoning->text boundary are preserved (a kind, submission or epoch switch flushes). The runtimeEpoch is part of the merge key so a runtime rebuild can never ride a stale window: the frontend filters whole events by epoch equality, and a merged event stamped with an old epoch would silently drop new text after a rebuild. Windows are generation- tagged so a stale timer callback that lost the lock race to a Push can neither stop the newer timer nor drain the newer window. The merge sits only on the webview exit: bot sinks, project-tree metadata, telemetry and the display buffer keep receiving every event unchanged. Flushes emit outside the mutex (the webview channel may block), and the window is drained when a tab closes so the final tail is not dropped. Tests: window merge, reasoning/text boundary, per-submission isolation, epoch-switch flush, non-stream events flush immediately, timer flush without further pushes, reasoning merge content, stale-generation flush ignored, and a sink-level test proving bot sinks still see per-event delivery while the webview gets the merged stream.
Streaming rows are measured every frame (tail growth, live code highlighting) and their height is a moving target. Writing those intermediate heights into transcriptHeightCache poisoned later estimates for the same row key: a re-mount (history replay, tab switch) used a transient mid-stream height, and the settle correction then jumped the viewport again. createTranscriptMeasureElement now accepts a skipCacheWriteWhen predicate; useTranscriptRowMeasurements passes one that matches answer rows with item.streaming and reasoning rows that are still streaming (item.streaming && !reasoningComplete, the same rule segmentHasRunningWork uses). Settled rows keep writing their final measured height, so the cache stays as useful as before. Tests: predicate assertions (answer/reasoning/user/missing), an integration check that only settled rows reach the cache through the measure element; tsc and eslint clean.
Lxiny-zy
force-pushed
the
fix/transcript-stream-render-jitter
branch
from
August 11, 2026 03:10
de20c17 to
8d859b2
Compare
8 tasks
Contributor
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Streaming rendering jitter on fast model streams. Two compounding sources:
internal/agent/agent.goemitsText/Reasoningper chunk);tabEventSink.Emitforwards every event to Wailsruntime.EventsEmitone by one. When the single webview event channel backs up,EventsEmitblocks, backlog latency spikes, and the frontend's per-frame batch drains a stale backlog in one jump instead of a smooth stream.transcriptHeightCache, so a later re-mount of the same row key (history replay, tab switch) started from a mid-stream height and settled with a second viewport jump.Fix
webviewStreamCoalescer(new,desktop/tab_stream_coalescer.go): merges consecutive same-kind text/reasoning deltas for the same tab+submission into one wire event per 16ms window (one 60Hz frame — the frontend already coalesces a second time per frame, so the merged rate never exceeds the repaint rate). Event order and the reasoning→text boundary are preserved (a kind or submission switch flushes immediately). The merge sits only on the webview exit: bot sinks, project-tree metadata, telemetry and the display buffer keep receiving every event unchanged.createTranscriptMeasureElementaccepts askipCacheWriteWhenpredicate;useTranscriptRowMeasurementsmatches answer rows whoseitem.streamingis still true. Settled rows keep writing their final measured height.Tests
Go (
tab_stream_coalescer_test.go, all pass; fullgo test ./desktop/...green):turn_done) flush the pending delta immediatelyFrontend (
transcript-height-cache.test.ts, 5 assertions;tsc --noEmitand eslint clean):Cache-impact: none - frontend rendering path + Wails event bridge only; no prompt or tool schema bytes change
Cache-guard: none
System-prompt-review: none - no prompt, config schema, memory, output style, or skill behavior changes
Documentation-impact: none - internal rendering/IPC performance fix; no CLI, docs or config surface changed