| author | |
| committer | |
| log | aa2bfc604c538dbaa7e2a7f8698104503497d47a |
| tree | d6284fae3a2915ddc457ee2438bd2ad2047500c7 |
| parent | 7f02b849584332368a5489631d3286409eed90a8 |
| signature | Signed by PGP key B5690EEEBB952194 |
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 | 26 | import { downloadAndShareAttachment } from "../../lib/attachmentDownload"; |
| 27 | 27 | import { beginForegroundHandoff } from "../../lib/foreground-handoff"; |
| 28 | 28 | import { usePreviewStreamAccess } from "../../state/preview"; |
| 29 | import { previewPromptText } from "./previewPromptText"; | |
| 29 | 30 | |
| 30 | 31 | import { |
| 31 | 32 | previewStreamDocument, |
| ... | ... | @@ -256,6 +257,7 @@ function PreviewStreamDocumentView({ |
| 256 | 257 | const [hostSetup, setHostSetup] = useState<PreviewStreamHostSetup | null>(null); |
| 257 | 258 | const [started, setStarted] = useState(false); |
| 258 | 259 | const [control, setControl] = useState<PreviewStreamControl | null>(null); |
| 260 | const dialogRef = useRef<PreviewStreamControl["dialog"]>(null); | |
| 259 | 261 | const [promptText, setPromptText] = useState(""); |
| 260 | 262 | const [fileChooser, setFileChooser] = useState<PreviewStreamFileChooser | null>(null); |
| 261 | 263 | const answerFileChooser = (pick: boolean) => { |
| ... | ... | @@ -371,11 +373,14 @@ function PreviewStreamDocumentView({ |
| 371 | 373 | const message = previewStreamMessage(event.nativeEvent.data); |
| 372 | 374 | if (message === null) return; |
| 373 | 375 | switch (message.type) { |
| 374 | case "control": | |
| 376 | case "control": { | |
| 375 | 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 | 381 | onControl?.(message); |
| 378 | 382 | return; |
| 383 | } | |
| 379 | 384 | case "unauthorized": |
| 380 | 385 | if (!onUnauthorized()) { |
| 381 | 386 | fail("This session can't open the browser stream. Reconnect to try again."); |
| ... | ... | @@ -413,6 +418,8 @@ function PreviewStreamDocumentView({ |
| 413 | 418 | } |
| 414 | 419 | setStatus(message.status); |
| 415 | 420 | if (message.status === "connecting") { |
| 421 | // Dialogs may have closed and reopened while this viewer was disconnected. | |
| 422 | dialogRef.current = null; | |
| 416 | 423 | setControl(null); |
| 417 | 424 | onControl?.(null); |
| 418 | 425 | } |
apps/mobile/src/features/browser/previewPromptText.test.ts created+41| ... | ... | @@ -0,0 +1,41 @@ |
| 1 | import { describe, expect, it } from "vite-plus/test"; | |
| 2 | ||
| 3 | import { previewPromptText } from "./previewPromptText"; | |
| 4 | ||
| 5 | describe("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 @@ |
| 1 | import type { PreviewStreamControl } from "@t3tools/client-runtime/preview/server-browser-stream"; | |
| 2 | ||
| 3 | type Dialog = PreviewStreamControl["dialog"]; | |
| 4 | ||
| 5 | /** Updates the native response draft when a browser control message arrives. */ | |
| 6 | export 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 | } |