Repository navigation
Conversation
Three independent defects could leave a fence stuck in its stock code-block form. Each one is reproducible on a transcript that is rendered all at once (a session switch), or while a reply is still streaming. 1. markDirty dropped the node once dirtyTargets reached its 512-entry cap. A session switch renders the whole transcript in one pass, so the cap is hit almost immediately and the remaining fences fell through to the rotation pass — which only scans 24 rows every 5 seconds. Measured on a 7-fence transcript: every fence took 35s to appear. The overflow now sets a flag and the next sweep runs one full discovery pass, which is a single linear query and covers the transcript in tens of milliseconds. 2. Fence language was read from the snapshot by DOM ordinal (fences[index]). The two lists desynchronise when the snapshot lags behind the DOM, when the two sides filter differently, or when one message is split into several text blocks. A miss returns undefined, and a streaming fence with an unknown language is not taken over until the reply settles. Match by body prefix first and keep the ordinal as the fallback. 3. During streaming, a body that momentarily parsed to no complete component unmounted the mount and restored the raw code block. Once the first mount had produced a real component, mount.skeleton was false, so every component boundary flashed the code block back and the fence appeared to "sit there" while the reply streamed. Keep whatever is already rendered until the fence settles. Verification: tsc clean, tsdown builds, check-standalone and check-map-assets pass. The vitest suite reports the same 413 pre-existing environment failures before and after this change (1693 passing either way), so no new failures.
This branch has not been deployed
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.
What
Three independent defects could leave a
dsh-uifence stuck in its stock code-block form. Each is observable either on a transcript rendered in one pass (switching sessions) or while a reply is still streaming.1. Dirty-target overflow silently dropped discovery work
markDirtyreturns early oncedirtyTargetsreachesDIRTY_TARGET_LIMIT(512). A session switch renders the whole transcript at once, so the cap is hit almost immediately. Every fence after that depends onrotationSweep, which only scansROTATION_ROWS(24) rows everyROTATION_TICKS × SWEEP_MS(5s).Measured on a real 7-fence transcript (ego lite, DSH 0.2.1-rc.3):
The 5000ms cadence is exactly
ROTATION_TICKS × SWEEP_MS.2. Snapshot language was read by DOM ordinal
fences[index]assumes the DOM code-block order matches the snapshot fence order. That breaks when the snapshot lags behind the DOM, when the two sides filter differently (reasoning / tool rows), or when one message is split into several text blocks by tool calls. A miss yieldsundefined, andrenderBlockreturns early for a streaming fence with an unknown language — so the fence waits for the reply to settle before content-based detection can take over.3. Streaming unmounted the mount at component boundaries
if (mount.skeleton && !settled)only kept the mount while it was still a skeleton. After the first mount produced a real component,mount.skeletonwas false, so any intermediate body that parsed tonullhitunmountBlock— the raw code block reappeared — and the next chunk mounted again. The visible effect is a code block that "just sits there" for the whole streaming phase.How
overflowRescan; the next sweep runs one fullfindFenceCandidates()pass, same scope as the initial scan. A single linear query covers the transcript in tens of milliseconds.sameFenceBody/resolveFenceByBodymatch a fence by body prefix, falling back to the ordinal. Exact body equality still wins first, so the common path is unchanged.if (!settled)keeps the already-rendered content (skeleton or last good tree) until the fence settles, instead of flashing the code block back.Verification
tsc -p tsconfig.json— cleantsdown— all bundles buildscripts/check-standalone.mjs/scripts/check-map-assets.mjs— passvitest run— 413 pre-existing environment failures before and after this change (1693 passing either way, 31 files failing on both sides). They are jsdom/localStorage setup issues in this environment, unrelated todom-fence.Scope
Only
src/client/dom-fence.tsx(+61 / −9). No build artifacts (upstream gitignoreslib/), no dependency changes, no new tests.Happy to split this into three PRs if smaller changes are preferred.