Skip to content

feat(app): stabilize long session timelines#32331

Open
Hona wants to merge 13 commits into
anomalyco:devfrom
Hona:refactor/tanstack-virtual-session
Open

feat(app): stabilize long session timelines#32331
Hona wants to merge 13 commits into
anomalyco:devfrom
Hona:refactor/tanstack-virtual-session

Conversation

@Hona

@Hona Hona commented Jun 14, 2026

Copy link
Copy Markdown
Member

TLDR;

  • Long desktop sessions now scroll without visible messages jumping, overlapping, blanking, or loading late.
  • Streaming code highlighting moves off the main thread and updates without replacing already-rendered code.
  • History loading, expanding tool content, and scrollbar dragging preserve the content the user is looking at.

Changelog

Added

  • Streaming code blocks now update syntax highlighting as code arrives while retaining the existing code block and token DOM.

Fixed

  • Scrolling through long sessions with mixed Markdown, reasoning, tool output, and diffs no longer causes visible messages to jump by unrelated distances.
  • Loading older session history now keeps the current visible message in the same screen position while the new history finishes rendering.
  • Expanding explored context or other dynamically sized content no longer paints over the following message while its height is measured.
  • Dragging the desktop scrollbar thumb no longer moves in the opposite direction when session content changes height during the drag.
  • Collapsed tool sections remain collapsed when later assistant content streams into the same turn.
  • Rendered diffs remain mounted when sibling content or diff counts update, avoiding visible resets.
  • Streaming text deltas now append to the text already stored for the message instead of replacing it with only the latest delta.

Changed

  • Long session timelines now use TanStack Virtual with stable semantic row keys and additional render lead for asynchronous Markdown and diff content.
  • Syntax highlighting for streaming Markdown code runs in a dedicated worker, isolated from diff rendering work.
  • The timeline keeps a measured visual anchor only while older history is being inserted and immediately yields to new wheel, touch, or pointer input.
  • Diff rendering uses Pierre 1.2.10 and syntax highlighting uses Shiki 4.2.0.

Perf

Benchmark setup used the same streaming-session workload at 30x CPU throttling unless noted. These numbers describe development profiling, not a CI performance gate.

  1. Production/Virtua calibration: approximately 4.84-4.90 FPS. This early baseline was partly affected by the text-delta accumulator bug, so it is directional rather than a clean final comparison.
  2. Dedicated Shiki worker with compact token tuples: 45.7 FPS, p50 16.8ms, p95 34.3ms, p99 67ms, 22 frames below 20 FPS, 288 dropped-frame equivalents, and a longest slow-frame streak of 2.
  3. Worker tokenizer batching/coalescing: rejected because p99, maximum frame time, and long tasks regressed. No comparable result is claimed.
  4. Same-style token merging: rejected because average FPS and tail latency regressed. No comparable result is claimed.
  5. Keeping dense streamed tokens after a fence closed: rejected because layout and paint performance regressed.
  6. Custom diff-height estimators and Pretext-style reservation: rejected because scrollbar range and anchor stability regressed; these were not retained as FPS candidates.
  7. Pierre 1.2.10 parsePatchFiles: approximately 2.3x faster in the isolated parser benchmark. This is not a timeline FPS measurement.
  8. Final cleaned PR: not re-benchmarked after removing profiling instrumentation, so 45.7 FPS remains the latest verified FPS result rather than a claim about the exact final commit.

Verification

  • 377 app unit tests
  • 39 UI unit tests
  • 7 focused timeline browser tests
  • Repository-wide typecheck: 23 tasks passed
  • Production app build
  • Frozen dependency install
  • git diff --check

@Hona Hona requested a review from Brendonovich as a code owner June 14, 2026 15:34
Copilot AI review requested due to automatic review settings June 14, 2026 15:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves the desktop session timeline’s scroll stability and rendering performance for long, mixed-content conversations by migrating timeline virtualization to TanStack Virtual, tightening anchor/scroll preservation during history prepends, and moving streaming Markdown code highlighting off the main thread (while keeping code block DOM stable during streaming updates).

Changes:

  • Replace virtua timeline virtualization with @tanstack/solid-virtual using stable semantic row keys, overscan/lead tuning, and scroll anchoring for prepend/append scenarios.
  • Add streaming Markdown code highlighting via a dedicated Shiki worker and incremental token/DOM updates (plus updated Shiki/Pierre versions).
  • Improve scrollbar thumb dragging logic to remain monotonic when content height changes; expand E2E coverage for scroll stability regressions.

Reviewed changes

