import { useCallback, useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; import { Icon } from "../../icon"; import { LoadingOverlay } from "../../loading-overlay"; import type { ToolInstance } from "../../../api/sessions"; import { deleteInstance, listInstances, restartInstance, startInstance, stopInstance, } from "../../../api/sessions"; import type { ToolType } from "../../../api/tool-types"; import { CreateSessionForm } from "../session/create-session-form"; import { listConfigProfiles, type ConfigProfile, } from "../../../api/config-profiles"; import { listSSHKeys, type SSHKey } from "../../../api/ssh-keys"; import { useEventContext } from "../../../state/events"; import { useSessions } from "../../../state/sessions"; const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? "http://localhost:8000"; interface InstanceListProps { projectId: string; repoId: string; projectName?: string; repoName?: string; toolTypes: ToolType[]; } export const InstanceList = ({ projectId, repoId, projectName, repoName, toolTypes, }: InstanceListProps) => { const navigate = useNavigate(); const { refreshSessions } = useSessions(); const [instances, setInstances] = useState([]); const [loading, setLoading] = useState(false); const [showCreate, setShowCreate] = useState(false); const [error, setError] = useState(null); // Stop confirmation const [stopConfirmId, setStopConfirmId] = useState(null); // Config profile selection for start/restart const [configProfiles, setConfigProfiles] = useState([]); const [profileSelectInstanceId, setProfileSelectInstanceId] = useState< string | null >(null); const [selectedProfileForAction, setSelectedProfileForAction] = useState(""); const [selectedSshKeyIdsForAction, setSelectedSshKeyIdsForAction] = useState< string[] >([]); const [sshKeys, setSshKeys] = useState([]); // Per-instance busy state for actions const [busyInstanceId, setBusyInstanceId] = useState(null); const [busyLabel, setBusyLabel] = useState("Working..."); const loadInstances = useCallback(async () => { setLoading(true); try { const data = await listInstances(projectId, repoId); setInstances(data); } catch { setError("Failed to load instances"); } finally { setLoading(false); } }, [projectId, repoId]); const { events } = useEventContext(); useEffect(() => { void loadInstances(); }, [loadInstances]); // Lightweight list refresh every 60 seconds for resilience useEffect(() => { const interval = setInterval(() => void loadInstances(), 60000); return () => clearInterval(interval); }, [loadInstances]); // Real-time status updates from SSE events useEffect(() => { if (events.length === 0) return; const latestEvent = events[events.length - 1]; const statusEvents = [ "instance.started", "instance.health_changed", "instance.error", "instance.stopped", "instance.restarted", ]; if (!statusEvents.includes(latestEvent.event)) return; setInstances((prev) => prev.map((inst) => inst.id === latestEvent.instance_id ? { ...inst, status: latestEvent.status ?? inst.status } : inst, ), ); }, [events]); const handleCreateSuccess = async () => { setShowCreate(false); await loadInstances(); await refreshSessions(); }; const loadConfigProfiles = useCallback( async (toolTypeId: string) => { try { const [profiles, keys] = await Promise.all([ listConfigProfiles(projectId, toolTypeId), listSSHKeys(), ]); setConfigProfiles(profiles); setSshKeys(keys); } catch { // ignore } }, [projectId], ); const handleStart = async ( instanceId: string, configProfileId?: string, sshKeyIds?: string[], ) => { setBusyInstanceId(instanceId); setBusyLabel("Starting..."); try { await startInstance( projectId, repoId, instanceId, configProfileId, sshKeyIds, ); setProfileSelectInstanceId(null); setSelectedProfileForAction(""); setSelectedSshKeyIdsForAction([]); await loadInstances(); } catch { setError("Failed to start instance"); } finally { setBusyInstanceId(null); } }; const handleStop = async (instanceId: string) => { setBusyInstanceId(instanceId); setBusyLabel("Stopping..."); try { await stopInstance(projectId, repoId, instanceId); setStopConfirmId(null); await loadInstances(); } catch { setError("Failed to stop instance"); } finally { setBusyInstanceId(null); } }; const handleRestart = async ( instanceId: string, configProfileId?: string, sshKeyIds?: string[], ) => { setBusyInstanceId(instanceId); setBusyLabel("Restarting..."); try { await restartInstance( projectId, repoId, instanceId, configProfileId, sshKeyIds, ); setProfileSelectInstanceId(null); setSelectedProfileForAction(""); setSelectedSshKeyIdsForAction([]); await loadInstances(); } catch { setError("Failed to restart instance"); } finally { setBusyInstanceId(null); } }; const handleDelete = async (instanceId: string) => { if (!confirm("Are you sure you want to delete this instance?")) return; setBusyInstanceId(instanceId); setBusyLabel("Deleting..."); try { await deleteInstance(projectId, repoId, instanceId); // Update state immediately instead of reloading setInstances((prev) => prev.filter((i) => i.id !== instanceId)); await refreshSessions(); } catch { setError("Failed to delete instance"); } finally { setBusyInstanceId(null); } }; const getStatusColor = (status: string) => { switch (status) { case "running": return "var(--success)"; case "starting": case "probing": return "var(--info)"; case "unhealthy": return "var(--warning)"; case "error": return "var(--danger)"; case "pending": case "building": return "var(--warning)"; default: return "var(--muted)"; } }; return (

Tool Instances

{error &&
{error}
} {loading ? (

Loading instances...

) : instances.length === 0 ? (

No instances yet. Launch a tool to get started.

) : (
{instances.map((instance) => (
{instance.display_name}
{instance.status}
{instance.selected_config_profile_id && (
Profile:{" "} {configProfiles.find( (p) => p.id === instance.selected_config_profile_id, )?.name || instance.selected_config_profile_id}
)}
{instance.status === "running" && instance.url && instance.tool_type_interfaces.includes("web") && ( <> Open )} {instance.status === "running" && instance.tool_type_interfaces.includes("terminal") && ( )} {instance.status !== "running" && ( <> {profileSelectInstanceId === instance.id ? (
{sshKeys.map((key) => ( ))}
) : ( )} )} {instance.status === "running" && ( <> {stopConfirmId === instance.id ? (
Stop?
) : ( )} {profileSelectInstanceId === instance.id ? (
{sshKeys.map((key) => ( ))}
) : ( )} )}
))}
)} {showCreate && (

Launch Tool

setShowCreate(false)} submitLabel="Launch" />
)}
); };