import { Trans, useLingui } from "@lingui/react/macro"; import type { Bot, ComputerStatus } from "@rakazo/contracts"; import { Button, cn, Input } from "@rakazo/ui-web"; import { ArrowLeft, ArrowRight, ExternalLink, Loader2, RotateCw } from "lucide-react"; import { type FormEvent, useCallback, useEffect, useRef, useState } from "react"; import { normalizeBrowserUrl } from "../../lib/browser-url"; import { rpc } from "../../lib/rpc"; import { screenIframeSandbox } from "../../pages/shell/computer-screen"; import { ComputersUnavailableHint, computersAreUnavailable } from "../ComputersUnavailableHint"; import { AgentCursor } from "../computer/AgentCursor"; import { BrowserCoworkOverlay } from "./BrowserCoworkOverlay"; type ChromeState = { url: string; title: string; loading: boolean; canGoBack: boolean; canGoForward: boolean; }; const EMPTY_CHROME: ChromeState = { url: "", title: "", loading: false, canGoBack: false, canGoForward: false, }; export function OoComputerBrowser({ active, computer, embeddedScreenUrl, computerScreenError, sandboxProvider, booting, onBoot, onRefreshScreen, }: { active: Bot; computer: ComputerStatus | null; embeddedScreenUrl: string | null; computerScreenError: React.ReactNode; sandboxProvider: string | undefined; booting: boolean; onBoot: () => Promise; onRefreshScreen: () => Promise; }) { const { t } = useLingui(); const [chrome, setChrome] = useState(EMPTY_CHROME); const [draft, setDraft] = useState(""); const [invalid, setInvalid] = useState(false); const [busy, setBusy] = useState(false); const editing = useRef(false); const inputRef = useRef(null); const engagedScreen = useRef(false); const refreshChrome = useCallback(async () => { if (computer?.state !== "running") return; try { const next = await rpc.computer.browserChromeState({ botId: active.id }); setChrome(next); if (!editing.current) setDraft(next.url); } catch { /* Address bar may lag the live tab while the computer boots. */ } }, [active.id, computer?.state]); useEffect(() => { if (computer?.state !== "running") return; void refreshChrome(); const timer = window.setInterval(() => void refreshChrome(), 4000); return () => window.clearInterval(timer); }, [computer?.state, refreshChrome]); useEffect(() => { if (computer?.state === "running" && (embeddedScreenUrl || computer?.kind === "desktop")) return; void onBoot().catch(() => undefined); }, [active.id, computer?.kind, computer?.state, embeddedScreenUrl, onBoot]); const runNav = useCallback( async (task: () => Promise) => { setBusy(true); try { const next = await task(); setChrome(next); if (!editing.current) setDraft(next.url); await onRefreshScreen(); } finally { setBusy(false); } }, [onRefreshScreen], ); const onSubmit = (event: FormEvent) => { event.preventDefault(); const url = normalizeBrowserUrl(draft); setInvalid(!url); if (!url) return; editing.current = false; void runNav(async () => { const next = await rpc.computer.browserNavigate({ botId: active.id, url }); await rpc.computer .browserUserAction({ botId: active.id, kind: "navigate", detail: url, }) .catch(() => undefined); return next; }); inputRef.current?.blur(); }; const reportUserAction = useCallback( (detail: { kind: "click" | "scroll" | "type"; detail: string }) => { void rpc.computer .browserUserAction({ botId: active.id, kind: detail.kind, detail: detail.detail, passwordField: detail.kind === "type" && detail.detail.length > 1 ? false : undefined, }) .catch(() => undefined); }, [active.id], ); const showScreen = computer?.state === "running" && embeddedScreenUrl && !computerScreenError; const unavailable = computersAreUnavailable(sandboxProvider); return ( <>
{ editing.current = true; event.currentTarget.select(); }} onBlur={() => { editing.current = false; }} onChange={(event) => { setDraft(event.target.value); setInvalid(false); }} className="mx-1 h-control-sm min-w-0 flex-1 rounded-full bg-muted/60 px-3 text-ui-body-sm" />
{unavailable ? (
) : showScreen ? ( <>