From 41adb3b6e843da193487eee3ef66ff160d89724e Mon Sep 17 00:00:00 2001 From: Naiyuan Qing <145280634+NevilleQingNY@users.noreply.github.com> Date: Fri, 24 Jul 2026 10:56:32 +0800 Subject: [PATCH] fix(views): anchor newest-first live transcript across prepends MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Virtuoso items opt out of native scroll anchoring, so in newest-first sort every live flush prepended rows and shifted the reading position. Compensate with a counting-down firstItemIndex (Virtuoso's prepend contract); sort/filter/task changes remount the list via an epoch key so the value never increases within one instance. scrollToIndex keeps data-relative indices — verified against scrollToIndexSystem, which never reads firstItemIndex. Co-Authored-By: Claude Fable 5 --- .../task-transcript/agent-transcript-dialog.tsx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/views/common/task-transcript/agent-transcript-dialog.tsx b/packages/views/common/task-transcript/agent-transcript-dialog.tsx index 30da1ba44..3c96bb150 100644 --- a/packages/views/common/task-transcript/agent-transcript-dialog.tsx +++ b/packages/views/common/task-transcript/agent-transcript-dialog.tsx @@ -287,6 +287,20 @@ export function AgentTranscriptDialog({ const isAntigravityLiveEmpty = isLive && displayItems.length === 0 && runtimeInfo?.provider === "antigravity"; + // Newest-first shows live events as PREPENDS, and Virtuoso items opt out of + // native scroll anchoring (`overflow-anchor: none`), so without compensation + // every 500ms flush shifts the reading position. Virtuoso's contract: a + // decrease of firstItemIndex by N anchors the viewport across an N-item + // prepend, and the value must never increase within an instance — counting + // down from a large base satisfies that while the list only grows. Sort, + // filter, or task changes can shrink the list, so `listEpoch` remounts the + // instance (fresh at top) instead of letting firstItemIndex climb. + // `scrollToIndex`/`computeItemKey` are unaffected: indices stay data-relative + // (verified against scrollToIndexSystem — it never reads firstItemIndex). + const firstItemIndex = + sortDirection === "newest_first" ? 1_000_000 - displayItems.length : 0; + const listEpoch = `${task.id}:${sortDirection}:${activeFilterKeys.join(",")}`; + const detailSeqs = useMemo( () => displayItems.filter(hasEventDetail).map((item) => item.seq), [displayItems], @@ -758,9 +772,11 @@ export function AgentTranscriptDialog({ // of DOM rows (#5733). Rows expand/collapse to variable heights; // Virtuoso re-measures them via ResizeObserver. item.seq} components={LIST_COMPONENTS} itemContent={(_, item) => (