Skip to content

feat(desktop): agent transcript should open at the last page with zero visible scrolling (ChatGPT/Claude-style open) #331

Description

@agentfleets

When opening an agent transcript, the feed visibly scrolls/jumps to the last message — often several times in the first second — instead of simply opening at the last page the way ChatGPT/Claude conversations do. Traced through the code; the motion is a designed-in re-pin loop, and the commercial-grade fix is to make the first visible frame already be the bottom.

Root cause

Open sequence today (desktop/src/surfaces/AgentTranscript.tsx):

  1. Up to 500 events load (LOAD_TAIL = 500, :25,137), then Virtuoso mounts with initialTopMostItemIndex={{ index: liveData.length - 1, align: 'end' }} (:396). At mount all row heights are estimates.
  2. Over the next frames, rows measure and hydrate — markdown, code blocks, KaTeX, async variable fonts, and images rendered with no width/height attributes (desktop/src/ui/Markdown.tsx:41,43) — so real heights keep changing.
  3. Every measured height change fires totalListHeightChanged={pinBottom} (:402), which calls scrollToIndex({ index: n - 1, align: 'end' }) (:234-239). This is deliberate — the comment at :218-226 documents re-asserting the end on every height change until the user takes control.
  4. Each pin jumps to an offset computed from the current (still-stabilizing) measurements, so consecutive pins land at different offsets → the user watches the transcript jump/creep repeatedly until hydration settles. key={agentId} (:390) re-runs this whole sequence on every agent switch.

In short: the bottom is maintained by JS re-scrolling during layout, and on WebKit each correction is visible.

How commercial apps avoid it

  • The first visible frame is already the last page — no programmatic scrolling after reveal.
  • Anchoring is done by layout/browser scroll-anchoring, not JS: content grows upward above the fold without moving the viewport.
  • History is windowed: only the last page (~20–50 messages) renders on open; older pages load on scroll-up.
  • Media dimensions are reserved (aspect-ratio placeholders), so hydration doesn't change heights after reveal.

Suggested fixes (ordered by effort/impact)

1. Hide until settled, then reveal instantly (the ChatGPT effect, ~10 lines).
Render Virtuoso with visibility: hidden (layout preserved, so measuring works) until the bottom position has been asserted — e.g. flip a settled state on the first totalListHeightChanged after mount (or after a short rAF chain) — then reveal. The user's first visible frame is the bottom; the re-pin loop still runs but is invisible. No behavior change beyond the visual.

2. Reduce height churn at the source.

  • Give markdown images explicit dimensions or an aspect-ratio placeholder (Markdown.tsx:41,43) so image loads don't reflow rows.
  • Pass defaultItemHeight (≈ average card height) to Virtuoso so the initial offset estimate is close to final — fewer corrections on mount.

3. If the pin loop is kept, make it silent and sparse.
rAF-debounce pinBottom, skip when atBottomStateChange already reports bottom, and stop re-pinning once settled (new events while at bottom are already covered by followOutput; the pin loop's only real job is the hydration window).

4. Structural endgame — windowed history.
Open with tail: 50 and prepend older pages via startReached + firstItemIndex bookkeeping. The events endpoint is currently tail-only with no older-than cursor (comment at :19-25), so this needs a before_seq cursor on the hub — or client-side windowing (fetch 500, feed Virtuoso a growing slice; data is cheap, hydration churn is the cost).

5. Small consistency note.
The insight-mode Virtuoso (:490-497) has no initialTopMostItemIndex and opens at the top; decide its open position deliberately (top is defensible for a turns/errors navigator, but it should be a choice, not a default).

Acceptance criteria

  • Opening any agent transcript (including one with 500 events, images, and KaTeX) shows the last page in the first visible frame, with zero visible scroll motion.
  • Switching agents never shows intermediate scroll positions.
  • New streamed events still auto-scroll when at bottom, and never yank the view when the user has scrolled up (existing followOutput contract preserved).

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions