diff --git a/bun.lock b/bun.lock index 98e47d8f..d9713441 100644 --- a/bun.lock +++ b/bun.lock @@ -167,6 +167,7 @@ "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", + "@legendapp/list": "3.2.0", "@lezer/highlight": "^1.2.3", "@opencode-ai/sdk": "1.18.21", "@pierre/diffs": "1.3.0-beta.6", @@ -917,6 +918,8 @@ "@kwsites/promise-deferred": ["@kwsites/promise-deferred@1.1.1", "", {}, "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw=="], + "@legendapp/list": ["@legendapp/list@3.2.0", "", { "dependencies": { "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "react": "*", "react-dom": "*", "react-native": "*" }, "optionalPeers": ["react-dom", "react-native"] }, "sha512-bN+g/oQYjFz+UAyuBN4cmYJAwdJS1TdNcZZOVlh3+VwCQUWrsg0PH46Mvm76gdZSCYMfoFanPY4dKnILcYEzeg=="], + "@levischuck/tiny-cbor": ["@levischuck/tiny-cbor@0.2.11", "", {}, "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow=="], "@lezer/common": ["@lezer/common@1.5.1", "", {}, "sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw=="], diff --git a/packages/ui/package.json b/packages/ui/package.json index 14eaf104..dc5e40f1 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -43,6 +43,7 @@ "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", + "@legendapp/list": "3.2.0", "@lezer/highlight": "^1.2.3", "@opencode-ai/sdk": "1.18.21", "@pierre/diffs": "1.3.0-beta.6", diff --git a/packages/ui/src/components/chat/ChatContainer.tsx b/packages/ui/src/components/chat/ChatContainer.tsx index a6384531..9174668b 100644 --- a/packages/ui/src/components/chat/ChatContainer.tsx +++ b/packages/ui/src/components/chat/ChatContainer.tsx @@ -18,8 +18,8 @@ import { StatusRowContainer } from './StatusRowContainer'; import { SessionRecapNote } from '@/components/chat/SessionRecapSpacer'; import ScrollToBottomButton from './components/ScrollToBottomButton'; import { PromptNavigatorRail } from './components/PromptNavigatorRail'; -import { ScrollShadow } from '@/components/ui/ScrollShadow'; -import { useChatAutoFollow, type AnimationHandlers, type ContentChangeReason } from '@/hooks/useChatAutoFollow'; +import { useScrollShadow } from '@/components/ui/useScrollShadow'; +import { useChatTimelineScroll, type AnimationHandlers, type ContentChangeReason, type TimelineListHandle } from '@/hooks/useChatTimelineScroll'; import { useChatTimelineController } from './hooks/useChatTimelineController'; import { TimelineDialog } from './TimelineDialog'; import { useChatTurnNavigation } from './hooks/useChatTurnNavigation'; @@ -151,10 +151,16 @@ type ChatViewportProps = { currentSessionKey: string; isDesktopExpandedInput: boolean; isMobile: boolean; - stickyUserHeader: boolean; directory?: string; scrollRef: React.RefObject; messageListRef: React.RefObject; + registerList: (list: TimelineListHandle | null) => void; + anchorMessageId: string | null; + onAnchorReady: (messageId: string, anchorIndex: number) => void; + onAnchorSizeChanged: (messageId: string) => void; + composerOverlayHeight: number; + onIsAtEndChange: (isAtEnd: boolean) => void; + onTimelineDataChange: () => void; pendingRevealWork: boolean; renderedMessages: SessionMessageRecord[]; isLoadingOlder: boolean; @@ -169,7 +175,6 @@ type ChatViewportProps = { } | null; handleMessageContentChange: (reason?: ContentChangeReason) => void; getAnimationHandlers: (messageId: string) => AnimationHandlers; - handleHistoryScroll: () => void; scrollToBottom: () => void; sessionQuestions: QuestionRequest[]; sessionPermissions: PermissionRequest[]; @@ -190,10 +195,16 @@ const ChatViewport = React.memo(({ currentSessionKey, isDesktopExpandedInput, isMobile, - stickyUserHeader, directory, scrollRef, messageListRef, + registerList, + anchorMessageId, + onAnchorReady, + onAnchorSizeChanged, + composerOverlayHeight, + onIsAtEndChange, + onTimelineDataChange, pendingRevealWork, renderedMessages, isLoadingOlder, @@ -203,7 +214,6 @@ const ChatViewport = React.memo(({ retryOverlay, handleMessageContentChange, getAnimationHandlers, - handleHistoryScroll, scrollToBottom, sessionQuestions, sessionPermissions, @@ -315,6 +325,60 @@ const ChatViewport = React.memo(({ scrollRef.current?.focus({ preventScroll: true }); }, [scrollRef]); + // Everything that used to sit beside the list inside the scroll container + // now renders as the list's header/footer, so it keeps scrolling with the + // rows exactly as before. + const listHeader = React.useMemo(() => ( + showLoadOlderButton ? ( +
+ +
+ ) : null + ), [isLoadingOlder, onLoadOlder, showLoadOlderButton, t]); + + const listFooter = React.useMemo(() => ( + <> + {(sessionQuestions.length > 0 || sessionPermissions.length > 0) && ( +
+ {sessionQuestions.map((question) => ( + + ))} + {sessionPermissions.map((permission) => ( + + ))} +
+ )} + + + +
+ +
+ +