import { t } from "../../lib/console-text"; import { useWorkspaceRigs } from "../../lib/use-workspace-rigs"; import { useWorkspaceMachines } from "../../lib/use-workspace-machines"; import { StartupTimings, useStartupDetails, setStartupDetails } from "../../../session-ui"; import { buildTimeline } from "../../../timeline"; import { labelReasoningEffort } from "../../../model-policy"; import { ModelName } from "../../../components/model-mark"; import { PreferenceToggleRow } from "../transcription-settings"; import { SessionStatus as SessionStatusBadge } from "../../../components/session-status"; import type { SessionEventsConnectionState } from "../../../hooks/use-session-events"; import { MACHINES_SESSION_POLL_MS } from "../../../machines"; import { useNavigate } from "../../lib/navigation"; import { CopyIcon, FileJsonIcon, Loader2Icon, RotateCcwIcon } from "lucide-react"; import { useEffect, useMemo, useState } from "react"; import { toast } from "../../lib/toast"; import { ConnectionPill, CopyableMono, InfoRow, InspectorSection } from "../common"; import { ModelContextInspectorPane } from "./model-context-inspector"; import { sessionSandboxLabel } from "./sandbox-switcher"; import { Button } from "../../../ui/button"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../../../ui/collapsible"; import { ScrollArea } from "../../../ui/scroll-area"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../../ui/tabs"; import { Select } from "../../../ui/select"; import { useAppContext } from "../../app-context"; import { eventDisplayLabel, isTerminalSessionStatus } from "../../lib/events"; import { formatTimestamp } from "../../lib/format"; import { withOccurrenceKeys } from "../../lib/react-key"; import { retainSessionRestartAttemptAfterFailure, sessionRestartOperationController, } from "../../lib/session-restart-operation-controller"; import { classifySessionTenancyFailure } from "../../lib/session-tenancy"; import { repositoryDisplayName } from "../../lib/session-tools"; import type { Session, SessionEvent } from "../../types"; export function SessionInspector(props: { session: Session; events: SessionEvent[]; connectionState: SessionEventsConnectionState; onReloadSession: () => Promise; }) { const startupDetails = useStartupDetails(); const startupPhases = useMemo( () => buildTimeline(t, props.events).filter((item) => item.kind === "startup-phase"), [props.events], ); const context = useAppContext(); const navigate = useNavigate(); const rigs = useWorkspaceRigs({ workspaceId: props.session.workspaceId }); const [selectedRigId, setSelectedRigId] = useState(props.session.rigId ?? ""); const [restarting, setRestarting] = useState(false); const [runtimeFailure, setRuntimeFailure] = useState(null); const restartScope = useMemo( () => ({ principalId: `${context.accessContext.subjectId}:${context.accessKeyVersion}`, workspaceId: props.session.workspaceId, sessionId: props.session.id, }), [ context.accessContext.subjectId, context.accessKeyVersion, props.session.id, props.session.workspaceId, ], ); const pendingRestartAttempt = sessionRestartOperationController.snapshot(restartScope); useEffect(() => { setSelectedRigId(props.session.rigId ?? ""); }, [props.session.id, props.session.rigId]); const restartWithSetup = async () => { if (!props.session.tenancy) return; const attempt = sessionRestartOperationController.prepare( restartScope, { visibility: props.session.tenancy.visibility, rigId: selectedRigId || null, }, () => crypto.randomUUID(), ); setRestarting(true); setRuntimeFailure(null); let receiptConfirmed = false; try { const result = await context.client.forkSession(props.session.workspaceId, props.session.id, { idempotencyKey: attempt.idempotencyKey, visibility: attempt.visibility, workspaceSharedAcknowledged: false, rigId: attempt.rigId, }); receiptConfirmed = true; const destination = await context.client.getSession(result.workspaceId, result.sessionId, { fresh: true, }); if (destination.workspaceId !== result.workspaceId || destination.id !== result.sessionId) { throw new Error("The restart receipt did not resolve to the expected session."); } sessionRestartOperationController.settle(restartScope, attempt); toast.success("Restarted with new setup", { description: "The original session and sandbox were left unchanged.", }); void navigate({ to: "/workspaces/$workspaceId/sessions/$sessionId", params: { workspaceId: result.workspaceId, sessionId: result.sessionId }, }); } catch (error) { const classified = classifySessionTenancyFailure(error); if (!retainSessionRestartAttemptAfterFailure(error, receiptConfirmed)) { sessionRestartOperationController.settle(restartScope, attempt); } const message = classified.message; setRuntimeFailure(message); toast.error("Session could not be restarted", { description: message }); } finally { setRestarting(false); } }; const terminalSession = isTerminalSessionStatus(props.session.status); // The session's ACTIVE compute, not its home backend: when a connected machine // is active (live-swapped or targeted at create) show the machine name, so the // inspector agrees with the "Run on" header instead of reading "modal" while a // selfhosted box runs the turn. Degrades to the home backend when no machine is // active (or selfhosted is disabled → the fleet 404s to empty). const fleet = useWorkspaceMachines({ sessionId: props.session.id, pollIntervalMs: MACHINES_SESSION_POLL_MS, }); const activeMachine = fleet.machines.find((machine) => machine.active && machine.kind === "selfhosted") ?? null; // Compute context, honestly: don't fall back to the home backend while the // fleet is still resolving - that reads "Cloud sandbox" even when a machine // runs the turn. Show a loading/unavailable note until we actually know. The // home box uses the neutral label, never its hosting vendor. const computeUnknown = fleet.machines.length === 0 && (fleet.loading || Boolean(fleet.error)); const computeLabel = activeMachine ? "Machine" : "Sandbox"; const computeValue = activeMachine ? activeMachine.name : computeUnknown ? fleet.loading ? "Checking…" : "Unavailable" : sessionSandboxLabel(t, props.session.sandboxBackend); const displayEvents = props.events; const sortedEvents = [...displayEvents].sort((a, b) => b.sequence - a.sequence); const lifecycleEvents = [...displayEvents] .filter((event) => !event.type.endsWith(".delta")) .sort((a, b) => b.sequence - a.sequence); const repositories = props.session.resources.filter((resource) => resource.kind === "repository"); return (
{terminalSession ? "Archived debug" : "Debug"}
{props.events.length} events
Overview Context Events Timeline Startup Raw
setStartupDetails(!startupDetails)} wrapDescription />
} /> } /> ) : ( "none" ) } /> ) : ( "none" ) } /> } /> {props.session.tenancy ? (
Restart with sandbox environment

Sandbox Environment setup is immutable for a live sandbox. Restart creates an independent history fork with a fresh sandbox and leaves this session unchanged.

) : null} {runtimeFailure ? (

{runtimeFailure}

) : null}
{repositories.length === 0 ? (

No repositories selected for this session.

) : (
{withOccurrenceKeys(repositories, (resource) => JSON.stringify(resource)).map( ({ key, item: resource }) => (
{repositoryDisplayName(resource)}
{resource.uri}
ref {resource.ref} {resource.mountPath ? ( {resource.mountPath} ) : null}
), )}
)}
{sortedEvents.map((event) => ( ))}
{lifecycleEvents.map((event) => (
{eventDisplayLabel(event)} #{event.sequence}
{formatTimestamp(event.occurredAt)}
))}
); } function EventDebugRow({ event }: { event: SessionEvent }) { const [open, setOpen] = useState(false); return (
          {JSON.stringify(event.payload, null, 2)}
        
); } function RawJsonPane({ value }: { value: unknown }) { const json = JSON.stringify(value, null, 2); return (
{json}
); }