diff --git a/.changeset/desktop-environment.md b/.changeset/desktop-environment.md new file mode 100644 index 000000000..80aa00105 --- /dev/null +++ b/.changeset/desktop-environment.md @@ -0,0 +1,13 @@ +--- +'@cloudflare/sandbox': patch +--- + +Add desktop environment support for AI computer-use workflows. + +Start a full Linux desktop (Xvfb + XFCE4 + x11vnc + noVNC) inside the +sandbox and control it programmatically via `sandbox.desktop.*` methods. +Supports screenshots, mouse clicks, keyboard input, and live browser +streaming via noVNC preview URLs. + +Enable with `sandbox.desktop.start()`. Requires the desktop container +image variant. diff --git a/examples/desktop/.gitignore b/examples/desktop/.gitignore new file mode 100644 index 000000000..2fbb0d2d1 --- /dev/null +++ b/examples/desktop/.gitignore @@ -0,0 +1,26 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local +.wrangler +.turbo + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/examples/desktop/Dockerfile b/examples/desktop/Dockerfile new file mode 100644 index 000000000..a50416672 --- /dev/null +++ b/examples/desktop/Dockerfile @@ -0,0 +1,8 @@ +FROM sandbox-desktop:local +EXPOSE 6080 + +# Cloudflare-branded desktop wallpaper +COPY assets/wallpaper.png /usr/share/backgrounds/cloudflare.png +COPY assets/set-wallpaper.sh /usr/local/bin/set-wallpaper.sh +RUN chmod +x /usr/local/bin/set-wallpaper.sh +COPY assets/set-wallpaper.desktop /etc/xdg/autostart/set-wallpaper.desktop diff --git a/examples/desktop/assets/set-wallpaper.desktop b/examples/desktop/assets/set-wallpaper.desktop new file mode 100644 index 000000000..a8fca841f --- /dev/null +++ b/examples/desktop/assets/set-wallpaper.desktop @@ -0,0 +1,7 @@ +[Desktop Entry] +Type=Application +Name=Set Wallpaper +Exec=/usr/local/bin/set-wallpaper.sh +Hidden=false +NoDisplay=true +X-GNOME-Autostart-enabled=true diff --git a/examples/desktop/assets/set-wallpaper.sh b/examples/desktop/assets/set-wallpaper.sh new file mode 100644 index 000000000..2543b602b --- /dev/null +++ b/examples/desktop/assets/set-wallpaper.sh @@ -0,0 +1,7 @@ +#!/bin/bash +# Set the Cloudflare wallpaper for whatever monitor XFCE detects. +# Runs as an XFCE autostart entry after xfdesktop initializes. +sleep 2 +for prop in $(xfconf-query -c xfce4-desktop -l 2>/dev/null | grep last-image); do + xfconf-query -c xfce4-desktop -p "$prop" -s "/usr/share/backgrounds/cloudflare.png" +done diff --git a/examples/desktop/assets/wallpaper.png b/examples/desktop/assets/wallpaper.png new file mode 100644 index 000000000..68df7f3a4 Binary files /dev/null and b/examples/desktop/assets/wallpaper.png differ diff --git a/examples/desktop/index.html b/examples/desktop/index.html new file mode 100644 index 000000000..ff202499b --- /dev/null +++ b/examples/desktop/index.html @@ -0,0 +1,13 @@ + + + + + + + Cloudflare Sandbox Desktop + + +
+ + + diff --git a/examples/desktop/package.json b/examples/desktop/package.json new file mode 100644 index 000000000..a609ef1e9 --- /dev/null +++ b/examples/desktop/package.json @@ -0,0 +1,31 @@ +{ + "name": "@cloudflare/sandbox-desktop-example", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "deploy": "vite build && wrangler deploy", + "typecheck": "tsc -b", + "cf-typegen": "wrangler types" + }, + "dependencies": { + "@cloudflare/kumo": "^1.8.0", + "@phosphor-icons/react": "^2.1.10", + "react": "^19.2.0", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@cloudflare/sandbox": "*", + "@cloudflare/vite-plugin": "^1.15.2", + "@tailwindcss/vite": "^4.2.1", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "tailwindcss": "^4.1.17", + "typescript": "^5.9.3", + "vite": "^7.2.4", + "wrangler": "^4.63.0" + } +} diff --git a/examples/desktop/src/App.tsx b/examples/desktop/src/App.tsx new file mode 100644 index 000000000..f60521cde --- /dev/null +++ b/examples/desktop/src/App.tsx @@ -0,0 +1,44 @@ +import { useRef } from 'react'; +import { DesktopViewer } from './components/DesktopViewer'; +import { Header } from './components/Header'; +import { useDesktop } from './hooks/useDesktop'; + +export default function App() { + const { isRunning, isStarting, streamUrl, error, start, stop } = useDesktop(); + const viewerRef = useRef(null); + + const handleStart = () => { + const el = viewerRef.current; + if (el) { + // Measure the available space and start the desktop at that resolution + start([el.clientWidth, el.clientHeight]); + } else { + start(); + } + }; + + return ( +
+
+ + {error && ( +
+ {error} +
+ )} + +
+ +
+
+ ); +} diff --git a/examples/desktop/src/api/client.ts b/examples/desktop/src/api/client.ts new file mode 100644 index 000000000..4a561dfe8 --- /dev/null +++ b/examples/desktop/src/api/client.ts @@ -0,0 +1,31 @@ +interface DesktopStatus { + status: 'active' | 'partial' | 'inactive'; + processes: Record< + string, + { running: boolean; pid?: number; uptime?: number } + >; + resolution: [number, number] | null; + dpi: number | null; +} +async function json(input: RequestInfo, init?: RequestInit): Promise { + const res = await fetch(input, init); + if (!res.ok) { + const body = await res.text(); + throw new Error(body || res.statusText); + } + return res.json() as Promise; +} + +export const api = { + startDesktop: (resolution?: [number, number]) => + json<{ success: boolean }>('/api/start', { + method: 'POST', + headers: resolution ? { 'Content-Type': 'application/json' } : undefined, + body: resolution ? JSON.stringify({ resolution }) : undefined + }), + stopDesktop: () => + json<{ success: boolean }>('/api/stop', { method: 'POST' }), + getStatus: () => json('/api/status'), + getStreamUrl: () => + json<{ url: string }>('/api/stream-url', { method: 'POST' }) +}; diff --git a/examples/desktop/src/components/DesktopViewer.tsx b/examples/desktop/src/components/DesktopViewer.tsx new file mode 100644 index 000000000..f42f65d40 --- /dev/null +++ b/examples/desktop/src/components/DesktopViewer.tsx @@ -0,0 +1,54 @@ +interface DesktopViewerProps { + isRunning: boolean; + isStarting: boolean; + streamUrl: string | null; +} + +export function DesktopViewer({ + isRunning, + isStarting, + streamUrl +}: DesktopViewerProps) { + if (isStarting) { + return ( +
+
+

Starting desktop environment…

+

This may take a few seconds

+
+
+ ); + } + + if (!isRunning) { + return ( +
+
+

Desktop is stopped

+

+ Click "Start Desktop" to begin +

+
+
+ ); + } + + if (!streamUrl) { + return ( +
+
+

Connecting…

+
+
+ ); + } + + return ( +