Skip to content

Commit ec3ebdb

Browse files
authored
fix(web): preserve scroll position when expanding work details (#29166)
1 parent a852de0 commit ec3ebdb

2 files changed

Lines changed: 40 additions & 14 deletions

File tree

‎apps/web/src/components/chat/MessagesTimeline.browser.test.tsx‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,45 @@ const TASK_ID = ScheduledTaskId.make("task-1");
217217
const AUTOMATION_PROMPT = "Triage the inbox\n\nLabel every new issue and close duplicates.";
218218

219219
describe("MessagesTimeline work rows", () => {
220+
it("keeps the worked-for heading in place when expanded at the bottom", async () => {
221+
const props = baseProps();
222+
const runId = RunId.make("completed-run");
223+
const container = document.createElement("div");
224+
container.style.cssText = "height: 600px; width: 900px; display: flex; flex-direction: column;";
225+
await using screen = await render(
226+
<MessagesTimeline
227+
{...props}
228+
timelineEntries={[
229+
...Array.from({ length: 12 }, (_, index) =>
230+
message(`Earlier context ${index}`, {}, `context-${index}`),
231+
),
232+
message("Progress details\n\n".repeat(50), { role: "assistant", runId }, "progress"),
233+
message("Done", { role: "assistant", runId }, "final"),
234+
]}
235+
/>,
236+
{ container },
237+
);
238+
const heading = screen.getByRole("button", { name: /^Worked for/ });
239+
await expect.element(heading).toBeVisible();
240+
const viewport = props.listRef.current!.getScrollableNode()!;
241+
await vi.waitFor(() =>
242+
expect(viewport.scrollHeight - viewport.clientHeight - viewport.scrollTop).toBeLessThan(2),
243+
);
244+
expect(viewport.scrollTop).toBeGreaterThan(0);
245+
const top = heading.element().getBoundingClientRect().top;
246+
const height = viewport.scrollHeight;
247+
using _timers = fakeTimers();
248+
await heading.click();
249+
await act(() => vi.advanceTimersByTime(1000));
250+
await vi.waitFor(() => {
251+
expect(viewport.scrollHeight).toBeGreaterThan(height + 500);
252+
expect(heading.element().getBoundingClientRect().top).toBeCloseTo(top, 0);
253+
expect(viewport.scrollHeight - viewport.clientHeight - viewport.scrollTop).toBeGreaterThan(
254+
500,
255+
);
256+
});
257+
});
258+
220259
it.each(["delegated", "legacy", "missing"] as const)(
221260
"uses the reported task for %s completion notifications",
222261
async (source) => {

‎apps/web/src/components/chat/MessagesTimeline.tsx‎

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -470,13 +470,6 @@ const ConversationTimeline = memo(function ConversationTimeline({
470470
const normalizedFindQuery = findQuery.trim();
471471
const findActive = findExpanded;
472472

473-
const disclosurePinsEndRef = useRef(false);
474-
const pinDisclosureToEnd = () => {
475-
if (!disclosurePinsEndRef.current) return;
476-
const element = listRef.current?.getScrollableNode();
477-
if (element) element.scrollTop = element.scrollHeight;
478-
};
479-
480473
const scheduleDisclosureSettle = () => {
481474
if (disclosureSettleFrameRef.current !== null) {
482475
cancelAnimationFrame(disclosureSettleFrameRef.current);
@@ -485,10 +478,7 @@ const ConversationTimeline = memo(function ConversationTimeline({
485478
cancelAnimationFrame(disclosureSettleSecondFrameRef.current);
486479
}
487480
disclosureSettleFrameRef.current = requestAnimationFrame(() => {
488-
pinDisclosureToEnd();
489481
disclosureSettleSecondFrameRef.current = requestAnimationFrame(() => {
490-
pinDisclosureToEnd();
491-
disclosurePinsEndRef.current = false;
492482
disclosureAnchorKeyRef.current = null;
493483
setDisclosureToggleSettling(false);
494484
disclosureSettleFrameRef.current = null;
@@ -499,13 +489,11 @@ const ConversationTimeline = memo(function ConversationTimeline({
499489

500490
const suspendEndScrollMaintenanceForDisclosure = (anchorKey: string) => {
501491
disclosureAnchorKeyRef.current = anchorKey;
502-
disclosurePinsEndRef.current = resolveTimelineIsAtEnd(listRef.current?.getState()) === true;
503492
setDisclosureToggleSettling(true);
504493
scheduleDisclosureSettle();
505494
};
506495

507496
const shouldRestoreVisibleContentPosition = (row: MessagesTimelineRow) => {
508-
if (disclosurePinsEndRef.current) return false;
509497
const disclosureAnchorKey = disclosureAnchorKeyRef.current;
510498
return disclosureAnchorKey === null || row.id === disclosureAnchorKey;
511499
};
@@ -955,8 +943,7 @@ const ConversationTimeline = memo(function ConversationTimeline({
955943
};
956944

957945
const handleItemSizeChanged = () => {
958-
if (disclosurePinsEndRef.current) {
959-
queueMicrotask(pinDisclosureToEnd);
946+
if (disclosureAnchorKeyRef.current !== null) {
960947
scheduleDisclosureSettle();
961948
}
962949
scheduleMinimapRefresh();

0 commit comments

Comments
 (0)