Copilot reviewed 30 out of 31 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
patches/virtua@0.49.1.patch Removes the repo’s virtua patch now that the timeline moves away from virtua.
patches/@TanStack%2Fsolid-virtual@3.13.28.patch Patches Solid Virtual to stabilize updates/reconciliation behavior for virtual items.
packages/ui/src/pierre/virtualizer.ts Updates Pierre virtual metrics API usage (fileGapspacing).
packages/ui/src/pierre/index.ts Adjusts diff CSS variable overrides for Pierre 1.2 behavior.
packages/ui/src/context/marked.tsx Exports OpenCodeTheme for reuse (worker init), plus minor theme updates.
packages/ui/src/components/scroll-view.tsx Reworks thumb-drag scrolling math to avoid inverted motion during content height changes.
packages/ui/src/components/scroll-view.test.ts Adds unit tests for the new thumb-drag scroll mapping/clamping.
packages/ui/src/components/message-part.tsx Introduces onContentRendered plumbing to notify parent virtualizers when async-sized content finishes rendering.
packages/ui/src/components/markdown.tsx Refactors Markdown rendering into keyed blocks; adds streaming code highlighting with stable code DOM and incremental token updates.
packages/ui/src/components/markdown.css Adds margin normalization for block-wrapped Markdown rendering.
packages/ui/src/components/markdown-worker.ts Adds worker lifecycle + request/response management for streaming code highlighting.
packages/ui/src/components/markdown-worker-protocol.ts Defines the worker protocol and stable/unstable token accumulation logic.
packages/ui/src/components/markdown-worker-protocol.test.ts Adds unit tests for worker token accumulation/reset semantics.
packages/ui/src/components/markdown-stream.ts Adds block projection to avoid re-parsing frozen blocks and to stream code fences more efficiently.
packages/ui/src/components/markdown-stream.test.ts Extends tests for new block/projection behavior and code-fence streaming.
packages/ui/src/components/markdown-shiki.worker.ts Implements the Shiki tokenizer/highlighter inside a dedicated worker.
packages/ui/src/components/file.tsx Updates Pierre virtual metrics usage (fileGapspacing).
packages/ui/package.json Adds @shikijs/stream; removes virtua dependency from UI workspace.
packages/app/src/pages/session/message-timeline.tsx Rebuilds timeline virtualization with TanStack Virtual; adds prepend anchor capture/restore and async content measurement integration.
packages/app/src/pages/session/message-timeline.data.ts Adds a TurnGap row type and refactors spacing logic away from frame padding flags.
packages/app/src/pages/session.tsx Hooks history loader into timeline anchor capture/restore; adjusts autoscroll integration (including overflowAnchor: "none").
packages/app/src/context/global-sync/event-reducer.ts Fixes streaming text delta accumulation to start from existing part content instead of only the latest delta.
packages/app/src/context/global-sync/event-reducer.test.ts Adds regression test ensuring delta accumulation initializes from current part text.
packages/app/package.json Adds @tanstack/solid-virtual; removes virtua from app workspace dependencies.
packages/app/e2e/utils/mock-server.ts Adds message delay hooks and SSE retry: support to help reproduce timing-sensitive timeline scenarios.
packages/app/e2e/smoke/session-timeline.spec.ts Adds smoke test asserting visible message position is preserved while history is prepended; verifies bottom spacer behavior.
packages/app/e2e/regression/session-timeline-context-resize.spec.ts Removes logging; tweaks sampling to capture post-paint overlap behavior.
packages/app/e2e/regression/session-timeline-collapse-state.spec.ts Strengthens regression assertions around diff/tool row stability; adds sticky header alignment regression test.
package.json Bumps @pierre/diffs to 1.2.10, shiki to 4.2.0; adds @shikijs/stream and @tanstack/solid-virtual; removes virtua; wires patch for Solid Virtual.
bunfig.toml Expands minimumReleaseAgeExcludes to include Pierre packages (diffs/theming).
bun.lock Updates lockfile for dependency bumps/removals and patched dependencies list.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/app/src/pages/session.tsx Outdated
Comment on lines 112 to 117
while (true) {
await input.loadMore(id)
input.onAfterLoad?.()
if (input.sessionID() !== id) return

const nextLoaded = input.loaded()
Comment on lines +565 to +573
onBeforeElUpdated: (fromEl, toEl) => {
if (
fromEl instanceof HTMLButtonElement &&
toEl instanceof HTMLButtonElement &&
fromEl.getAttribute("data-slot") === "markdown-copy-button" &&
toEl.getAttribute("data-slot") === "markdown-copy-button"
) {
return false
}
Comment on lines +595 to +613
const code = existing?.querySelector("code")
if (code instanceof HTMLElement) {
code.className = `language-${block.language}`
const previous = renderedCodeTokens.get(next)
const reset = !previous || previous.language !== block.language || block.stable.length < previous.stableCount
const stableCount = reset ? 0 : previous.stableCount
const tail = [...block.stable.slice(stableCount), ...block.unstable]
const prior = reset ? [] : previous.unstable
const prefix = prior.findIndex((token, index) => !sameToken(token, tail[index]))
const keep = stableCount + (prefix < 0 ? Math.min(prior.length, tail.length) : prefix)
while (code.children.length > keep) code.lastElementChild?.remove()
tail.slice(keep - stableCount).map(createTokenSpan).forEach((span) => code.appendChild(span))
renderedCodeTokens.set(next, {
language: block.language,
stableCount: block.stable.length,
unstable: block.unstable,
})
return
}
@Hona Hona added the beta label Jun 14, 2026
@Hona Hona added the beta label Jun 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants