From 4b964e171c17c3294c48ac8794f29d7f75c9c8f4 Mon Sep 17 00:00:00 2001 From: Alex Southwell <4596216+saphid@users.noreply.github.com> Date: Tue, 3 Nov 2026 09:12:41 +1100 Subject: [PATCH 1/2] fix(preview): a dropped stream withdraws its stale file picker Co-Authored-By: Claude Opus 5.5 --- .../src/preview/serverBrowserStream.test.ts | 17 +++++++++++++++++ .../src/preview/serverBrowserStream.ts | 5 +++++ 2 files changed, 22 insertions(+) diff --git a/packages/client-runtime/src/preview/serverBrowserStream.test.ts b/packages/client-runtime/src/preview/serverBrowserStream.test.ts index cfde00363a005..29a94927922c6 100644 --- a/packages/client-runtime/src/preview/serverBrowserStream.test.ts +++ b/packages/client-runtime/src/preview/serverBrowserStream.test.ts @@ -107,6 +107,23 @@ describe("preview stream control", () => { expect(client.send({ type: "dialog", accept: false })).toBe(false); client.stop(); }); + + it("withdraws an offered file picker when the stream drops", () => { + const onFileChooser = vi.fn(); + const client = createPreviewStreamClient(target, { + onFrame: vi.fn(), + onFileChooser, + onViewport: vi.fn(), + onConnectedChange: vi.fn(), + onUnauthorized: vi.fn(), + }); + const socket = FakeSocket.current; + socket.dispatchEvent(new Event("open")); + socket.message(JSON.stringify({ type: "fileChooser", id: "c1", multiple: false, accept: "" })); + socket.dispatchEvent(Object.assign(new Event("close"), { code: 1012 })); + expect(onFileChooser).toHaveBeenLastCalledWith(null); + client.stop(); + }); }); describe("preview stream downloads", () => { diff --git a/packages/client-runtime/src/preview/serverBrowserStream.ts b/packages/client-runtime/src/preview/serverBrowserStream.ts index b71fff335f881..cb325edd5d4a5 100644 --- a/packages/client-runtime/src/preview/serverBrowserStream.ts +++ b/packages/client-runtime/src/preview/serverBrowserStream.ts @@ -346,6 +346,11 @@ export function createPreviewStreamClient( if (socket !== ws) return; socket = null; control = null; + // The server re-offers a picker still open, under a new id, when this viewer reattaches. + if (fileChooser !== null) { + fileChooser = null; + events.onFileChooser?.(null); + } if (opened) events.onConnectedChange(false); if (stopped) return; if (event.code === 4404) { From 2a930a7df646e269e249b465e374e9a5cacd466b Mon Sep 17 00:00:00 2001 From: Alex Southwell <4596216+saphid@users.noreply.github.com> Date: Tue, 3 Nov 2026 10:15:46 +1100 Subject: [PATCH 2/2] fix(client-runtime): stopping the browser stream also clears the file picker Co-Authored-By: Claude Opus 5.5 --- .../src/preview/serverBrowserStream.test.ts | 14 +++++++++++--- .../src/preview/serverBrowserStream.ts | 14 +++++++++----- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/packages/client-runtime/src/preview/serverBrowserStream.test.ts b/packages/client-runtime/src/preview/serverBrowserStream.test.ts index 29a94927922c6..7b749acea4402 100644 --- a/packages/client-runtime/src/preview/serverBrowserStream.test.ts +++ b/packages/client-runtime/src/preview/serverBrowserStream.test.ts @@ -108,21 +108,29 @@ describe("preview stream control", () => { client.stop(); }); - it("withdraws an offered file picker when the stream drops", () => { + it("withdraws an offered file picker when the stream drops or stops", () => { const onFileChooser = vi.fn(); - const client = createPreviewStreamClient(target, { + const events = { onFrame: vi.fn(), onFileChooser, onViewport: vi.fn(), onConnectedChange: vi.fn(), onUnauthorized: vi.fn(), - }); + }; + const client = createPreviewStreamClient(target, events); const socket = FakeSocket.current; socket.dispatchEvent(new Event("open")); socket.message(JSON.stringify({ type: "fileChooser", id: "c1", multiple: false, accept: "" })); socket.dispatchEvent(Object.assign(new Event("close"), { code: 1012 })); expect(onFileChooser).toHaveBeenLastCalledWith(null); client.stop(); + + const restarted = createPreviewStreamClient(target, events); + FakeSocket.current.message( + JSON.stringify({ type: "fileChooser", id: "c2", multiple: false, accept: "" }), + ); + restarted.stop(); + expect(onFileChooser).toHaveBeenLastCalledWith(null); }); }); diff --git a/packages/client-runtime/src/preview/serverBrowserStream.ts b/packages/client-runtime/src/preview/serverBrowserStream.ts index cb325edd5d4a5..617c1d18424f3 100644 --- a/packages/client-runtime/src/preview/serverBrowserStream.ts +++ b/packages/client-runtime/src/preview/serverBrowserStream.ts @@ -224,6 +224,13 @@ export function createPreviewStreamClient( let control: PreviewStreamControl | null = null; let fileChooser: string | null = null; + // The server re-offers a picker still open, under a new id, when this viewer reattaches. + const withdrawFileChooser = () => { + if (fileChooser === null) return; + fileChooser = null; + events.onFileChooser?.(null); + }; + const connect = () => { if (stopped) return; const ws = new WebSocket(url); @@ -346,11 +353,7 @@ export function createPreviewStreamClient( if (socket !== ws) return; socket = null; control = null; - // The server re-offers a picker still open, under a new id, when this viewer reattaches. - if (fileChooser !== null) { - fileChooser = null; - events.onFileChooser?.(null); - } + withdrawFileChooser(); if (opened) events.onConnectedChange(false); if (stopped) return; if (event.code === 4404) { @@ -390,6 +393,7 @@ export function createPreviewStreamClient( const ws = socket; socket = null; control = null; + withdrawFileChooser(); ws?.close(); }, };