import { useCallback, useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; import { Icon } from "./icon"; import type { ToolInstance } from "../api/sessions"; import { checkInstanceHealth, deleteInstance, listInstances, recreateInstanceTunnel, restartInstance, startInstance, stopInstance, } from "../api/sessions"; import type { ToolType } from "../api/tool_types"; import { CreateSessionForm } from "./create-session-form"; import { listConfigProfiles, type ConfigProfile } from "../api/config_profiles"; 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 [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); // Health check state const [healthStatus, setHealthStatus] = useState>({}); // Config profile selection for start/restart const [configProfiles, setConfigProfiles] = useState([]); const [profileSelectInstanceId, setProfileSelectInstanceId] = useState(null); const [selectedProfileForAction, setSelectedProfileForAction] = useState(""); // Per-instance busy state for actions const [busyInstanceId, setBusyInstanceId] = useState(null); 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]); useEffect(() => { void loadInstances(); }, [loadInstances]); // Health check polling useEffect(() => { const runningInstances = instances.filter(i => i.status === "running" && i.url?.startsWith("http")); if (runningInstances.length === 0) return; const checkHealth = async () => { for (const instance of runningInstances) { try { const health = await checkInstanceHealth(projectId, repoId, instance.id); setHealthStatus(prev => ({ ...prev, [instance.id]: { healthy: health.healthy, lastCheck: Date.now() } })); } catch { setHealthStatus(prev => ({ ...prev, [instance.id]: { healthy: false, lastCheck: Date.now() } })); } } }; // Check immediately void checkHealth(); // Then every 30 seconds const interval = setInterval(() => void checkHealth(), 30000); return () => clearInterval(interval); }, [instances, projectId, repoId]); const handleCreateSuccess = async () => { setShowCreate(false); await loadInstances(); }; const loadConfigProfiles = useCallback(async (toolTypeId: string) => { try { const profiles = await listConfigProfiles(projectId, toolTypeId); setConfigProfiles(profiles); } catch { // ignore } }, [projectId]); const handleStart = async (instanceId: string, configProfileId?: string) => { setBusyInstanceId(instanceId); try { await startInstance(projectId, repoId, instanceId, configProfileId); setProfileSelectInstanceId(null); setSelectedProfileForAction(""); await loadInstances(); } catch { setError("Failed to start instance"); } finally { setBusyInstanceId(null); } }; const handleStop = async (instanceId: string) => { setBusyInstanceId(instanceId); 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) => { setBusyInstanceId(instanceId); try { await restartInstance(projectId, repoId, instanceId, configProfileId); setProfileSelectInstanceId(null); setSelectedProfileForAction(""); 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); try { await deleteInstance(projectId, repoId, instanceId); // Update state immediately instead of reloading setInstances(prev => prev.filter(i => i.id !== instanceId)); } catch { setError("Failed to delete instance"); } finally { setBusyInstanceId(null); } }; const handleRecreateTunnel = async (instanceId: string) => { setBusyInstanceId(instanceId); try { await recreateInstanceTunnel(projectId, repoId, instanceId); await loadInstances(); } catch { setError("Failed to recreate tunnel"); } finally { setBusyInstanceId(null); } }; const getStatusColor = (status: string) => { switch (status) { case "running": return "var(--success)"; case "error": return "var(--danger)"; case "pending": case "building": return "var(--warning)"; default: return "var(--muted)"; } }; const isTunnelUnhealthy = (instance: ToolInstance) => { if (instance.status !== "running") return false; if (!instance.url?.startsWith("http")) return false; const health = healthStatus[instance.id]; if (!health) return false; return !health.healthy; }; return (

Tool Instances

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

Loading instances...

) : instances.length === 0 ? (

No instances yet. Launch a tool to get started.

) : (
{instances.map((instance) => (
{busyInstanceId === instance.id && (
)}
{instance.display_name}
{instance.status} {isTunnelUnhealthy(instance) && ( tunnel error )}
{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 {isTunnelUnhealthy(instance) && ( )} )} {instance.status === "running" && instance.tool_type_interfaces.includes("terminal") && ( )} {instance.status !== "running" && ( <> {profileSelectInstanceId === instance.id ? (
) : ( )} )} {instance.status === "running" && ( <> {stopConfirmId === instance.id ? (
Stop?
) : ( )} {profileSelectInstanceId === instance.id ? (
) : ( )} )}
))}
)} {showCreate && (

Launch Tool

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