diff --git a/webui/src/components/thread/ThreadMessages.tsx b/webui/src/components/thread/ThreadMessages.tsx index 00d41bd42..0e286d47f 100644 --- a/webui/src/components/thread/ThreadMessages.tsx +++ b/webui/src/components/thread/ThreadMessages.tsx @@ -136,6 +136,7 @@ export function ThreadMessages({ return (
{unit.type === "activity" ? ( diff --git a/webui/src/components/thread/ThreadViewport.tsx b/webui/src/components/thread/ThreadViewport.tsx index b540a9218..761025060 100644 --- a/webui/src/components/thread/ThreadViewport.tsx +++ b/webui/src/components/thread/ThreadViewport.tsx @@ -73,7 +73,7 @@ export const INITIAL_HISTORY_WINDOW = 160; export const HISTORY_WINDOW_INCREMENT = 120; interface HistoryScrollAnchor { - element: HTMLElement; + key: string; offsetTop: number; } @@ -316,8 +316,10 @@ export const ThreadViewport = forwardRef { const scroller = scrollRef.current; + const content = messageContentRef.current; const anchor = historyScrollAnchorRef.current; - if (!scroller || !anchor) return false; - if (!anchor.element.isConnected) { + if (!scroller || !content || !anchor) return false; + const element = Array.from( + content.querySelectorAll("[data-thread-display-unit]"), + ).find((candidate) => candidate.dataset.threadDisplayUnit === anchor.key); + if (!element) { historyScrollAnchorRef.current = null; return false; } const nextOffset = - anchor.element.getBoundingClientRect().top + element.getBoundingClientRect().top - scroller.getBoundingClientRect().top; const delta = nextOffset - anchor.offsetTop; if (Math.abs(delta) < 0.5) return true; @@ -344,7 +350,7 @@ export const ThreadViewport = forwardRef { historyScrollAnchorRef.current = null; diff --git a/webui/src/tests/thread-viewport.test.tsx b/webui/src/tests/thread-viewport.test.tsx index 4535b62dd..7c2ff51dd 100644 --- a/webui/src/tests/thread-viewport.test.tsx +++ b/webui/src/tests/thread-viewport.test.tsx @@ -1521,8 +1521,18 @@ describe("ThreadViewport", () => { dispatchUserScroll(scroller); }); + const replacement = anchor.cloneNode(true) as HTMLElement; + anchor.replaceWith(replacement); anchorDocumentTop += 180; scrollHeight += 180; + Object.defineProperty(replacement, "getBoundingClientRect", { + configurable: true, + value: () => DOMRect.fromRect({ + y: anchorDocumentTop - scroller.scrollTop, + width: 800, + height: 40, + }), + }); const content = screen.getByTestId("thread-message-region").firstElementChild; const observer = resizeObserver.observers.find((candidate) => content ? candidate.elements.includes(content) : false, @@ -1533,7 +1543,7 @@ describe("ThreadViewport", () => { }); expect(scroller.scrollTop).toBe(260); - expect(anchor.getBoundingClientRect().top).toBe(120); + expect(replacement.getBoundingClientRect().top).toBe(120); } finally { resizeObserver.restore(); }