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) => (
))}
-
+
);
})()}