Migrate chat scrolling and branch lists to LegendList#1953
Migrate chat scrolling and branch lists to LegendList#1953juliusmarminge wants to merge 8 commits intomainfrom
Conversation
- Replace custom chat auto-scroll and branch virtualization with LegendList - Remove deprecated chat scroll helpers and related tests - Add LegendList dependency for list rendering
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
- Reset the branch list correctly for virtualized and non-virtualized views - Preserve formatting cleanup in web package and timeline logic
ApprovabilityVerdict: Needs human review This PR replaces the entire chat scrolling and virtualization infrastructure, migrating from @tanstack/react-virtual to a beta version of @legendapp/list. The changes affect core runtime behavior including auto-scroll detection, scroll anchoring, and message height virtualization. While it simplifies the codebase by removing ~1500 lines of custom scroll handling, the scope and impact on the core chat experience warrant human review. You can customize Macroscope's approvability policy. Learn more. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Autofix Details
Bugbot Autofix prepared a fix for 1 of the 2 issues found in the latest run.
- ✅ Fixed: Unused
scrollToEnddependency in callback, refs accessed directly- Replaced direct
legendListRef.current?.scrollToEnd?.({ animated: true })calls in bothonSendandonSubmitPlanFollowUpwithscrollToEnd(true)to use the wrapper consistently.
- Replaced direct
Or push these changes by commenting:
@cursor push b3c9d5931b
Preview (b3c9d5931b)
diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx
--- a/apps/web/src/components/ChatView.tsx
+++ b/apps/web/src/components/ChatView.tsx
@@ -2435,7 +2435,7 @@
// Sending a message should always bring the latest user turn into view.
isAtEndRef.current = true;
requestAnimationFrame(() => {
- legendListRef.current?.scrollToEnd?.({ animated: true });
+ scrollToEnd(true);
setShowScrollToBottom(false);
});
@@ -2830,7 +2830,7 @@
]);
isAtEndRef.current = true;
requestAnimationFrame(() => {
- legendListRef.current?.scrollToEnd?.({ animated: true });
+ scrollToEnd(true);
setShowScrollToBottom(false);
});You can send follow-ups to the cloud agent here.
- Drop browser harness measurements for timeline height parity - Delete the unused timelineHeight helper and its unit tests
- Stop forcing LegendList to scroll to the end on thread mount - Rely on existing scroll state handling instead
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 3 total unresolved issues (including 2 from previous reviews).
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Uncancelled
requestAnimationFramein send message handlers- Added a
pendingSendScrollFrameRefto track RAF IDs from both send handlers, cancel any pending frame before scheduling a new one, and cancel on component unmount via a cleanup effect.
- Added a
Or push these changes by commenting:
@cursor push 546899ceba
Preview (546899ceba)
diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx
--- a/apps/web/src/components/ChatView.tsx
+++ b/apps/web/src/components/ChatView.tsx
@@ -700,6 +700,7 @@
const attachmentPreviewHandoffByMessageIdRef = useRef<Record<string, string[]>>({});
const attachmentPreviewPromotionInFlightByMessageIdRef = useRef<Record<string, true>>({});
const sendInFlightRef = useRef(false);
+ const pendingSendScrollFrameRef = useRef<number | null>(null);
const terminalOpenByThreadRef = useRef<Record<string, boolean>>({});
const terminalState = useTerminalStateStore((state) =>
@@ -1176,6 +1177,13 @@
}
};
}, [clearAttachmentPreviewHandoffs]);
+ useEffect(() => {
+ return () => {
+ if (pendingSendScrollFrameRef.current != null) {
+ cancelAnimationFrame(pendingSendScrollFrameRef.current);
+ }
+ };
+ }, []);
const handoffAttachmentPreviews = useCallback((messageId: MessageId, previewUrls: string[]) => {
if (previewUrls.length === 0) return;
@@ -2423,7 +2431,11 @@
]);
// Sending a message should always bring the latest user turn into view.
isAtEndRef.current = true;
- requestAnimationFrame(() => {
+ if (pendingSendScrollFrameRef.current != null) {
+ cancelAnimationFrame(pendingSendScrollFrameRef.current);
+ }
+ pendingSendScrollFrameRef.current = requestAnimationFrame(() => {
+ pendingSendScrollFrameRef.current = null;
legendListRef.current?.scrollToEnd?.({ animated: true });
setShowScrollToBottom(false);
});
@@ -2818,7 +2830,11 @@
},
]);
isAtEndRef.current = true;
- requestAnimationFrame(() => {
+ if (pendingSendScrollFrameRef.current != null) {
+ cancelAnimationFrame(pendingSendScrollFrameRef.current);
+ }
+ pendingSendScrollFrameRef.current = requestAnimationFrame(() => {
+ pendingSendScrollFrameRef.current = null;
legendListRef.current?.scrollToEnd?.({ animated: true });
setShowScrollToBottom(false);
});You can send follow-ups to the cloud agent here.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Infinite scroll listener inactive for virtualized branch list
- Guarded the branches.length useEffect to skip when shouldVirtualizeBranchList is true, since branchListScrollElementRef is null in that path and the LegendList onEndReached callback already handles infinite loading for the virtualized case.
Or push these changes by commenting:
@cursor push 6f4824801a
Preview (6f4824801a)
diff --git a/apps/web/src/components/BranchToolbarBranchSelector.tsx b/apps/web/src/components/BranchToolbarBranchSelector.tsx
--- a/apps/web/src/components/BranchToolbarBranchSelector.tsx
+++ b/apps/web/src/components/BranchToolbarBranchSelector.tsx
@@ -460,8 +460,9 @@
}, [isBranchMenuOpen, maybeFetchNextBranchPage]);
useEffect(() => {
+ if (shouldVirtualizeBranchList) return;
maybeFetchNextBranchPage();
- }, [branches.length, maybeFetchNextBranchPage]);
+ }, [branches.length, maybeFetchNextBranchPage, shouldVirtualizeBranchList]);
const triggerLabel = getBranchTriggerLabel({
activeWorktreePath,You can send follow-ups to the cloud agent here.
When shouldVirtualizeBranchList is true, branchListScrollElementRef stays null because setBranchListRef is only attached to the non-virtualized ComboboxList. The branches.length effect would call maybeFetchNextBranchPage which bails out immediately due to the null scroll element. Guard the effect to skip when virtualized, since the LegendList onEndReached callback already handles infinite loading in that path, and with 40+ items the visible area is always filled. Applied via @cursor push command
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Scroll-to-bottom button not reset on thread switch
- Added
isAtEndRef.current = trueandsetShowScrollToBottom(false)to the existinguseEffectkeyed onactiveThread?.idso scroll state is properly reset when switching threads.
- Added
Or push these changes by commenting:
@cursor push 07fd60cc17
Preview (07fd60cc17)
diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx
--- a/apps/web/src/components/ChatView.tsx
+++ b/apps/web/src/components/ChatView.tsx
@@ -1968,6 +1968,8 @@
useEffect(() => {
setExpandedWorkGroups({});
setPullRequestDialogState(null);
+ isAtEndRef.current = true;
+ setShowScrollToBottom(false);
if (planSidebarOpenOnNextThreadRef.current) {
planSidebarOpenOnNextThreadRef.current = false;
setPlanSidebarOpen(true);You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit 1620842. Configure here.
Reset isAtEndRef and showScrollToBottom when activeThread changes so stale scroll state from a previous thread does not persist into the new one. Applied via @cursor push command
- Scroll LegendList to the end without animation when sending messages - Avoid mid-flight layout changes from landing the view at the wrong position


Summary
@legendapp/list.ChatViewscroll state by delegating stick-to-bottom behavior toLegendList.tanstack/react-virtualchat scroll helpers and related tests.Testing
bun fmt,bun lint,bun typecheck, andbun run test.Note
Medium Risk
Medium risk because it replaces core scrolling/virtualization behavior in the chat timeline and branch selector, which can introduce UX regressions (scroll position, pagination, stick-to-bottom) despite limited surface area outside UI.
Overview
Migrates list virtualization from
@tanstack/react-virtualto@legendapp/listfor both the chat timeline (MessagesTimeline/ChatView) and the branch selector dropdown.Chat scrolling is simplified by delegating stick-to-bottom, initial alignment, and scroll-to-end behavior to LegendList APIs (with a new
onIsAtEndChangesignal driving the “scroll to bottom” pill), removing custom scroll handlers/threshold logic and deleting the oldchat-scrollutilities.Branch selector virtualization now renders via
LegendListinside a newComboboxListVirtualizedwrapper, using LegendList ref methods for index-into-view andonEndReachedfor pagination, and drops the prior virtualizer plumbing.Removes now-obsolete row height estimation code and multiple virtualization/estimator-focused tests, and updates dependencies to add
@legendapp/listand remove@tanstack/react-virtual.Reviewed by Cursor Bugbot for commit 85616f1. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Replace
@tanstack/react-virtualwith LegendList for chat and branch list scrolling@tanstack/react-virtualvirtualizer inMessagesTimeline.tsxandChatView.tsxwithLegendList, which handles end-aligned rendering, maintain-at-end scrolling, and item size estimation natively.BranchToolbarBranchSelector.tsxnow virtualizes its dropdown list viaLegendList, usingonEndReachedfor infinite pagination instead of a scroll event listener.MessagesTimeline.logic.ts; theMessagesTimelinecomponent now reports scroll end state via anonIsAtEndChangecallback instead of emitting virtualizer snapshots.MessagesTimelineand chat timeline height parity are removed as they no longer apply.estimatedItemSize=90is a fixed estimate; items significantly taller than this may cause layout jitter until measured.Macroscope summarized 85616f1.