Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3865,12 +3865,18 @@ export default function ChatView({ threadId }: ChatViewProps) {
}
return false;
};
const onToggleWorkGroup = useCallback((groupId: string) => {
setExpandedWorkGroups((existing) => ({
...existing,
[groupId]: !existing[groupId],
}));
}, []);
const onToggleWorkGroup = useCallback(
(groupId: string) => {
cancelPendingStickToBottom();
shouldAutoScrollRef.current = false;
setShowScrollToBottom(true);
setExpandedWorkGroups((existing) => ({
...existing,
[groupId]: !existing[groupId],
}));
},
[cancelPendingStickToBottom],
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Auto-scroll unnecessarily disabled when collapsing work groups

Low Severity

onToggleWorkGroup unconditionally sets shouldAutoScrollRef.current = false and setShowScrollToBottom(true) on every toggle — both expand and collapse. The PR's stated goal is to prevent the "yank to bottom" when expanding, but collapsing doesn't have that problem since content only shrinks. If a user is auto-scrolling during an active session and collapses an already-expanded work group, auto-scroll is killed and the scroll-to-bottom button flashes. While this self-corrects if a subsequent scroll event fires with isNearBottom, there's a brief window where incoming agent messages won't auto-scroll, and if the collapse doesn't trigger a scroll event (e.g., content remains taller than the viewport), auto-scroll stays disabled until the user manually scrolls near the bottom.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 703721b. Configure here.

const onExpandTimelineImage = useCallback((preview: ExpandedImagePreview) => {
setExpandedImage(preview);
}, []);
Expand Down Expand Up @@ -3978,6 +3984,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
<div
ref={setMessagesScrollContainerRef}
className="min-h-0 flex-1 overflow-x-hidden overflow-y-auto overscroll-y-contain px-3 py-3 sm:px-5 sm:py-4"
style={{ overflowAnchor: "none" }}
onScroll={onMessagesScroll}
onClickCapture={onMessagesClickCapture}
onWheel={onMessagesWheel}
Expand Down
42 changes: 42 additions & 0 deletions apps/web/src/components/chat/MessagesTimeline.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,4 +140,46 @@ describe("MessagesTimeline", () => {
expect(markup).toContain("Context compacted");
expect(markup).toContain("Work log");
});

it("opts work log expansion controls out of scroll anchoring", async () => {
const { MessagesTimeline } = await import("./MessagesTimeline");
const markup = renderToStaticMarkup(
<MessagesTimeline
hasMessages
isWorking={false}
activeTurnInProgress={false}
activeTurnStartedAt={null}
scrollContainer={null}
timelineEntries={Array.from({ length: 7 }, (_, index) => ({
id: `entry-${index + 1}`,
kind: "work" as const,
createdAt: "2026-03-17T19:12:28.000Z",
entry: {
id: `work-${index + 1}`,
createdAt: "2026-03-17T19:12:28.000Z",
label: `Tool call ${index + 1}`,
tone: "tool" as const,
},
}))}
completionDividerBeforeEntryId={null}
completionSummary={null}
turnDiffSummaryByAssistantMessageId={new Map()}
nowIso="2026-03-17T19:12:30.000Z"
expandedWorkGroups={{}}
onToggleWorkGroup={() => {}}
onOpenTurnDiff={() => {}}
revertTurnCountByUserMessageId={new Map()}
onRevertUserMessage={() => {}}
isRevertingCheckpoint={false}
onImageExpand={() => {}}
markdownCwd={undefined}
resolvedTheme="light"
timestampFormat="locale"
workspaceRoot={undefined}
/>,
);

expect(markup).toContain("Show 1 more");
expect(markup).toContain("data-scroll-anchor-ignore");
});
});
43 changes: 30 additions & 13 deletions apps/web/src/components/chat/MessagesTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
ZapIcon,
} from "lucide-react";
import { Button } from "../ui/button";
import { Collapsible, CollapsiblePanel, CollapsibleTrigger } from "../ui/collapsible";
import { clamp } from "effect/Number";
import { buildExpandedImagePreview, ExpandedImagePreview } from "./ExpandedImagePreview";
import { ProposedPlanCard } from "./ProposedPlanCard";
Expand Down Expand Up @@ -318,39 +319,55 @@ export const MessagesTimeline = memo(function MessagesTimeline({
const groupedEntries = row.groupedEntries;
const isExpanded = expandedWorkGroups[groupId] ?? false;
const hasOverflow = groupedEntries.length > MAX_VISIBLE_WORK_LOG_ENTRIES;
const visibleEntries =
hasOverflow && !isExpanded
? groupedEntries.slice(-MAX_VISIBLE_WORK_LOG_ENTRIES)
: groupedEntries;
const hiddenCount = groupedEntries.length - visibleEntries.length;
const overflowEntries = hasOverflow
? groupedEntries.slice(0, -MAX_VISIBLE_WORK_LOG_ENTRIES)
: [];
const alwaysVisibleEntries = hasOverflow
? groupedEntries.slice(-MAX_VISIBLE_WORK_LOG_ENTRIES)
: groupedEntries;
const onlyToolEntries = groupedEntries.every((entry) => entry.tone === "tool");
const showHeader = hasOverflow || !onlyToolEntries;
const groupLabel = onlyToolEntries ? "Tool calls" : "Work log";

return (
<div className="rounded-xl border border-border/45 bg-card/25 px-2 py-1.5">
<Collapsible
open={isExpanded}
onOpenChange={() => onToggleWorkGroup(groupId)}
className="rounded-xl border border-border/45 bg-card/25 px-2 py-1.5"
>
{showHeader && (
<div className="mb-1.5 flex items-center justify-between gap-2 px-0.5">
<p className="text-[9px] uppercase tracking-[0.16em] text-muted-foreground/55">
{groupLabel} ({groupedEntries.length})
</p>
{hasOverflow && (
<button
type="button"
<CollapsibleTrigger
className="text-[9px] uppercase tracking-[0.12em] text-muted-foreground/55 transition-colors duration-150 hover:text-foreground/75"
onClick={() => onToggleWorkGroup(groupId)}
data-scroll-anchor-ignore
>
{isExpanded ? "Show less" : `Show ${hiddenCount} more`}
</button>
{isExpanded ? "Show less" : `Show ${overflowEntries.length} more`}
</CollapsibleTrigger>
)}
</div>
)}
<div className="space-y-0.5">
{visibleEntries.map((workEntry) => (
{hasOverflow && (
<CollapsiblePanel keepMounted>
<div className="space-y-0.5">
{overflowEntries.map((workEntry) => (
<SimpleWorkEntryRow
key={`work-row:${workEntry.id}`}
workEntry={workEntry}
/>
))}
</div>
</CollapsiblePanel>
)}
{alwaysVisibleEntries.map((workEntry) => (
<SimpleWorkEntryRow key={`work-row:${workEntry.id}`} workEntry={workEntry} />
))}
</div>
</div>
</Collapsible>
);
})()}

Expand Down
Loading