authorgravatar for saphid@gmail.comAlex Southwell <saphid@gmail.com> 2026-10-12 11:46:48+11:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-12 00:46:48+00:00
logaa2bfc604c538dbaa7e2a7f8698104503497d47a
treed6284fae3a2915ddc457ee2438bd2ad2047500c7
parent7f02b849584332368a5489631d3286409eed90a8
signature Signed by PGP key B5690EEEBB952194

fix(mobile): preserve browser prompt replies across control updates (#18419)

Co-authored-by: SAPHID <saphid@users.noreply.github.com> Co-authored-by: GPT-6.1 Sol <noreply@openai.com>

3 files changed, 65 insertions(+), 2 deletions(-)

apps/mobile/src/features/browser/PreviewStreamWebView.tsx+9-2
...@@ -26,6 +26,7 @@ import { AppText } from "../../components/AppText";...@@ -26,6 +26,7 @@ import { AppText } from "../../components/AppText";
26import { downloadAndShareAttachment } from "../../lib/attachmentDownload";26import { downloadAndShareAttachment } from "../../lib/attachmentDownload";
27import { beginForegroundHandoff } from "../../lib/foreground-handoff";27import { beginForegroundHandoff } from "../../lib/foreground-handoff";
28import { usePreviewStreamAccess } from "../../state/preview";28import { usePreviewStreamAccess } from "../../state/preview";
29import { previewPromptText } from "./previewPromptText";
2930
30import {31import {
31 previewStreamDocument,32 previewStreamDocument,
...@@ -256,6 +257,7 @@ function PreviewStreamDocumentView({...@@ -256,6 +257,7 @@ function PreviewStreamDocumentView({
256 const [hostSetup, setHostSetup] = useState<PreviewStreamHostSetup | null>(null);257 const [hostSetup, setHostSetup] = useState<PreviewStreamHostSetup | null>(null);
257 const [started, setStarted] = useState(false);258 const [started, setStarted] = useState(false);
258 const [control, setControl] = useState<PreviewStreamControl | null>(null);259 const [control, setControl] = useState<PreviewStreamControl | null>(null);
260 const dialogRef = useRef<PreviewStreamControl["dialog"]>(null);
259 const [promptText, setPromptText] = useState("");261 const [promptText, setPromptText] = useState("");
260 const [fileChooser, setFileChooser] = useState<PreviewStreamFileChooser | null>(null);262 const [fileChooser, setFileChooser] = useState<PreviewStreamFileChooser | null>(null);
261 const answerFileChooser = (pick: boolean) => {263 const answerFileChooser = (pick: boolean) => {
...@@ -371,11 +373,14 @@ function PreviewStreamDocumentView({...@@ -371,11 +373,14 @@ function PreviewStreamDocumentView({
371 const message = previewStreamMessage(event.nativeEvent.data);373 const message = previewStreamMessage(event.nativeEvent.data);
372 if (message === null) return;374 if (message === null) return;
373 switch (message.type) {375 switch (message.type) {
374 case "control":376 case "control": {
375 setControl(message);377 setControl(message);
376 setPromptText(message.dialog?.defaultValue ?? "");378 const previousDialog = dialogRef.current;
379 dialogRef.current = message.dialog;
380 setPromptText((draft) => previewPromptText(previousDialog, message.dialog, draft));
377 onControl?.(message);381 onControl?.(message);
378 return;382 return;
383 }
379 case "unauthorized":384 case "unauthorized":
380 if (!onUnauthorized()) {385 if (!onUnauthorized()) {
381 fail("This session can't open the browser stream. Reconnect to try again.");386 fail("This session can't open the browser stream. Reconnect to try again.");
...@@ -413,6 +418,8 @@ function PreviewStreamDocumentView({...@@ -413,6 +418,8 @@ function PreviewStreamDocumentView({
413 }418 }
414 setStatus(message.status);419 setStatus(message.status);
415 if (message.status === "connecting") {420 if (message.status === "connecting") {
421 // Dialogs may have closed and reopened while this viewer was disconnected.
422 dialogRef.current = null;
416 setControl(null);423 setControl(null);
417 onControl?.(null);424 onControl?.(null);
418 }425 }
apps/mobile/src/features/browser/previewPromptText.test.ts created+41
...@@ -0,0 +1,41 @@
1import { describe, expect, it } from "vite-plus/test";
2
3import { previewPromptText } from "./previewPromptText";
4
5describe("browser prompt response", () => {
6 const prompt = { type: "prompt" as const, message: "Your name?", defaultValue: "Guest" };
7
8 it("preserves a typed response across repeated control broadcasts", () => {
9 expect(previewPromptText(prompt, { ...prompt }, "Alice")).toBe("Alice");
10 });
11
12 it("preserves an intentionally empty response", () => {
13 expect(previewPromptText(prompt, { ...prompt }, "")).toBe("");
14 });
15
16 it("initializes a newly opened prompt", () => {
17 expect(previewPromptText(null, prompt, "")).toBe("Guest");
18 });
19
20 it("resets a stale draft after reconnecting without dialog history", () => {
21 expect(previewPromptText(null, prompt, "Alice")).toBe("Guest");
22 });
23
24 it("resets when the message changes", () => {
25 expect(previewPromptText(prompt, { ...prompt, message: "Next name?" }, "Alice")).toBe("Guest");
26 });
27
28 it("resets when the default value changes", () => {
29 expect(previewPromptText(prompt, { ...prompt, defaultValue: "Bob" }, "Alice")).toBe("Bob");
30 });
31
32 it("resets when the dialog type changes with matching message and default", () => {
33 expect(previewPromptText(prompt, { ...prompt, type: "confirm" }, "Alice")).toBe("Guest");
34 });
35
36 it("clears a closed dialog and initializes an identical reopened prompt", () => {
37 const cleared = previewPromptText(prompt, null, "Alice");
38 expect(cleared).toBe("");
39 expect(previewPromptText(null, prompt, cleared)).toBe("Guest");
40 });
41});
apps/mobile/src/features/browser/previewPromptText.ts created+15
...@@ -0,0 +1,15 @@
1import type { PreviewStreamControl } from "@t3tools/client-runtime/preview/server-browser-stream";
2
3type Dialog = PreviewStreamControl["dialog"];
4
5/** Updates the native response draft when a browser control message arrives. */
6export function previewPromptText(previous: Dialog, next: Dialog, draft: string) {
7 if (
8 next?.type !== previous?.type ||
9 next?.message !== previous?.message ||
10 next?.defaultValue !== previous?.defaultValue
11 ) {
12 return next?.defaultValue ?? "";
13 }
14 return draft;
15}