refactor(chat): replace timeline scroll engine with anchored-turn LegendList
Sending a message now parks that message near the top of the viewport and streams the reply into reserved end space below it, instead of jumping to the bottom and chasing it. - swap @tanstack/react-virtual for @legendapp/list in the chat timeline; the streaming tail becomes a normal list row rather than a separately rendered block, so one component owns the scroll position - add timelineScrollAnchoring: pure anchored-turn geometry plus the three scroll modes (following-end / anchoring-new-turn / free-scrolling) - replace useChatAutoFollow with useChatTimelineScroll, which opts out of automatic movement on real gestures via a generation counter instead of the timer windows the old implementation needed to recognise its own writes - move the load-older button, question/permission cards, recap, status row and bottom spacer into the list header/footer, since the list owns its container - extract useScrollShadow so the shadows can attach to that container maintainScrollAtEnd and maintainVisibleContentPosition replace the manual prepend anchor-hold and the mobile quiet-window prepend deferral. Validated: workspace type-check, lint, web build, ui tests per file. Scroll behaviour itself is unverified and needs manual testing on web, desktop and iOS.
This commit is contained in:
@@ -19,7 +19,7 @@ import { SaveProjectPlanDialog } from '@/components/session/SaveProjectPlanDialo
|
||||
import { ForkSessionDialog, type ForkSessionExecution } from '@/components/session/ForkSessionDialog';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { ArrowsMerge } from '@/components/icons/ArrowsMerge';
|
||||
import type { ContentChangeReason } from '@/hooks/useChatAutoFollow';
|
||||
import type { ContentChangeReason } from '@/hooks/useChatTimelineScroll';
|
||||
|
||||
import { MarkdownImageGallery, SimpleMarkdownRenderer } from '../MarkdownRenderer';
|
||||
import { useSessionUIStore } from '@/sync/session-ui-store';
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import type { Part } from '@opencode-ai/sdk/v2';
|
||||
import { MarkdownRenderer } from '../../MarkdownRenderer';
|
||||
import type { StreamPhase, ToolPopupContent } from '../types';
|
||||
import type { ContentChangeReason } from '@/hooks/useChatAutoFollow';
|
||||
import type { ContentChangeReason } from '@/hooks/useChatTimelineScroll';
|
||||
import { useStreamingTextThrottle } from '../../hooks/useStreamingTextThrottle';
|
||||
import { resolveAssistantDisplayText, shouldRenderAssistantText } from './assistantTextVisibility';
|
||||
import { streamPerfCount, streamPerfObserve } from '@/stores/utils/streamDebug';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import type { Part } from '@opencode-ai/sdk/v2';
|
||||
import type { ContentChangeReason } from '@/hooks/useChatAutoFollow';
|
||||
import type { ContentChangeReason } from '@/hooks/useChatTimelineScroll';
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
import { ReasoningTimelineBlock } from './ReasoningPart';
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { cn } from '@/lib/utils';
|
||||
import type { TurnActivityRecord as TurnActivityPart } from '../../lib/turns/types';
|
||||
import type { ToolPart as ToolPartType } from '@opencode-ai/sdk/v2';
|
||||
import type { StreamPhase } from '../types';
|
||||
import type { ContentChangeReason } from '@/hooks/useChatAutoFollow';
|
||||
import type { ContentChangeReason } from '@/hooks/useChatTimelineScroll';
|
||||
import type { ToolPopupContent } from '../types';
|
||||
import ToolPart from './ToolPart';
|
||||
import { MinDurationShineText } from './MinDurationShineText';
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { animate, type AnimationPlaybackControls } from 'motion';
|
||||
import type { Part } from '@opencode-ai/sdk/v2';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { ContentChangeReason } from '@/hooks/useChatAutoFollow';
|
||||
import type { ContentChangeReason } from '@/hooks/useChatTimelineScroll';
|
||||
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
||||
import { Icon } from '@/components/icon/Icon';
|
||||
import { BusyDots } from './BusyDots';
|
||||
|
||||
@@ -20,7 +20,7 @@ import { toast } from '@/components/ui';
|
||||
import { Text } from '@/components/ui/text';
|
||||
import { FileTypeIcon } from '@/components/icons/FileTypeIcon';
|
||||
import { copyTextToClipboard } from '@/lib/clipboard';
|
||||
import type { ContentChangeReason } from '@/hooks/useChatAutoFollow';
|
||||
import type { ContentChangeReason } from '@/hooks/useChatTimelineScroll';
|
||||
import type { ToolPopupContent } from '../types';
|
||||
import { PlainDiffFallback } from './PlainDiffFallback';
|
||||
import { lazyWithChunkRecovery } from '@/lib/chunkLoadRecovery';
|
||||
|
||||
Reference in New Issue
Block a user