perf(app): stabilize long session timelines#402
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
✅ Files skipped from review due to trivial changes (1)
📝 WalkthroughWalkthroughAdds a standalone event coalescer for queued SDK events and integrates it into the global SDK buffer. Separately, session history/windowing gains a mode-aware model ("bottom" | "reading" | "hash"), new control helpers and APIs, updated hash-scroll / timeline wiring, and expanded tests. ChangesEvent Coalescing & SDK Integration
Mode-Aware History Window & Hash Scroll Wiring
Sequence DiagramsequenceDiagram
participant User
participant SDK as SDK Stream
participant Queue as Event Queue
participant Coalesce as coalesceQueuedEvents
participant Emitter as Event Emitter
participant History as HistoryWindow
participant Timeline as Timeline Render
User->>SDK: streaming response events
SDK->>Queue: enqueue `QueuedGlobalEvent` (delta/update/status)
SDK->>Queue: schedule flush
Queue->>Coalesce: coalesceQueuedEvents(queue)
Note over Coalesce: replaceables overridden,\nstale deltas removed, contiguous deltas concatenated
Coalesce-->>Emitter: emit coalesced events
Emitter->>History: deliver events
alt mode == "bottom"
History->>Timeline: compute bottom window
else mode == "reading"
History->>Timeline: expand for reading (preserve view)
else mode == "hash"
History->>Timeline: pin hash target
end
Timeline->>User: render messages
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Review rate limit: 2/10 reviews remaining, refill in 46 minutes and 24 seconds. Comment |
There was a problem hiding this comment.
Code Review
This pull request introduces a centralized event coalescing mechanism for the SDK event queue and refines session history window management by implementing explicit modes (bottom, reading, and hash) to handle message appends and scrolling more effectively. A performance improvement was suggested for the coalesceQueuedEvents function to avoid redundant calls to rebuildReplaceable unless a removal actually occurs, preventing potential performance bottlenecks.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
packages/app/src/context/global-sdk-event-queue.test.ts (1)
37-73: ⚡ Quick winAdd a directory-isolation regression test.
Current coverage doesn’t verify that same
messageID/partID/fieldacross differentdirectoryvalues are never merged or pruned together. Adding this is low-cost and protects multi-session behavior explicitly.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/app/src/context/global-sdk-event-queue.test.ts` around lines 37 - 73, Add a regression test that asserts directory isolation: within the same describe block add a test that uses coalesceQueuedEvents with queued events created via queued(delta(...), updated(...), status(...)) where the deltas/updates share the same messageID/partID/field but have different directory values; verify using eventTypes(events) and deltas(events) that events from different directory values are never merged or pruned together (i.e., they remain separate entries and stale-to-updated pruning only applies within the same directory), referencing the existing helpers coalesceQueuedEvents, queued, delta, updated, status, eventTypes and deltas to locate where to add and what to assert.packages/app/src/pages/session/use-session-history-window.test.ts (1)
87-89: 💤 Low valueTest name slightly misaligned with test case.
The test is named "reading mode does not slide the window..." but actually tests
mode: "bottom"withuserScrolled: true. This tests the pre-transition behavior (bottom mode respecting scroll position) rather than reading mode itself. The test is valid but the name could be clearer.Consider renaming to clarify it tests the bottom-mode-with-scroll-freeze behavior:
- test("reading mode does not slide the window when the user scrolls upward inside the rendered range", () => { + test("bottom mode freezes window position when user has scrolled", () => {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/app/src/pages/session/use-session-history-window.test.ts` around lines 87 - 89, Rename the test whose body calls resolveHistoryTurnStart({ mode: "bottom", storedTurnStart: 15, length: 26, userScrolled: true }) so its description reflects bottom-mode scroll-freeze behavior instead of "reading mode" (e.g., change the test name string from "reading mode does not slide the window when the user scrolls upward inside the rendered range" to something like "bottom mode preserves storedTurnStart when user has scrolled inside rendered range"); update only the test description text associated with that assertion to match the actual scenario using resolveHistoryTurnStart and leave the assertion logic unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/app/src/context/global-sdk-event-queue.ts`:
- Around line 66-72: The loop that prunes stale deltas on message.part.updated
currently scans the entire result array (in global-sdk-event-queue.ts inside the
handler using partKey(event) and rebuildReplaceable), causing O(n²) behavior
under many updates; instead maintain an index (e.g., a Map keyed by partKey)
that maps each partKey to the queued delta entries (or their indices/IDs) so
that when handling a "message.part.updated" you only look up and remove matching
deltas from result via that map, and keep the map updated whenever you
push/splice entries and call rebuildReplaceable to preserve correctness.
---
Nitpick comments:
In `@packages/app/src/context/global-sdk-event-queue.test.ts`:
- Around line 37-73: Add a regression test that asserts directory isolation:
within the same describe block add a test that uses coalesceQueuedEvents with
queued events created via queued(delta(...), updated(...), status(...)) where
the deltas/updates share the same messageID/partID/field but have different
directory values; verify using eventTypes(events) and deltas(events) that events
from different directory values are never merged or pruned together (i.e., they
remain separate entries and stale-to-updated pruning only applies within the
same directory), referencing the existing helpers coalesceQueuedEvents, queued,
delta, updated, status, eventTypes and deltas to locate where to add and what to
assert.
In `@packages/app/src/pages/session/use-session-history-window.test.ts`:
- Around line 87-89: Rename the test whose body calls resolveHistoryTurnStart({
mode: "bottom", storedTurnStart: 15, length: 26, userScrolled: true }) so its
description reflects bottom-mode scroll-freeze behavior instead of "reading
mode" (e.g., change the test name string from "reading mode does not slide the
window when the user scrolls upward inside the rendered range" to something like
"bottom mode preserves storedTurnStart when user has scrolled inside rendered
range"); update only the test description text associated with that assertion to
match the actual scenario using resolveHistoryTurnStart and leave the assertion
logic unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 4f33e216-d857-4c70-95fb-82a46f7d00dc
📒 Files selected for processing (7)
packages/app/src/context/global-sdk-event-queue.test.tspackages/app/src/context/global-sdk-event-queue.tspackages/app/src/context/global-sdk.tsxpackages/app/src/pages/session/use-session-hash-scroll.tspackages/app/src/pages/session/use-session-history-window.test.tspackages/app/src/pages/session/use-session-history-window.tspackages/app/src/pages/session/use-session-timeline-interaction.ts
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/app/src/pages/session/use-session-history-window.test.ts`:
- Around line 16-40: The harness uses plain locals (messages, userScrolled) so
Solid reactivity in createSessionHistoryWindow never sees changes; update
createHarness to expose reactive sources (e.g., use createSignal or createStore)
for messages and userScrolled and pass their getter functions into
createSessionHistoryWindow (so visibleUserMessages and userScrolled read the
reactive getters), then implement setCount and setUserScrolled to call the
corresponding signal/setter to trigger memo/effect invalidation in the hook.
In `@packages/app/src/pages/session/use-session-history-window.ts`:
- Around line 276-284: The effect currently collapses every non-"bottom" mode
into returnToLatestIfFollowing(), which causes "hash" mode to be dropped
unintentionally; update the branches that check userScrolled and state.mode so
that returnToLatestIfFollowing() (and any implicit resumeLatestWindow behavior)
only runs when state.mode === "reading" (or when explicitly resuming), and do
not auto-exit when state.mode === "hash"; adjust the if (!userScrolled &&
state.mode !== "bottom") branch and the final fallback to guard on state.mode
=== "reading" instead of all non-"bottom" modes so "hash" remains sticky until
an explicit clear/resume action.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: f2d6ef5a-5072-4ed2-acba-0a9681e747c3
📒 Files selected for processing (2)
packages/app/src/pages/session/use-session-history-window.test.tspackages/app/src/pages/session/use-session-history-window.ts
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/app/src/context/global-sdk-event-queue.ts`:
- Around line 5-26: The composite key helpers deltaKey, partKey and
replaceableKey currently join components with ":" which can collide if any
component (especially event.directory) contains ":"; update these functions to
produce unambiguous keys by encoding or serializing components (e.g., return
JSON.stringify([event.directory, props.messageID, props.partID, props.field])
for deltaKey, JSON.stringify([event.directory, props.messageID, props.partID])
for partKey, and JSON.stringify(["session.status", event.directory,
event.payload.properties.sessionID]) or JSON.stringify(["lsp.updated",
event.directory]) for replaceableKey) so keys are deterministic and
collision-free for all QueuedGlobalEvent payload shapes. Ensure all branches
return a string (not undefined) and keep the same semantics for different
payload types.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: ccc45e2b-c67a-4595-bf6f-d770eb3d9141
📒 Files selected for processing (3)
packages/app/src/context/global-sdk-event-queue.test.tspackages/app/src/context/global-sdk-event-queue.tspackages/app/src/pages/session/use-session-history-window.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/app/src/pages/session/use-session-history-window.test.ts
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/app/src/pages/session/use-session-history-window.ts (1)
159-183:⚠️ Potential issue | 🟠 Major | ⚡ Quick winGuard async history expansion against mode changes.
Both async
loadMorecontinuations unconditionally re-enterreadingmode after the await. If the user has already resumed bottom-following or jumped to a hash target while the request is in flight, the late completion still callsexpandForReading(...)and can yank the timeline back out of the current mode.Suggested fix
+ const requestedMode = mode() + while (true) { await input.loadMore(id) if (input.sessionID() !== id) return @@ if (opts?.prefetch) { + if (mode() !== requestedMode) return const current = turnStart() preserveScroll(() => expandForReading(current + growth)) return } @@ - preserveScroll(() => expandForReading(Math.max(0, afterVisible - target))) + if (mode() === requestedMode) { + preserveScroll(() => expandForReading(Math.max(0, afterVisible - target))) + }Apply the same guard in
loadAndRevealas well if you want the button path to preserve a later mode transition too.Also applies to: 205-244
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/app/src/pages/session/use-session-history-window.ts` around lines 159 - 183, The async continuation after awaiting input.loadMore(...) must abort if the UI mode or session changed while the request was in flight: add guards right after each await (the one in this function and the same spot in loadAndReveal) to return early unless input.sessionID() === id and the UI is still in reading-mode (i.e. turnStart() === 0 or whatever current-mode predicate you use); only then compute growth and call expandForReading(...). Ensure you also preserve the existing sessionID check and the state.prefetchNoGrowth update but do the mode/session checks before calling expandForReading.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Outside diff comments:
In `@packages/app/src/pages/session/use-session-history-window.ts`:
- Around line 159-183: The async continuation after awaiting input.loadMore(...)
must abort if the UI mode or session changed while the request was in flight:
add guards right after each await (the one in this function and the same spot in
loadAndReveal) to return early unless input.sessionID() === id and the UI is
still in reading-mode (i.e. turnStart() === 0 or whatever current-mode predicate
you use); only then compute growth and call expandForReading(...). Ensure you
also preserve the existing sessionID check and the state.prefetchNoGrowth update
but do the mode/session checks before calling expandForReading.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 7e57e62a-02e0-4f69-a00b-d849af920b1f
📒 Files selected for processing (5)
packages/app/src/context/global-sdk-event-queue.test.tspackages/app/src/context/global-sdk-event-queue.tspackages/app/src/pages/session/use-session-history-window.test.tspackages/app/src/pages/session/use-session-history-window.tspackages/app/src/pages/session/use-session-timeline-interaction.ts
✅ Files skipped from review due to trivial changes (1)
- packages/app/src/pages/session/use-session-timeline-interaction.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- packages/app/src/context/global-sdk-event-queue.ts
- packages/app/src/pages/session/use-session-history-window.test.ts
Summary
message.part.deltaevents safely before emitting them to the renderer.Why
Long-running sessions could accumulate too many rendered turns during streaming, making the timeline more likely to flicker, jump, or become expensive to update.
Related Issue
Part of #397. This PR covers the first-stage bounded rendering and safe delta coalescing work; broader follow-ups such as turn normalization, Markdown pacing, and true virtualization should stay in #397 or separate follow-up issues.
Human Review Status
Pending. A human should make the final merge decision after reviewing the final diff, verification evidence, and CI.
Review Focus
Risk Notes
Medium: this changes session timeline rendering behavior and global event flush preparation. The changes are intentionally scoped and covered by focused unit tests, but reviewers should pay close attention to scroll restoration and stream ordering.
How To Verify
Screenshots or Recordings
Manual screenshots were captured locally from the isolated Electron run. No permanent artifact is attached because this is a performance/behavior fix rather than a visual design change.
Checklist
dev, and my PR title and commit messages use Conventional Commits in EnglishMaintainer labeling requested: please add type
perf, scopeapp, and priorityP2if appropriate.Summary by CodeRabbit
New Features
Refactor
Tests