From 76a499cc54a6d189d12c62b6f2e9cd4f492b4794 Mon Sep 17 00:00:00 2001 From: Dotta Date: Sat, 10 Oct 2026 12:22:44 -0500 Subject: [PATCH] Keep an explicit link to prepared Boat previews Co-Authored-By: Paperclip --- .../TaskComputerPanel.test.tsx | 29 +++++++++++++++++++ .../task-side-panel/TaskComputerPanel.tsx | 15 ++++++---- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/ui/src/components/task-side-panel/TaskComputerPanel.test.tsx b/ui/src/components/task-side-panel/TaskComputerPanel.test.tsx index c97a3be6890..ee143e15c14 100644 --- a/ui/src/components/task-side-panel/TaskComputerPanel.test.tsx +++ b/ui/src/components/task-side-panel/TaskComputerPanel.test.tsx @@ -41,6 +41,35 @@ beforeEach(() => { afterEach(async () => { if (!unmounted) await unmount(); host.remove(); delete (Element.prototype as { checkVisibility?: unknown }).checkVisibility; vi.restoreAllMocks(); vi.useRealTimers(); }); describe("Computer connection lifetime", () => { + it("offers an explicit preview link when the browser blocks the new window", async () => { + vi.spyOn(window, "open").mockReturnValue(null); + vi.mocked(computersApi.preview).mockResolvedValue({ url: "https://preview.example/app" }); + await render(); + await act(async () => host.querySelector("form")!.dispatchEvent(new Event("submit", { bubbles: true, cancelable: true }))); + expect(computersApi.preview).toHaveBeenCalledWith("task-a", "environment-a", 5173); + const link = host.querySelector("a")!; + expect(link.href).toBe("https://preview.example/app"); + expect(link.textContent).toBe("Open port 5173 in a new tab"); + expect(link.rel).toBe("noopener noreferrer"); + await render({ environmentId: "environment-b" }); + expect(host.querySelector("a")).toBeNull(); + }); + + it("discards a preview response after the task changes", async () => { + const close = vi.fn(); + const replace = vi.fn(); + vi.spyOn(window, "open").mockReturnValue({ close, location: { replace } } as unknown as Window); + const pending = deferred<{ url: string }>(); + vi.mocked(computersApi.preview).mockReturnValue(pending.promise); + await render(); + await act(async () => host.querySelector("form")!.dispatchEvent(new Event("submit", { bubbles: true, cancelable: true }))); + await render({ issueId: "task-b" }); + await act(async () => pending.resolve({ url: "https://preview.example/app" })); + expect(close).toHaveBeenCalledOnce(); + expect(replace).not.toHaveBeenCalled(); + expect(host.querySelector("a")).toBeNull(); + }); + it("connects only after the explicit action and keeps credentials in the iframe with no referrer", async () => { await render(); await tick(60_000); expect(computersApi.connect).not.toHaveBeenCalled(); diff --git a/ui/src/components/task-side-panel/TaskComputerPanel.tsx b/ui/src/components/task-side-panel/TaskComputerPanel.tsx index 04d5d4c6feb..da3cb214dd0 100644 --- a/ui/src/components/task-side-panel/TaskComputerPanel.tsx +++ b/ui/src/components/task-side-panel/TaskComputerPanel.tsx @@ -23,19 +23,22 @@ function ComputerConnection({ issueId, environmentId, active = true }: ComputerP const connectPending = useRef(false); const [port, setPort] = useState("5173"); const [previewPending, setPreviewPending] = useState(false); + const [preview, setPreview] = useState<{ url: string; port: string } | null>(null); async function openPreview() { const previewWindow = window.open("about:blank", "_blank"); - if (!previewWindow) { setError("Allow a new tab to open the preview."); return; } - previewWindow.opener = null; + if (previewWindow) previewWindow.opener = null; setPreviewPending(true); + setPreview(null); setError(null); try { const result = await computersApi.preview(issueId, environmentId, Number(port)); - if (mounted.current) previewWindow.location.replace(result.url); - else previewWindow.close(); + if (mounted.current) { + setPreview({ url: result.url, port }); + previewWindow?.location.replace(result.url); + } else previewWindow?.close(); } catch (cause) { - previewWindow.close(); + previewWindow?.close(); if (mounted.current) setError(cause instanceof Error ? cause.message : "Could not open the preview."); } finally { if (mounted.current) setPreviewPending(false); } } @@ -155,6 +158,8 @@ function ComputerConnection({ issueId, environmentId, active = true }: ComputerP className="min-w-0 flex-1" onChange={event => setPort(event.target.value)} /> + {preview && preview.port === port && Open port {preview.port} in a new tab} ; }