Replace the flat 2-render burst cap with cycle-aware tracking: unseen
monotonic window keys proceed (genuine convergence A->B->C->settled is
never capped), while repeated oscillating keys (A->B->A->B) terminate the
burst so the rAF/measure loop stays bounded. Thread an internal-measurement
provenance marker through both rAF layers into renderMessages() and reset
burst state on every unmarked (externally initiated) render trigger, so a
pending internal callback can never starve a legitimate new cycle.
_scheduleMessageVirtualMeasurementRefresh() reset _messageVirtualMeasurementRetryCount
every time _messageVirtualMeasurementCycleKey changed. When WebKit alternates
between two measured window-metric sets (A -> B -> A -> B) after
getBoundingClientRect(), each key transition renewed the two-render budget and
the requestAnimationFrame/geometry-measurement loop never terminated
(WebContent ~30% CPU for minutes after session recovery).
The MESSAGE_VIRTUAL_MEASUREMENT_MAX_RERENDERS budget now caps the whole
convergence burst globally; the cycle-key branch only records the new key.
The budget still resets at the correct lifecycle boundaries:
_markMessageVirtualMeasurementsSettled() and _clearMessageVirtualHeightCache().
Adds a deterministic node-based regression test driving the real ui.js
function with alternating keys ['A','B','A','B'] (before: 4 scheduled
re-renders; after: 2), plus a stable-key guard and a structural test.
franksong2702: preserve the reader's semantic viewport anchor during mid-stream transcript rebuilds
before raw scrollTop fallback; remount virtualized anchors before restore w/ _programmaticScroll
scoped to the remount frame; treat captured userUnpinned/pinned snapshot as authoritative so
distance-inference can't re-pin a reader who scrolled away during a live reply. Browser-only scroll
state. Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
ROOT FIX (product bug): POST /api/settings never invalidated the session-list
cache, so toggling show_cli_sessions/show_cron_sessions could serve a stale
sidebar for up to the cache TTL when the settings-file mtime stamp collided with
a cached entry. The handler now clears the cache directly on those toggles. This
is also the root cause of the recurring test_gateway_sync CI flake (session
absent from /api/sessions right after the visibility toggle).
DEFENSE-IN-DEPTH: cleanup_test_sessions fixture now resets show_cli_sessions to
default BEFORE each test too, not only in teardown.
FLAKE FIX (test infra): bump 31 node-subprocess correctness-harness timeouts from
10s -> 30s across 23 test files. Node spawn + V8 warmup under parallel CI shard
load can exceed 10s, causing TimeoutExpired flakes (e.g. test_1325 on #4445).
Only real node subprocess.run() sites changed — mock signatures, HTTP urlopen,
and deliberate timeout-behavior tests (test_api_timeout, timeout=0/1/2) untouched.
Two coherent fixes for the #4346 transcript-virtualization scroll-jump
cluster, shipped together (contributor-approved) because both touch
static/ui.js + the same virtualization test file and interact (the
cache-reset path uses the per-role default from #4367). Both only run when
the default-OFF experimental "Virtualize long transcripts" setting is on.
#4367 — per-role virtual scroll height estimates:
Replaces the flat 140px unmeasured-row estimate with per-role fallbacks
(user 120 / assistant 160 / tool_call 400 / default 140) via
_messageVirtualDefaultHeightForRole + _messageVirtualRoleForEntry, so
tool-call rows (which measure 500-2000px) aren't under-counted before
measurement. Additive + backward-compat (opts.roleForIdx optional → flat
default when absent). The two sibling offset calculators route through the
per-role defaults too.
#4368 — anchor-based scroll compensation during measurement:
Adds _compensateScrollForMeasurementDelta (capture anchor + scrollTop →
render → re-pin scrollTop by the anchor row's measured delta; null/missing/
<2px guards; programmatic-scroll flag managed) wrapping
renderMessages({preserveScroll:true}) in _scheduleMessageVirtualizedRender,
plus a 150ms scroll-active guard deferring measurement refresh while the user
scrolls, and session-switch cleanup of the three scroll-settle globals.
Maintainer note: combining the two required the test harness for
test_clear_height_cache_resets_scroll_settle_globals (#4368) to also eval
_messageVirtualDefaultHeightForRole + define MESSAGE_VIRTUAL_DEFAULT_ROW_HEIGHTS
(#4367), since the combined _clearMessageVirtualHeightCache resets the estimate
via the per-role helper. Both PRs' test sets retained.
Co-authored-by: Rod Boev <rod.boev@gmail.com>
Adds a Preferences setting 'Virtualize long transcripts' (default ON, opt-out).
When disabled, _currentMessageVirtualWindow returns a full non-virtualized window
so the entire transcript renders at once (also short-circuits the virtual
re-measure loop). Server-persisted (virtualize_transcript bool, default True),
hot-applies via renderMessages on toggle. i18n in all 13 locales.
Pairs with @rodboev's #4328 stream-end anchor scroll fix (cherry-picked into
this branch) to fully resolve#4325.
* perf(chat): virtualize long message transcripts (#500)
* fix(chat): preserve virtual height cache on append (#500)
* fix(chat): bound virtual tail follow-up (#500)
* fix(chat): surface older history and bound virtual rerenders (#500)
* fix(chat): stabilize virtual history fallback (#500)
* fix(chat): preserve virtual compression anchors (#500)
* Keep virtualization regressions aligned with rebased sources (#500)
* fix(chat): preserve virtual heights across prepends (#500)
* test(update): isolate restart timing checks (#814)
* fix(chat): restore render contracts around virtual cache
* fix(chat): preserve legacy assistant anchor predicates
* fix(#3714): restore worklog-disclosure capture after HTML-cache early-return
Deep-review finding: the capture of _captureWorklogDetailDisclosureState was
moved before the HTML-cache fast-path return, failing the invariant test
test_render_rebuild_preserves_worklog_detail_disclosure_click_state (it must
not traverse the previous-session DOM when the cache path returns early).
Moved it back to just before inner.innerHTML='' (after the cache return).
Co-authored-by: Rod Boev <rod.boev@gmail.com>
* fix(#3714): restore Transparent Stream rehydration in HTML-cache fast path
Full-suite gate caught test_issue3820_chat_activity_display_mode failure: the
virtualization rewrite of the renderMessages HTML-cache fast-path block dropped
the _rehydrateTransparentStreamDom(inner) call (master has it; PR had 2 of 3
occurrences). On a cache-restore session switch, Transparent Stream controls
lost their property-assigned handlers. Restored the call in the same position
master has it (right after _sessionHtmlCacheSid=sid).
Co-authored-by: Rod Boev <rod.boev@gmail.com>
* fix(#3714): preserve live turn on scroll-rebuild even with zero streamed text
Opus ship-review residual (now fixed, not deferred): the live-turn re-attach
after a scroll-triggered virtualized rebuild was gated on _preservedLen>0, so a
turn showing only Activity/tool/worklog blocks with no streamed text yet (mid
tool-call) would not be preserved — scrolling up into history on a >80-row
transcript mid-stream could blink those live-only blocks for a frame. Broaden
the gate to also restore when the preserved turn has MORE structural blocks than
the rebuilt (lagging-S.messages) turn. Hoisted the _structuralCount helper above
the gate; whole-turn-vs-segment restore logic unchanged.
Co-authored-by: Rod Boev <rod.boev@gmail.com>
* chore(release): stamp v0.51.423 (Release OJ) for #500 transcript virtualization
---------
Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>