import { useCallback, useEffect, useMemo, useState } from "react"; import { useNavigate } from "react-router-dom"; import { listProjects } from "../api/projects"; import { listRepositories } from "../api/git_repositories"; import { getUserSessions, deleteInstance, stopInstance, startInstance, checkInstanceHealth, recreateInstanceTunnel, createInstance, } from "../api/sessions"; import { listToolTypes } from "../api/tool_types"; import { getUserConfig, updateUserConfig } from "../api/settings"; import type { Project } from "../types/project"; import type { Session } from "../types/session"; import type { GitRepository } from "../types/git-repository"; import type { ToolType } from "../types/tool-type"; import { Icon } from "../components/icon"; import { LoadingState } from "../components/ui"; import { ErrorState } from "../components/ui"; type SessionsStatus = "loading" | "ready" | "error"; type CreateStatus = "idle" | "creating" | "error"; export const SessionsPage = () => { const navigate = useNavigate(); const [status, setStatus] = useState("loading"); const [sessions, setSessions] = useState([]); const [lastSessionId, setLastSessionId] = useState(null); const [projects, setProjects] = useState([]); const [repositories, setRepositories] = useState([]); const [toolTypes, setToolTypes] = useState([]); const [selectedProject, setSelectedProject] = useState(""); const [selectedRepo, setSelectedRepo] = useState(""); const [selectedToolType, setSelectedToolType] = useState(""); const [displayName, setDisplayName] = useState(""); const [createStatus, setCreateStatus] = useState("idle"); const [createError, setCreateError] = useState(null); const [deleteConfirmId, setDeleteConfirmId] = useState(null); const [stopConfirmId, setStopConfirmId] = useState(null); const [tunnelHealth, setTunnelHealth] = useState< Record< string, { healthy: boolean; status_code: number | null; error?: string } > >({}); const [recreatingId, setRecreatingId] = useState(null); const loadSessions = useCallback(async () => { setStatus("loading"); try { const [sessionsData, config] = await Promise.all([ getUserSessions(), getUserConfig(), ]); setSessions(sessionsData); setLastSessionId(config.last_session_id ?? null); setStatus("ready"); } catch { setStatus("error"); } }, []); useEffect(() => { void loadSessions(); }, [loadSessions]); useEffect(() => { const loadProjects = async () => { try { const data = await listProjects(); setProjects(data); } catch { // ignore } }; void loadProjects(); }, []); useEffect(() => { const loadToolTypes = async () => { try { const data = await listToolTypes(); setToolTypes(data); } catch { // ignore } }; void loadToolTypes(); }, []); // Poll tunnel health every 30 seconds for running instances useEffect(() => { const checkHealth = async () => { const runningSessions = sessions.filter( (s) => s.status === "running" && s.url, ); for (const session of runningSessions) { try { const health = await checkInstanceHealth( session.project_id, session.repository_id, session.id, ); setTunnelHealth((prev) => ({ ...prev, [session.id]: health, })); } catch { setTunnelHealth((prev) => ({ ...prev, [session.id]: { healthy: false, status_code: null, error: "check failed", }, })); } } }; // Check immediately and then every 30 seconds void checkHealth(); const interval = setInterval(() => void checkHealth(), 30000); return () => clearInterval(interval); }, [sessions]); useEffect(() => { if (!selectedProject) { setRepositories([]); return; } const loadRepos = async () => { try { const data = await listRepositories(selectedProject); setRepositories(data); } catch { setRepositories([]); } }; void loadRepos(); }, [selectedProject]); const activeSessions = useMemo( () => sessions.filter((s) => ["running", "building", "pending"].includes(s.status), ), [sessions], ); const recentSessions = useMemo( () => sessions .filter((s) => ["stopped", "error"].includes(s.status)) .slice(0, 5), [sessions], ); const lastSession = useMemo( () => sessions.find((s) => s.id === lastSessionId) ?? null, [sessions, lastSessionId], ); const handleCreate = async (e: React.FormEvent) => { e.preventDefault(); setCreateError(null); if (!selectedProject || !selectedRepo || !selectedToolType) { setCreateError("Project, repository, and tool type are required"); return; } setCreateStatus("creating"); try { const instance = await createInstance( selectedProject, selectedRepo, selectedToolType, displayName || undefined, ); // Auto-start the instance await startInstance(selectedProject, selectedRepo, instance.id); await updateUserConfig({ last_session_id: instance.id }); setCreateStatus("idle"); setSelectedProject(""); setSelectedRepo(""); setSelectedToolType(""); setDisplayName(""); await loadSessions(); } catch { setCreateStatus("error"); setCreateError("Failed to create session"); } }; const handleStop = async ( sessionId: string, projectId: string, repoId: string, ) => { try { await stopInstance(projectId, repoId, sessionId); setStopConfirmId(null); await loadSessions(); } catch { setStopConfirmId(null); } }; const handleDelete = async ( sessionId: string, projectId: string, repoId: string, ) => { try { await deleteInstance(projectId, repoId, sessionId); setDeleteConfirmId(null); // Remove from local state immediately setSessions((prev) => prev.filter((s) => s.id !== sessionId)); } catch { setDeleteConfirmId(null); } }; const handleRecreateTunnel = async (session: Session) => { setRecreatingId(session.id); try { await recreateInstanceTunnel( session.project_id, session.repository_id, session.id, ); // Refresh sessions to get new URL await loadSessions(); } catch { // ignore } finally { setRecreatingId(null); } }; const handleOpen = (session: Session) => { if (session.url) { window.open(session.url, "_blank", "noopener,noreferrer"); } else if (session.tool_type_interfaces?.includes("terminal")) { navigate(`/instances/${session.id}/terminal`); } else { navigate(`/projects/${session.project_id}`); } }; const handleResumeLast = async () => { if (!lastSession) return; // Find the project and repo IDs const project = projects.find((p) => p.name === lastSession.project_name); if (project) { navigate(`/projects/${project.id}`); } }; return (

Sessions

{status === "loading" && ( )} {status === "error" && ( void loadSessions()} /> )} {status === "ready" && ( <> {/* Last Session */} {lastSession && (

Last Session

{lastSession.display_name || lastSession.tool_type_name || "Unnamed Session"}

{lastSession.tool_type_name} · {lastSession.project_name} ·{" "} {lastSession.repository_name}

{lastSession.url && (

{lastSession.url}

)} {lastSession.status}
{lastSession.url ? ( Open ) : ( )}
)} {/* Active Sessions */}

Active Sessions {activeSessions.length > 0 && ( {activeSessions.length} )}

{activeSessions.length === 0 ? (

No active sessions

) : (
{activeSessions.map((session) => (

{session.display_name || session.tool_type_name || "Unnamed Session"}

{session.tool_type_name} · {session.project_name}

{session.url && (

{session.url}

)} {session.status} {tunnelHealth[session.id] && !tunnelHealth[session.id].healthy && ( tunnel error )}
{session.url ? ( Open ) : ( )} {tunnelHealth[session.id] && !tunnelHealth[session.id].healthy && ( )} {stopConfirmId === session.id ? (
Stop?
) : ( )} {deleteConfirmId === session.id ? (
) : ( )}
))}
)}
{/* Recent Sessions */} {recentSessions.length > 0 && (

Recent Sessions

{recentSessions.map((session) => (
{session.display_name || session.tool_type_name || "Unnamed Session"} {session.tool_type_name} · {session.project_name}
{session.url ? ( Open ) : ( )} {deleteConfirmId === session.id ? (
) : ( )}
))}
)} {/* Create Session */}

Create New Session

{createError &&

{createError}

}
)}
); };