import { useCallback, useReducer, useRef, useState, } from "react"; import { ArrowPathIcon, ArrowTopRightOnSquareIcon, ClipboardDocumentIcon, } from "@heroicons/react/20/solid"; import { SECONDARY_BUTTON_CLASS, Tooltip } from "./ui"; import { ErrorState } from "./state"; import { useToast } from "./toast"; import { apiData, humanInTheLoopApi } from "../lib/api-client"; import { useRunState } from "../lib/queries"; import { sandboxInstance } from "../lib/run-sandbox-lifecycle"; import { buildFullScreenTerminalUrl, sandboxStatusDetail, terminalAccessCommandLabel, } from "./terminal-view-helpers"; import { TERMINAL_BACKGROUND, useTerminalSession, type ConnectionStatus, type TerminalConnectionError, } from "../hooks/use-terminal-session"; const ICON_BUTTON_CLASS = "inline-flex size-9 items-center justify-center rounded-lg text-fg-2 outline-1 -outline-offset-1 outline-white/10 transition-colors hover:bg-overlay hover:text-fg focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-teal-500"; function terminalAccessCommandCopiedMessage(provider: string | null): string { return provider === "docker" ? "Docker exec command copied." : "SSH command copied."; } function terminalAccessCommandErrorMessage(provider: string | null): string { return provider === "docker" ? "Could not copy Docker exec command." : "Could not copy SSH command."; } function statusDotClasses(status: ConnectionStatus): string { switch (status) { case "ready": return "bg-teal-500"; case "error": return "bg-coral"; case "closed": return "bg-fg-muted"; case "connecting": return "bg-amber animate-pulse"; } } function statusLabel(status: ConnectionStatus): string { switch (status) { case "ready": return "Connected"; case "error": return "Error"; case "closed": return "Closed"; case "connecting": return "Connecting"; } } function StatusPill({ status, detail, }: { status: ConnectionStatus; detail: string | null; }) { return ( ); } export default function TerminalView({ runId, leading, chromeless = false, }: { runId: string; leading?: React.ReactNode; chromeless?: boolean; }) { const { push } = useToast(); const stateQuery = useRunState(runId); const sandbox = stateQuery.data?.sandbox ?? null; const provider = sandboxInstance(sandbox)?.provider ?? null; const sandboxDetail = sandboxStatusDetail(sandbox); const accessCommandLabel = terminalAccessCommandLabel(provider); const [connectionKey, reconnectTerminal] = useReducer((key: number) => key + 1, 0); const [status, setStatus] = useState("connecting"); const [error, setError] = useState(null); const terminalEl = useRef(null); const headingId = `run-terminal-${runId}`; useTerminalSession({ connectionKey, runId, setError, setStatus, terminalEl, }); const reconnect = useCallback(() => { setError(null); setStatus("connecting"); reconnectTerminal(); }, []); const copyAccessCommand = useCallback(async () => { if (!accessCommandLabel) return; try { const response = await apiData(() => humanInTheLoopApi.createRunSshAccess(runId, { ttl_minutes: 60 }), ); await navigator.clipboard.writeText(response.command); push({ message: terminalAccessCommandCopiedMessage(provider) }); } catch (err) { push({ tone: "error", message: err instanceof Error ? err.message : terminalAccessCommandErrorMessage(provider), }); } }, [accessCommandLabel, runId, provider, push]); return (

Terminal

{!chromeless && (
{leading}
{accessCommandLabel && ( )}
)} {error ? (
) : chromeless ? (
) : (
)}
); }