diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 7c649b5003..43b81c0c36 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -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], + ); const onExpandTimelineImage = useCallback((preview: ExpandedImagePreview) => { setExpandedImage(preview); }, []); @@ -3978,6 +3984,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
{ 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( + ({ + 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"); + }); }); diff --git a/apps/web/src/components/chat/MessagesTimeline.tsx b/apps/web/src/components/chat/MessagesTimeline.tsx index 9be521b3be..dbbe67edb6 100644 --- a/apps/web/src/components/chat/MessagesTimeline.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.tsx @@ -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"; @@ -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 ( -
+ onToggleWorkGroup(groupId)} + className="rounded-xl border border-border/45 bg-card/25 px-2 py-1.5" + > {showHeader && (

{groupLabel} ({groupedEntries.length})

{hasOverflow && ( - + {isExpanded ? "Show less" : `Show ${overflowEntries.length} more`} + )}
)}
- {visibleEntries.map((workEntry) => ( + {hasOverflow && ( + +
+ {overflowEntries.map((workEntry) => ( + + ))} +
+
+ )} + {alwaysVisibleEntries.map((workEntry) => ( ))}
-
+ ); })()}