From 3c82b39f3dafbcc84cc5885989398bca66df69cc Mon Sep 17 00:00:00 2001 From: claude-ceo-assistant Date: Sat, 23 May 2026 16:28:14 -0700 Subject: [PATCH] feat(display): proxy native desktop streams after takeover --- canvas/package-lock.json | 7 + canvas/package.json | 1 + canvas/src/components/tabs/DisplayTab.tsx | 84 +++++++-- .../tabs/__tests__/DisplayTab.test.tsx | 79 +++++++- canvas/src/types/novnc.d.ts | 9 + workspace-server/cmd/server/cp_config_test.go | 5 +- .../internal/handlers/workspace_compute.go | 29 +-- .../handlers/workspace_compute_test.go | 146 +++++++++++++-- .../handlers/workspace_display_control.go | 59 ++++++ .../workspace_display_control_test.go | 58 ++++++ .../handlers/workspace_display_session.go | 168 ++++++++++++++++++ workspace-server/internal/router/router.go | 1 + .../router/workspace_display_route_test.go | 20 +++ 13 files changed, 601 insertions(+), 65 deletions(-) create mode 100644 canvas/src/types/novnc.d.ts create mode 100644 workspace-server/internal/handlers/workspace_display_session.go diff --git a/canvas/package-lock.json b/canvas/package-lock.json index e575c232a..e45dd64a3 100644 --- a/canvas/package-lock.json +++ b/canvas/package-lock.json @@ -8,6 +8,7 @@ "name": "molecule-monorepo-canvas", "version": "0.1.0", "dependencies": { + "@novnc/novnc": "^1.7.0", "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-tabs": "^1.1.12", @@ -1110,6 +1111,12 @@ "node": ">= 10" } }, + "node_modules/@novnc/novnc": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@novnc/novnc/-/novnc-1.7.0.tgz", + "integrity": "sha512-ucEJOx4T2avIRCleodk7YobZj5O2Ga2AeLfQ69A/yjG9HHba2+PDgwSkN3FttrmG+70ZGx21sElNFouK13RzyA==", + "license": "MPL-2.0" + }, "node_modules/@oxc-project/types": { "version": "0.127.0", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz", diff --git a/canvas/package.json b/canvas/package.json index b66efbf13..8e22d00c9 100644 --- a/canvas/package.json +++ b/canvas/package.json @@ -11,6 +11,7 @@ "test:coverage": "vitest run --coverage" }, "dependencies": { + "@novnc/novnc": "^1.7.0", "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-tabs": "^1.1.12", diff --git a/canvas/src/components/tabs/DisplayTab.tsx b/canvas/src/components/tabs/DisplayTab.tsx index f79177c99..5944db28a 100644 --- a/canvas/src/components/tabs/DisplayTab.tsx +++ b/canvas/src/components/tabs/DisplayTab.tsx @@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from "react"; import { api } from "@/lib/api"; +import type RFB from "@novnc/novnc"; interface DisplayStatus { available: boolean; @@ -11,13 +12,13 @@ interface DisplayStatus { protocol?: string; width?: number; height?: number; - viewer_url?: string; } interface DisplayControlStatus { controller: "none" | "user" | "agent"; controlled_by?: string; expires_at?: string; + session_url?: string; } interface Props { @@ -30,6 +31,7 @@ export function DisplayTab({ workspaceId }: Props) { const [error, setError] = useState(null); const [controlError, setControlError] = useState(null); const [controlBusy, setControlBusy] = useState(false); + const [sessionUrl, setSessionUrl] = useState(null); const requestGeneration = useRef(0); useEffect(() => { @@ -38,6 +40,7 @@ export function DisplayTab({ workspaceId }: Props) { let cancelled = false; setStatus(null); setControl(null); + setSessionUrl(null); setError(null); setControlError(null); setControlBusy(false); @@ -78,6 +81,7 @@ export function DisplayTab({ workspaceId }: Props) { }); if (requestGeneration.current !== generation) return; setControl(next); + setSessionUrl(next.session_url || null); } catch (err) { if (requestGeneration.current !== generation) return; setControlError("Failed to take control"); @@ -103,6 +107,7 @@ export function DisplayTab({ workspaceId }: Props) { const next = await api.post(`${controlPath}/release`, {}); if (requestGeneration.current !== generation) return; setControl(next); + setSessionUrl(null); } catch (err) { if (requestGeneration.current !== generation) return; setControlError("Failed to release control"); @@ -224,24 +229,19 @@ export function DisplayTab({ workspaceId }: Props) { control={control} controlBusy={controlBusy} controlError={controlError} + hasSession={!!sessionUrl} onAcquire={acquireControl} onRelease={releaseControl} /> - {status.viewer_url ? ( -