From 5b733ab5753d690fe5c80ce1c24d602956287d6f Mon Sep 17 00:00:00 2001 From: NekoPunch Date: Sat, 5 Sep 2026 00:22:18 -0700 Subject: [PATCH] fix(ui): keep write geometry through a late scroll echo The echo of the authority's own write refreshed the remembered scroll geometry. When that echo landed after content had grown but before the resize observer wrote the tail, the next offset move had no growth left to explain it and read as the reader scrolling away, so the pin was released and the tail stopped following. Generated-by: Claude Code Claude-Session: https://claude.ai/code/session_01RLZUUq5ri1bHzydQ7tw32b --- .../transcript-scroll-authority.test.ts | 19 +++++++++++++++++++ .../ui/src/transcript-scroll-authority.tsx | 4 +--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/__tests__/transcript-scroll-authority.test.ts b/packages/ui/src/__tests__/transcript-scroll-authority.test.ts index 4debc43eaf..1c9bf96946 100644 --- a/packages/ui/src/__tests__/transcript-scroll-authority.test.ts +++ b/packages/ui/src/__tests__/transcript-scroll-authority.test.ts @@ -231,6 +231,25 @@ test('a scroll event that arrives late is still this authority\'s own write', () }); }); +test('growth a late echo already sees still explains the offset move after it', () => { + withObservers((resize) => { + const root = fakeRoot({ scrollHeight: 3_523, clientHeight: 860 }); + const authority = createTranscriptScrollAuthority(); + authority.attach(root as unknown as HTMLElement); + + // Content grew before the write's echo landed, then the offset moved by + // less than that growth: content, not the reader. + root.grow(600); + root.emitScroll(); + root.scrollTop += 91; + root.emitScroll(); + assert.equal(authority.getSnapshot().pinned, true); + + resize(); + assert.equal(root.scrollTop, 3_263); + }); +}); + test('growth that outruns the write does not read as the reader scrolling up', () => { withObservers((resize) => { const root = fakeRoot(); diff --git a/packages/ui/src/transcript-scroll-authority.tsx b/packages/ui/src/transcript-scroll-authority.tsx index f4de10b70c..a6f8d57264 100644 --- a/packages/ui/src/transcript-scroll-authority.tsx +++ b/packages/ui/src/transcript-scroll-authority.tsx @@ -170,10 +170,8 @@ export function createTranscriptScrollAuthority(): TranscriptScrollAuthority { // scrollers (a tool output box, a terminal) never reach here at all: // `scroll` does not bubble, and there is no `wheel` listener to catch // instead. + // Growth this echo already sees has not reached the observer yet. if (lastWrittenTop !== undefined && Math.abs(target.scrollTop - lastWrittenTop) < 1) { - lastScrollHeight = target.scrollHeight; - lastClientHeight = target.clientHeight; - lastScrollTop = target.scrollTop; return; } // Content moves the offset too, and only ever by how much the end of