From 00c1af5a5adc2669e2946a16624fc4db7f3c4590 Mon Sep 17 00:00:00 2001 From: User Date: Wed, 15 Jul 2026 12:04:05 +0200 Subject: [PATCH] fix(frontend): reset live log follow state on resume --- frontend/src/shell/CentralStatus.test.tsx | 38 +++++++++++++++++++++++ frontend/src/shell/CentralStatus.tsx | 6 +++- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/frontend/src/shell/CentralStatus.test.tsx b/frontend/src/shell/CentralStatus.test.tsx index 0f84d51a..a973262e 100644 --- a/frontend/src/shell/CentralStatus.test.tsx +++ b/frontend/src/shell/CentralStatus.test.tsx @@ -83,3 +83,41 @@ test("preserves manual live-log scroll position away from the bottom", () => { expect(log.scrollTop).toBe(100); }); + +test("resets bottom-follow when the live log resumes after becoming inactive", () => { + useSessionStore.setState({ transcript: [{ role: "assistant", text: "First" }] }); + const { rerender } = render(); + const log = screen.getByRole("log", { name: "Live model activity" }); + setScrollGeometry(log, { scrollHeight: 400, clientHeight: 100, scrollTop: 100 }); + fireEvent.scroll(log); + + rerender(); + expect(screen.queryByRole("log", { name: "Live model activity" })).not.toBeInTheDocument(); + + const scrollProperties = ["scrollHeight", "clientHeight", "scrollTop"] as const; + const originalDescriptors = scrollProperties.map((property) => + Object.getOwnPropertyDescriptor(HTMLElement.prototype, property), + ); + Object.defineProperties(HTMLElement.prototype, { + scrollHeight: { configurable: true, value: 400 }, + clientHeight: { configurable: true, value: 100 }, + scrollTop: { configurable: true, writable: true, value: 0 }, + }); + + try { + rerender(); + const resumedLog = screen.getByRole("log", { name: "Live model activity" }); + expect(resumedLog.scrollTop).toBe(400); + + Object.defineProperty(resumedLog, "scrollHeight", { configurable: true, value: 500 }); + act(() => useSessionStore.getState().applyEvent({ type: "text_delta", text: " second" })); + + expect(resumedLog.scrollTop).toBe(500); + } finally { + scrollProperties.forEach((property, index) => { + const descriptor = originalDescriptors[index]; + if (descriptor) Object.defineProperty(HTMLElement.prototype, property, descriptor); + else Reflect.deleteProperty(HTMLElement.prototype, property); + }); + } +}); diff --git a/frontend/src/shell/CentralStatus.tsx b/frontend/src/shell/CentralStatus.tsx index 4fc43c3b..0396dbce 100644 --- a/frontend/src/shell/CentralStatus.tsx +++ b/frontend/src/shell/CentralStatus.tsx @@ -21,9 +21,13 @@ export function CentralStatus({ working }: { working: boolean }) { const followRef = useRef(true); useLayoutEffect(() => { + if (!working) { + followRef.current = true; + return; + } const viewport = scrollRef.current; if (viewport && followRef.current) viewport.scrollTop = viewport.scrollHeight; - }, [rowCount, tail]); + }, [working, rowCount, tail]); if (!working || rows.length === 0) return null;