diff --git a/apps/web/src/components/chat/MessagesTimeline.browser.test.tsx b/apps/web/src/components/chat/MessagesTimeline.browser.test.tsx index 2fcaa596a5b6c..9b156deaed8a2 100644 --- a/apps/web/src/components/chat/MessagesTimeline.browser.test.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.browser.test.tsx @@ -217,6 +217,45 @@ const TASK_ID = ScheduledTaskId.make("task-1"); const AUTOMATION_PROMPT = "Triage the inbox\n\nLabel every new issue and close duplicates."; describe("MessagesTimeline work rows", () => { + it("keeps the worked-for heading in place when expanded at the bottom", async () => { + const props = baseProps(); + const runId = RunId.make("completed-run"); + const container = document.createElement("div"); + container.style.cssText = "height: 600px; width: 900px; display: flex; flex-direction: column;"; + await using screen = await render( + + message(`Earlier context ${index}`, {}, `context-${index}`), + ), + message("Progress details\n\n".repeat(50), { role: "assistant", runId }, "progress"), + message("Done", { role: "assistant", runId }, "final"), + ]} + />, + { container }, + ); + const heading = screen.getByRole("button", { name: /^Worked for/ }); + await expect.element(heading).toBeVisible(); + const viewport = props.listRef.current!.getScrollableNode()!; + await vi.waitFor(() => + expect(viewport.scrollHeight - viewport.clientHeight - viewport.scrollTop).toBeLessThan(2), + ); + expect(viewport.scrollTop).toBeGreaterThan(0); + const top = heading.element().getBoundingClientRect().top; + const height = viewport.scrollHeight; + using _timers = fakeTimers(); + await heading.click(); + await act(() => vi.advanceTimersByTime(1000)); + await vi.waitFor(() => { + expect(viewport.scrollHeight).toBeGreaterThan(height + 500); + expect(heading.element().getBoundingClientRect().top).toBeCloseTo(top, 0); + expect(viewport.scrollHeight - viewport.clientHeight - viewport.scrollTop).toBeGreaterThan( + 500, + ); + }); + }); + it.each(["delegated", "legacy", "missing"] as const)( "uses the reported task for %s completion notifications", async (source) => { diff --git a/apps/web/src/components/chat/MessagesTimeline.tsx b/apps/web/src/components/chat/MessagesTimeline.tsx index 4c28163766058..cea2daa111207 100644 --- a/apps/web/src/components/chat/MessagesTimeline.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.tsx @@ -470,13 +470,6 @@ const ConversationTimeline = memo(function ConversationTimeline({ const normalizedFindQuery = findQuery.trim(); const findActive = findExpanded; - const disclosurePinsEndRef = useRef(false); - const pinDisclosureToEnd = () => { - if (!disclosurePinsEndRef.current) return; - const element = listRef.current?.getScrollableNode(); - if (element) element.scrollTop = element.scrollHeight; - }; - const scheduleDisclosureSettle = () => { if (disclosureSettleFrameRef.current !== null) { cancelAnimationFrame(disclosureSettleFrameRef.current); @@ -485,10 +478,7 @@ const ConversationTimeline = memo(function ConversationTimeline({ cancelAnimationFrame(disclosureSettleSecondFrameRef.current); } disclosureSettleFrameRef.current = requestAnimationFrame(() => { - pinDisclosureToEnd(); disclosureSettleSecondFrameRef.current = requestAnimationFrame(() => { - pinDisclosureToEnd(); - disclosurePinsEndRef.current = false; disclosureAnchorKeyRef.current = null; setDisclosureToggleSettling(false); disclosureSettleFrameRef.current = null; @@ -499,13 +489,11 @@ const ConversationTimeline = memo(function ConversationTimeline({ const suspendEndScrollMaintenanceForDisclosure = (anchorKey: string) => { disclosureAnchorKeyRef.current = anchorKey; - disclosurePinsEndRef.current = resolveTimelineIsAtEnd(listRef.current?.getState()) === true; setDisclosureToggleSettling(true); scheduleDisclosureSettle(); }; const shouldRestoreVisibleContentPosition = (row: MessagesTimelineRow) => { - if (disclosurePinsEndRef.current) return false; const disclosureAnchorKey = disclosureAnchorKeyRef.current; return disclosureAnchorKey === null || row.id === disclosureAnchorKey; }; @@ -955,8 +943,7 @@ const ConversationTimeline = memo(function ConversationTimeline({ }; const handleItemSizeChanged = () => { - if (disclosurePinsEndRef.current) { - queueMicrotask(pinDisclosureToEnd); + if (disclosureAnchorKeyRef.current !== null) { scheduleDisclosureSettle(); } scheduleMinimapRefresh();