Skip to content

fix(observer): align scroll-anchor ids with transcript display-block keys#1849

Merged
wpfleger96 merged 5 commits into
mainfrom
duncan/observer-scroll-ids
Jul 14, 2026
Merged

fix(observer): align scroll-anchor ids with transcript display-block keys#1849
wpfleger96 merged 5 commits into
mainfrom
duncan/observer-scroll-ids

Conversation

@wpfleger96

@wpfleger96 wpfleger96 commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

Summary

The observer feed's useAnchoredScroll hook was fed raw seq:timestamp ids while the inner AgentSessionTranscriptList rendered aggregated transcript block keys (turn:<id>, session-boundary:..., item ids). This id-namespace divergence caused the hook's floor writers to fire on every raw event against a DOM that barely changed, producing flicker and jump-to-tail during mid-history reading.

This PR fixes the divergence in both surfaces that wire useAnchoredScroll to observer transcript blocks:

  1. Thread panel (AgentSessionThreadPanel) — derives display-block keys from raw events via the production chain (buildTranscriptStatebuildTranscriptDisplayBlocksgetDisplayBlockKey), stabilized by value so the messages reference only changes when the ordered block-id sequence changes. View mode (raw vs transcript) folded into the reset key for clean re-initialization on toggle.

  2. autoTail consumers (AgentSessionTranscriptList, used by profile live-activity cards) — the same disjoint-namespace bug existed here: the hook was fed TranscriptItem ids while the DOM rendered getDisplayBlockKey(block). Fixed by deriving block keys from the already-computed displayBlocks array, value-stabilized with useStableArrayShallow. Non-autoTail behavior (hook disabled via channelId: null) is unchanged.

Additional cleanup: raw-mode derivation skip (don't run deriveTranscriptBlockIds when showRawFeed is true), buildTranscriptState widened to readonly ObserverEvent[] (eliminating the cast in deriveTranscriptBlockIds), and the mid-history zero-write test strengthened to actually latch mid-history via onScroll.

useAnchoredScroll.ts is byte-zero — untouched.

Changes

  • agentSessionTranscriptGrouping.ts — export deriveTranscriptBlockIds helper (production derivation chain); drop as ObserverEvent[] cast now that buildTranscriptState accepts readonly.
  • agentSessionTranscript.ts — widen buildTranscriptState and buildTranscript to accept readonly ObserverEvent[].
  • AgentSessionTranscriptList.tsx — derive block keys from displayBlocks via getDisplayBlockKey, stabilize with useStableArrayShallow, feed to useAnchoredScroll instead of raw items. Only active when autoTail is true.
  • AgentSessionThreadPanel.tsx — skip deriveTranscriptBlockIds when showRawFeed is true; derive transcript block ids, stabilize, fold mode into reset key.
  • useAnchoredScroll.observerScrollIds.test.mjs — mid-history test now latches mid-history via double onScroll (clear settle guard + latch anchor) with a fake [data-message-id] row, matching the pattern in the "new id mid-history" test.

What this fixes

  • Mid-history reading is never yanked to tail (both thread panel and autoTail surfaces)
  • At-bottom tail glue works without per-raw-event React floor writes
  • Profile live-activity cards (autoTail consumers) no longer flicker
  • Deterministic pin on feed open preserved (fix(desktop): unify observer feed scroll onto useAnchoredScroll #1825's win)
  • Raw mode unchanged; unnecessary transcript derivation skipped in raw mode
  • Archive load-older holds reading position

Fixes the v0.4.2 regression introduced by #1825.

…keys

The outer AgentSessionThreadPanel fed useAnchoredScroll raw seq:timestamp
ids while the inner transcript rendered aggregated block keys (turn:<id>,
session-boundary:..., item ids). Disjoint namespaces caused the floor
writers to fire on every raw event against a DOM that barely changed,
producing flicker and jump-to-tail during mid-history reading.

Derive the same display-block keys in the outer panel via
buildTranscriptState → buildTranscriptDisplayBlocks → getDisplayBlockKey,
stabilize the string[] by value with useStableArrayShallow so the hook's
messages reference only changes when the block id sequence actually
changes, and fold the view mode into the hook reset key so toggling
raw ↔ transcript re-initializes cleanly.

Fixes the v0.4.2 regression introduced by #1825.
@wpfleger96 wpfleger96 requested a review from a team as a code owner July 14, 2026 08:38
…level tests

The original tests used pure derivation assertions that would stay green
if useStableArrayShallow were removed, mode were dropped from the reset
key, or data-message-id rendering stopped entirely. Replace them with
behavior-level coverage that exercises the actual production wiring:

- Reference stability: verify useStableArrayShallow preserves the {id}[]
  reference when ordered block ids are value-equal, and propagates a new
  reference when they change (revert-proven: bypassing stabilization
  fails test 7).
- Hook-level zero-write: render useAnchoredScroll with an instrumented
  scrollTo spy; same ids at-bottom → zero writes, same ids mid-history →
  zero writes, new id at-bottom → one floor write, new id mid-history →
  no floor write.
- Ordered DOM parity: derive block keys and compare ordered sequences per
  commit including the transient [turn, single] → [single, turn] reorder.
- Mode-toggle reset: exercise channelId changes (transcript ↔ raw) in
  loaded and connecting→loaded states, assert re-pin behavior.
… events

D2: Extract the derivation chain (buildTranscriptState → buildTranscriptDisplayBlocks
→ getDisplayBlockKey) into an exported deriveTranscriptBlockIds helper in
agentSessionTranscriptGrouping.ts. AgentSessionThreadPanel now calls this
helper — tests exercise the same production code path from raw ObserverEvents
instead of pre-built TranscriptItems.

D1: AgentSessionTranscriptList cannot render under node:test — the component
tree transitively imports .css (BuzzLogoAnimation.tsx → buzz-logo-animation.css)
and the test-loader has no CSS stub. Named residual documented in test file;
the structural guarantee (both sides call the same getDisplayBlockKey) is
tested, but full-component render coverage requires a test-loader change.
…then tests

Fix the same disjoint-namespace scroll-id bug in AgentSessionTranscriptList's
autoTail path (profile live-activity cards) that was already fixed in the
thread panel: derive block keys from displayBlocks and feed value-stabilized
ids to useAnchoredScroll. Skip deriveTranscriptBlockIds when showRawFeed is
true (unnecessary work). Strengthen the mid-history zero-write test to
actually latch mid-history via onScroll. Widen buildTranscriptState to
readonly ObserverEvent[] and drop the cast in deriveTranscriptBlockIds.
@wpfleger96 wpfleger96 merged commit 1df4a4e into main Jul 14, 2026
25 checks passed
@wpfleger96 wpfleger96 deleted the duncan/observer-scroll-ids branch July 14, 2026 14:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant