From 14959482b22534df78aab9f7bc656dcba0f3c353 Mon Sep 17 00:00:00 2001 From: klopez4212 Date: Sun, 28 Jun 2026 11:56:42 +0100 Subject: [PATCH] Fix task conversation scroll reset --- .../agents/ui/AgentConversationScreen.tsx | 2 +- .../src/features/channels/ui/ChannelPane.tsx | 1 - .../features/messages/ui/MessageTimeline.tsx | 23 +++++++++---------- .../features/messages/ui/useAnchoredScroll.ts | 4 ++-- desktop/tests/e2e/stream.spec.ts | 6 ++++- 5 files changed, 19 insertions(+), 17 deletions(-) diff --git a/desktop/src/features/agents/ui/AgentConversationScreen.tsx b/desktop/src/features/agents/ui/AgentConversationScreen.tsx index d1daf6f67..c6d110d7b 100644 --- a/desktop/src/features/agents/ui/AgentConversationScreen.tsx +++ b/desktop/src/features/agents/ui/AgentConversationScreen.tsx @@ -811,10 +811,10 @@ export function AgentConversationScreen({ emptyTitle="No conversation messages yet" hasComposerOverlay isLoading={messagesQuery.isLoading && timelineMessages.length === 0} - layoutShiftKey={conversation.id} messageListPlacement="top" messages={timelineMessages} profiles={profiles} + scrollResetKey={conversation.id} scrollContainerRef={timelineScrollRef} showInitialDayDivider={false} trailingContent={ diff --git a/desktop/src/features/channels/ui/ChannelPane.tsx b/desktop/src/features/channels/ui/ChannelPane.tsx index e399ed02b..22a6bf358 100644 --- a/desktop/src/features/channels/ui/ChannelPane.tsx +++ b/desktop/src/features/channels/ui/ChannelPane.tsx @@ -794,7 +794,6 @@ export const ChannelPane = React.memo(function ChannelPane({ hasComposerOverlay={hasMainComposerOverlay} hasOlderMessages={hasOlderMessages} isFetchingOlder={isFetchingOlder} - layoutShiftKey={useSplitAuxiliaryPane ? "split" : "overlay"} isFollowingThreadById={isFollowingThreadById} isMessageUnreadById={isMessageUnreadById} personaLookup={personaLookup} diff --git a/desktop/src/features/messages/ui/MessageTimeline.tsx b/desktop/src/features/messages/ui/MessageTimeline.tsx index 34960515b..f1f7111c4 100644 --- a/desktop/src/features/messages/ui/MessageTimeline.tsx +++ b/desktop/src/features/messages/ui/MessageTimeline.tsx @@ -56,7 +56,7 @@ type MessageTimelineProps = { hasComposerOverlay?: boolean; contentTopPadding?: "chrome" | "compact"; isFetchingOlder?: boolean; - layoutShiftKey?: string | number | null; + scrollResetKey?: string | number | null; messageListPlacement?: "bottom" | "top"; messageFooters?: Record; /** Map from lowercase pubkey → persona display name for bot members. */ @@ -164,7 +164,7 @@ const MessageTimelineBase = React.forwardRef< contentTopPadding = "chrome", hasOlderMessages = true, isFetchingOlder = false, - layoutShiftKey = null, + scrollResetKey = null, messageListPlacement = "bottom", followThreadById, huddleMemberPubkeys, @@ -233,16 +233,15 @@ const MessageTimelineBase = React.forwardRef< liveSnapshot, }); const isRenderPending = deferredSnapshot !== liveSnapshot; - const scrollRouteKey = `${channelId ?? "none"}:${layoutShiftKey ?? "none"}`; + const scrollIdentityKey = `${channelId ?? "none"}:${scrollResetKey ?? "none"}`; const scrollRestorationId = targetMessageId - ? `message-timeline:${scrollRouteKey}:target:${targetMessageId}` - : `message-timeline:${scrollRouteKey}`; - // Keep the scroll node's DOM lifetime scoped to a channel. TanStack Router's - // scroll-restoration listener runs outside React and may write a saved - // scrollTop into the current scroll element during navigation; reusing the - // same node across channel routes can leave the newly-loaded message list - // painted at a stale offset until the user's next scroll event forces layout. - const scrollContainerDomKey = scrollRouteKey; + ? `message-timeline:${scrollIdentityKey}:target:${targetMessageId}` + : `message-timeline:${scrollIdentityKey}`; + // Keep the scroll node's DOM lifetime scoped to the rendered conversation. + // Channel layout changes (for example opening the thread panel) should keep + // this stable, while switching task conversations should start from a fresh + // scroll state. + const scrollContainerDomKey = scrollIdentityKey; const timelineBodySurface = selectTimelineBodySurface({ deferredCount: deferredMessages.length, @@ -265,7 +264,7 @@ const MessageTimelineBase = React.forwardRef< isLoading: showTimelineSkeleton, messages: deferredMessages, onTargetReached, - resetKey: scrollRouteKey, + resetKey: scrollIdentityKey, scrollContainerRef, targetMessageId, }); diff --git a/desktop/src/features/messages/ui/useAnchoredScroll.ts b/desktop/src/features/messages/ui/useAnchoredScroll.ts index 6d8e89ecc..eaf3d0ede 100644 --- a/desktop/src/features/messages/ui/useAnchoredScroll.ts +++ b/desktop/src/features/messages/ui/useAnchoredScroll.ts @@ -38,10 +38,10 @@ type UseAnchoredScrollOptions = { /** Inner content element — must wrap every renderable row, including the * sentinel and bottom anchor. Used to schedule layout work on resize. */ contentRef: React.RefObject; - /** Resets when changed; lets us drop anchor + scroll state across channels. */ + /** Scopes channel-level side effects and observer subscriptions. */ channelId?: string | null; /** Resets when changed; includes channel plus route-specific layout state. */ - resetKey?: string | null; + resetKey?: string | number | null; /** Suppresses initial scroll-to-bottom while a skeleton is showing. */ isLoading: boolean; /** Source of truth for the rendered list. Used to detect new-at-bottom diff --git a/desktop/tests/e2e/stream.spec.ts b/desktop/tests/e2e/stream.spec.ts index a8766cadb..b74c88582 100644 --- a/desktop/tests/e2e/stream.spec.ts +++ b/desktop/tests/e2e/stream.spec.ts @@ -42,10 +42,14 @@ async function ensureTimelineScrollable( ) { const input = senderPage.getByTestId("message-input"); const sendButton = senderPage.getByTestId("send-message"); + const minimumSeedMessages = 8; for (let index = 0; index < 24; index += 1) { const metrics = await getTimelineMetrics(receiverPage); - if (metrics.scrollHeight > metrics.clientHeight + 160) { + if ( + index >= minimumSeedMessages && + metrics.scrollHeight > metrics.clientHeight + 160 + ) { return; }