import type { ComputerMode, ComputerReleaseReason } from "@rakazo/contracts"; import { useLocalSearchParams, useNavigation } from "expo-router"; import { useEffect, useLayoutEffect, useRef, useState } from "react"; import { Modal, Pressable, Text, View } from "react-native"; import { initialWindowMetrics, SafeAreaProvider, SafeAreaView, } from "react-native-safe-area-context"; import { WebView } from "react-native-webview"; import { ComputerMaintenanceActions } from "../components/computer-maintenance-actions"; import { ComputerModePicker } from "../components/computer-mode-picker"; import { NativeSymbol } from "../components/native-symbol"; import { currentApiBase, rpc } from "../lib/api"; import { COMPUTER_HEARTBEAT_MS, type ComputerStatus, computerLabel, controlLabel, embeddableScreenUrl, previewPlaceholder, readScreenUrl, SCREEN_URL_OPEN_ATTEMPTS, } from "../lib/computer"; export default function Computer() { const navigation = useNavigation(); const { botId, name: nameParam } = useLocalSearchParams<{ botId?: string; name?: string }>(); const name = nameParam || "Bot"; const [computer, setComputer] = useState(null); const [screenUrl, setScreenUrl] = useState(null); const [screenError, setScreenError] = useState(null); const [error, setError] = useState(null); const [ready, setReady] = useState(false); const [booting, setBooting] = useState(false); const [switching, setSwitching] = useState(false); const [computerOpen, setComputerOpen] = useState(false); const autoBooted = useRef(null); const embeddedScreenUrl = embeddableScreenUrl(screenUrl, currentApiBase()); const hasControl = computer?.controlHolder === "user" && computer.controlBotId === botId; const label = computerLabel(computer?.mode, name); useLayoutEffect(() => { navigation.setOptions({ title: label }); }, [label, navigation]); async function refreshScreen(attempts: number) { if (!botId) return; try { setScreenUrl( await readScreenUrl(() => rpc<{ url: string | null }>("computer/screenUrl", { botId }), { attempts, }), ); } catch { // Keep the last known URL. Cold boots often fail this RPC once, then succeed. } } async function refresh(options?: { screenAttempts?: number }) { if (!botId) return; const status = await rpc("computer/status", { botId }); setComputer(status); await refreshScreen(options?.screenAttempts ?? 1); setReady(true); return status; } useEffect(() => { void refresh().catch((err: Error) => { setError(err.message); setReady(true); }); const timer = setInterval(() => void refresh().catch(() => undefined), 2000); return () => clearInterval(timer); }, [botId]); async function bootComputer({ takeControl, overlay, force = false, }: { takeControl: boolean; overlay: boolean; force?: boolean; }) { if (!botId) return; const needsBoot = force || computer?.state !== "running" || !screenUrl; if (overlay && needsBoot) setBooting(true); try { if (needsBoot) await rpc("computer/boot", { botId }); if (takeControl) await rpc("computer/takeover", { botId }); await refresh({ screenAttempts: SCREEN_URL_OPEN_ATTEMPTS }); setError(null); } catch (err) { setError(err instanceof Error ? err.message : "Could not open computer"); throw err; } finally { setBooting(false); } } useEffect(() => { if (!ready || !botId) return; if (computer?.state === "booting" || computer?.state === "suspended") return; if (autoBooted.current === botId) return; autoBooted.current = botId; void bootComputer({ takeControl: false, overlay: computer?.state !== "running", force: true, }).catch(() => undefined); }, [ready, botId, computer?.state]); useEffect(() => { if (!botId || computer?.state !== "running") return; const ping = () => void rpc("computer/heartbeat", { botId }).catch(() => undefined); ping(); const timer = setInterval(ping, COMPUTER_HEARTBEAT_MS); return () => clearInterval(timer); }, [botId, computer?.state]); async function openComputer() { if (!botId) return; const needsTakeover = !(computer?.controlHolder === "user" && computer.controlBotId === botId); try { await bootComputer({ takeControl: needsTakeover, overlay: needsTakeover || computer?.state !== "running", force: computer?.state !== "running", }); setComputerOpen(true); setScreenError(null); } catch { // error already set } } async function releaseComputer(reason?: ComputerReleaseReason) { if (!botId) return; await rpc("computer/release", { botId, reason }).catch(() => undefined); setComputerOpen(false); await refresh().catch(() => undefined); } async function setComputerMode(mode: ComputerMode) { if (!botId || mode === computer?.mode) return; setSwitching(true); setError(null); try { if (hasControl) { await rpc("computer/release", { botId, reason: computer?.takeoverRequested ? "skipped" : undefined, }); } await rpc("bots/setComputer", { botId, mode }); setComputer(null); setScreenUrl(null); autoBooted.current = null; await refresh(); } catch (err) { setError(err instanceof Error ? err.message : "Could not switch computer"); } finally { setSwitching(false); } } const placeholder = screenError ?? previewPlaceholder(computer?.state, booting, name, computer?.mode); return ( {error ? {error} : null} {computerOpen ? ( Open in full window ) : computer?.state === "running" && embeddedScreenUrl ? ( setScreenError("Could not load the desktop. This device cannot reach the screen URL.") } /> ) : ( {placeholder} )} void openComputer()} style={{ position: "absolute", top: 0, right: 0, bottom: 0, left: 0 }} /> {controlLabel(computer, name, botId)} {hasControl ? ( ) : ( void openComputer()} style={{ backgroundColor: "#1A1A1D", paddingHorizontal: 14, paddingVertical: 10, borderRadius: 12, }} > Take control )} {computer?.state === "error" || computer?.state === "stopped" || (computer?.state === "running" && !embeddedScreenUrl) ? ( { await refresh(); }} /> ) : null} void setComputerMode(mode)} /> Teach a task Recording a live demonstration needs desktop or web with the full computer view. You can still ask this bot to run saved skills from chat. { if (!booting) setComputerOpen(false); }} > {booting ? ( Booting {label} ) : ( {label} {hasControl ? ( You have control ) : null} {hasControl ? ( ) : ( void bootComputer({ takeControl: true, overlay: false }).catch( () => undefined, ) } hitSlop={8} style={{ borderWidth: 1, borderColor: "#26262A", paddingHorizontal: 12, paddingVertical: 8, borderRadius: 10, minHeight: 44, justifyContent: "center", }} > Take control )} setComputerOpen(false)} style={{ minWidth: 44, minHeight: 44, alignItems: "center", justifyContent: "center", }} > {computer?.state === "running" && embeddedScreenUrl ? ( setScreenError( "Could not load the desktop. This device cannot reach the screen URL.", ) } /> ) : ( {computer?.state === "suspended" ? "Computer is asleep" : computerLabel(computer?.mode, name)} )} )} ); } function ComputerReleaseActions({ takeoverRequested, onRelease, }: { takeoverRequested: boolean; onRelease: (reason?: ComputerReleaseReason) => Promise; }) { const actions: Array<{ label: string; reason?: ComputerReleaseReason; primary?: boolean }> = takeoverRequested ? [ { label: "Skip", reason: "skipped" }, { label: "I’m done", reason: "done", primary: true }, ] : [{ label: "Release" }]; return ( {actions.map((action) => ( void onRelease(action.reason)} hitSlop={8} style={{ minHeight: 44, justifyContent: "center", borderWidth: 1, borderColor: action.primary ? "#F1F1EF" : "#26262A", backgroundColor: action.primary ? "#F1F1EF" : "#1A1A1D", paddingHorizontal: 12, paddingVertical: 8, borderRadius: 10, }} > {action.label} ))} ); } function ScreenWebView({ url, interactive, onError, }: { url: string; interactive: boolean; onError: () => void; }) { return ( ); }