From 2a199fdddeb545a9442592841f95d2d2024ea428 Mon Sep 17 00:00:00 2001 From: Shakker Date: Sun, 2 Aug 2026 20:56:06 +0100 Subject: [PATCH] fix: cancel chat scrolls on reconnect --- ui/src/pages/chat/chat-pane-context.ts | 2 ++ ui/src/pages/chat/chat-pane-lifecycle.test.ts | 21 +++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/ui/src/pages/chat/chat-pane-context.ts b/ui/src/pages/chat/chat-pane-context.ts index af609ebb45fe..479c8d3c1c6c 100644 --- a/ui/src/pages/chat/chat-pane-context.ts +++ b/ui/src/pages/chat/chat-pane-context.ts @@ -37,6 +37,7 @@ import { reconcileStaleChatRunAfterSessionStatePublication, replayPendingChatAbort, } from "./run-lifecycle.ts"; +import { cancelChatScroll } from "./scroll.ts"; import { clearChatMessagesFromCache } from "./session-message-cache.ts"; import { normalizeSidebarLayout } from "./sidebar-layout.ts"; import { reconcileWaitingApprovalsFromSnapshot } from "./tool-stream.ts"; @@ -184,6 +185,7 @@ export abstract class ChatPaneContext extends ChatPaneLifecycle { } if (sourceChanged) { this.cancelHeaderRename(); + cancelChatScroll(state); releaseChatMediaResourceSubscriber(state.requestUpdate); // A reconnect can retain the browser client. Keep async ownership tied // to the logical connection, not only the transport object identity. diff --git a/ui/src/pages/chat/chat-pane-lifecycle.test.ts b/ui/src/pages/chat/chat-pane-lifecycle.test.ts index eacca92a0882..88c6450f4634 100644 --- a/ui/src/pages/chat/chat-pane-lifecycle.test.ts +++ b/ui/src/pages/chat/chat-pane-lifecycle.test.ts @@ -899,6 +899,27 @@ describe("chat pane connection lifecycle", () => { expect(state.chatLoading).toBe(true); }); + it("cancels scroll work owned by the prior Gateway connection", () => { + const client = { request: vi.fn() } as unknown as GatewayBrowserClient; + const { pane, state } = createTestChatPane({ client, sessions: {} as SessionCapability }); + const cancelCommit = vi.fn(); + const initialScrollGeneration = state.chatScrollGeneration; + state.chatScrollCommitCleanup = cancelCommit; + state.chatIsProgrammaticScroll = true; + + pane.applyGatewaySnapshot({ + ...pane.context.gateway.snapshot, + client, + phase: "reconnecting", + hello: null, + }); + + expect(cancelCommit).toHaveBeenCalledOnce(); + expect(state.chatScrollCommitCleanup).toBeNull(); + expect(state.chatScrollGeneration).toBe(initialScrollGeneration + 1); + expect(state.chatIsProgrammaticScroll).toBe(false); + }); + it("refreshes the transcript before secondary hydration after a same-client reconnect", () => { const request = vi.fn(() => new Promise(() => {})); const client = {