Recover saved Computer tabs after task environment changes

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-10 12:02:38 -05:00
1 parent 4e3cf6b07e
commit 85746677ff
2 files changed
+54 -3

No files matched your search

@@ -9,6 +9,7 @@ import { TooltipProvider } from "@/components/ui/tooltip";
import {
taskPanelAgentTasksTab,
taskPanelArtifactsTab,
taskPanelComputerTab,
taskPanelDocumentTab,
taskPanelPropertiesTab,
writeTaskSidePanelState,
@@ -26,8 +27,12 @@ class ResizeObserverStub {
(globalThis as { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserverStub as unknown as typeof ResizeObserver;
const browserFixture = vi.hoisted(() => ({ data: [] as import("@paperclipai/shared").TaskBrowser[], viewer: vi.fn(async () => ({ url: "https://live.browser-use.com/test-viewer" })), control: vi.fn(async () => ({})) }));
const computerFixture = vi.hoisted(() => ({ get: vi.fn<() => Promise<import("@paperclipai/shared").TaskComputer | null>>() }));
vi.mock("@/api/computers", () => ({ computersApi: { get: computerFixture.get } }));
const computerFixture = vi.hoisted(() => ({
get: vi.fn<() => Promise<import("@paperclipai/shared").TaskComputer | null>>(),
connect: vi.fn(),
disconnect: vi.fn(async () => undefined),
}));
vi.mock("@/api/computers", () => ({ computersApi: computerFixture }));
vi.mock("@/hooks/useTaskBrowsers", () => ({ useTaskBrowsers: () => ({ data: browserFixture.data, isError: false }) }));
vi.mock("@/api/browser-use", () => ({ browserUseApi: { viewer: browserFixture.viewer, control: browserFixture.control, presence: vi.fn(async () => ({ accepted: true })) } }));
const fixture = vi.hoisted(() => ({
@@ -135,6 +140,8 @@ describe("TaskSidePanel", () => {
browserFixture.data = [];
browserFixture.control.mockClear();
computerFixture.get.mockReset().mockResolvedValue(null);
computerFixture.connect.mockReset();
computerFixture.disconnect.mockClear();
fixture.documents = [];
fixture.plan = null;
routeFixture.location.search = "";
@@ -212,6 +219,36 @@ describe("TaskSidePanel", () => {
.some((item) => item.textContent === "Retry")).toBe(false);
});
it("releases a saved computer viewer after reassignment and opens the current computer", async () => {
const owner = { computerId: "computer-a", ownerId: "viewer-a", generation: 1 };
computerFixture.get.mockResolvedValue({ environmentId: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", name: "First Boat" });
computerFixture.connect.mockResolvedValue({
viewerUrl: "https://desktop.example.test/viewer",
expiresAt: new Date(Date.now() + 60_000).toISOString(), owner,
});
writeTaskSidePanelState("user-1", "company-1", "task-1", {
state: { tabs: [taskPanelComputerTab("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa")], activeTabId: "computer:aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa" },
launcherOpen: false, userInteracted: true, autoPlanHandled: false, updatedAt: 1,
});
await render(panel());
await act(async () => Array.from(container.querySelectorAll<HTMLButtonElement>("button"))
.find(button => button.textContent === "Connect")?.click());
expect(container.querySelector("iframe")).not.toBeNull();
await act(async () => queryClient.setQueryData(["task-computer", "task-1"], {
environmentId: "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb", name: "Second Boat",
}));
await vi.waitFor(() => expect(container.textContent).toContain("This task now uses a different computer."));
expect(container.querySelector("iframe")).toBeNull();
expect(computerFixture.disconnect).toHaveBeenCalledWith("task-1", "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", owner);
await act(async () => Array.from(container.querySelectorAll<HTMLButtonElement>("button"))
.find(button => button.textContent === "Open current computer")?.click());
expect(container.querySelector('[data-side-panel-tab-target="computer:aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"]')).toBeNull();
expect(container.querySelector('[data-side-panel-tab-target="computer:bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"][aria-selected="true"]')).not.toBeNull();
await act(async () => Array.from(container.querySelectorAll<HTMLButtonElement>("button"))
.find(button => button.textContent === "Connect")?.click());
expect(computerFixture.connect).toHaveBeenLastCalledWith("task-1", "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb");
});
it("lets an attachment request take focus from a workspace-file route", async () => {
routeFixture.location.search = "?file=README.md&workspace=project";
window.history.replaceState(null, "", `${routeFixture.location.pathname}${routeFixture.location.search}`);
@@ -868,7 +868,21 @@ export function TaskSidePanel({
>
{controller.tabs.map(tab => tab.payload.kind === "computer" ? (
<div key={tab.id} hidden={tab.id !== controller.activeTabId} className={cn("h-full min-h-0", tab.id !== controller.activeTabId && "hidden")}>
<TaskComputerPanel active={tab.id === controller.activeTabId} issueId={issue.id} environmentId={tab.payload.environmentId} />
{computerQuery.data !== undefined && computerQuery.data?.environmentId !== tab.payload.environmentId ? (
<div className="flex h-full flex-col items-center justify-center gap-4 p-6 text-center">
<p className="text-sm text-muted-foreground">
{computerQuery.data ? "This task now uses a different computer." : "This task no longer has a computer."}
</p>
{computerQuery.data && (
<Button onClick={() => {
controller.closeTab(tab.id);
controller.openTab(taskPanelComputerTab(computerQuery.data!.environmentId));
}}>Open current computer</Button>
)}
</div>
) : (
<TaskComputerPanel active={tab.id === controller.activeTabId} issueId={issue.id} environmentId={tab.payload.environmentId} />
)}
</div>
) : null)}
{controller.tabs.map(tab => tab.payload.kind === "browser" ? (