authorgravatar for 11655707+CodeZeno@users.noreply.github.comCraig <11655707+CodeZeno@users.noreply.github.com> 2026-03-30 10:53:21+10:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-03-29 17:53:21-07:00
logd41781987cea30ad3ff7d344292ecdfbf9325900
tree7aa9bd71d2f604077fffd264e36a131716bf1fe1
parentfc6507066ed348a459cb675ffd67eaf117f02987
signature Signed by PGP key B5690EEEBB952194

fix(web): preserve scroll when collapsing large changed-files trees (#1113)


2 files changed, 8 insertions(+), 1 deletions(-)

apps/web/src/components/chat/ChangedFilesTree.tsx+1
...@@ -50,6 +50,7 @@ export const ChangedFilesTree = memo(function ChangedFilesTree(props: {...@@ -50,6 +50,7 @@ export const ChangedFilesTree = memo(function ChangedFilesTree(props: {
50 <div key={`dir:${node.path}`}>50 <div key={`dir:${node.path}`}>
51 <button51 <button
52 type="button"52 type="button"
53 data-scroll-anchor-ignore
53 className="group flex w-full items-center gap-1.5 rounded-md py-1 pr-2 text-left hover:bg-background/80"54 className="group flex w-full items-center gap-1.5 rounded-md py-1 pr-2 text-left hover:bg-background/80"
54 style={{ paddingLeft: `${leftPadding}px` }}55 style={{ paddingLeft: `${leftPadding}px` }}
55 onClick={() => toggleDirectory(node.path, depth === 0)}56 onClick={() => toggleDirectory(node.path, depth === 0)}
apps/web/src/components/chat/MessagesTimeline.tsx+7-1
...@@ -264,9 +264,14 @@ export const MessagesTimeline = memo(function MessagesTimeline({...@@ -264,9 +264,14 @@ export const MessagesTimeline = memo(function MessagesTimeline({
264 rowVirtualizer.measure();264 rowVirtualizer.measure();
265 }, [rowVirtualizer, timelineWidthPx]);265 }, [rowVirtualizer, timelineWidthPx]);
266 useEffect(() => {266 useEffect(() => {
267 rowVirtualizer.shouldAdjustScrollPositionOnItemSizeChange = (_item, _delta, instance) => {267 rowVirtualizer.shouldAdjustScrollPositionOnItemSizeChange = (item, _delta, instance) => {
268 const viewportHeight = instance.scrollRect?.height ?? 0;268 const viewportHeight = instance.scrollRect?.height ?? 0;
269 const scrollOffset = instance.scrollOffset ?? 0;269 const scrollOffset = instance.scrollOffset ?? 0;
270 const itemIntersectsViewport =
271 item.end > scrollOffset && item.start < scrollOffset + viewportHeight;
272 if (itemIntersectsViewport) {
273 return false;
274 }
270 const remainingDistance = instance.getTotalSize() - (scrollOffset + viewportHeight);275 const remainingDistance = instance.getTotalSize() - (scrollOffset + viewportHeight);
271 return remainingDistance > AUTO_SCROLL_BOTTOM_THRESHOLD_PX;276 return remainingDistance > AUTO_SCROLL_BOTTOM_THRESHOLD_PX;
272 };277 };
...@@ -483,6 +488,7 @@ export const MessagesTimeline = memo(function MessagesTimeline({...@@ -483,6 +488,7 @@ export const MessagesTimeline = memo(function MessagesTimeline({
483 type="button"488 type="button"
484 size="xs"489 size="xs"
485 variant="outline"490 variant="outline"
491 data-scroll-anchor-ignore
486 onClick={() => onToggleAllDirectories(turnSummary.turnId)}492 onClick={() => onToggleAllDirectories(turnSummary.turnId)}
487 >493 >
488 {allDirectoriesExpanded ? "Collapse all" : "Expand all"}494 {allDirectoriesExpanded ? "Collapse all" : "Expand all"}