import { useState, useCallback, useRef } from "react"; import { stopInstance, deleteInstance, startInstance, recreateInstanceTunnel, renameInstance, } from "../api/sessions"; import type { Session } from "../api/sessions"; interface UseInstanceActionsOptions { onRefresh: () => Promise; } interface UseInstanceActionsReturn { loadingSessionId: string | null; dirtyDeleteSession: Session | null; dirtyDeleteFiles: string[]; handleOpen: (session: Session) => void; handleStart: (session: Session) => Promise; handleStop: (session: Session) => Promise; handleDelete: (session: Session) => Promise; handleForceDelete: (session: Session) => Promise; handleRecreateTunnel: (session: Session) => Promise; handleRename: (session: Session, newName: string) => Promise; clearDirtyDelete: () => void; } export function useInstanceActions( options: UseInstanceActionsOptions, ): UseInstanceActionsReturn { const { onRefresh } = options; const [loadingSessionId, setLoadingSessionId] = useState(null); const [dirtyDeleteSession, setDirtyDeleteSession] = useState( null, ); const [dirtyDeleteFiles, setDirtyDeleteFiles] = useState([]); const tabRefs = useRef>(new Map()); const handleOpen = useCallback((session: Session) => { const key = session.id; const existing = tabRefs.current.get(key); if (existing && !existing.closed) { existing.focus(); return; } let url: string; if (session.url) { url = session.url; } else if (session.tool_type_interfaces?.includes("terminal")) { url = `/instances/${session.id}/terminal`; } else { url = `/projects/${session.project_id}`; } document.title = `${session.display_name} — Headquarter`; const w = window.open(url, `session-${session.id}`); tabRefs.current.set(key, w); }, []); const handleStart = useCallback( async (session: Session) => { if (loadingSessionId === session.id) return; setLoadingSessionId(session.id); try { await startInstance( session.project_id, session.repository_id, session.id, ); await onRefresh(); } catch { // ignore } finally { setLoadingSessionId(null); } }, [loadingSessionId, onRefresh], ); const handleStop = useCallback( async (session: Session) => { if (loadingSessionId === session.id) return; setLoadingSessionId(session.id); try { await stopInstance( session.project_id, session.repository_id, session.id, ); await onRefresh(); } catch { // ignore } finally { setLoadingSessionId(null); } }, [loadingSessionId, onRefresh], ); const handleDelete = useCallback( async (session: Session) => { if (loadingSessionId === session.id) return; setLoadingSessionId(session.id); try { await deleteInstance( session.project_id, session.repository_id, session.id, ); setDirtyDeleteSession(null); setDirtyDeleteFiles([]); await onRefresh(); } catch (error) { const axiosError = error as { response?: { status?: number; data?: { detail?: { changed_files?: string[] } }; }; }; if (axiosError.response?.status === 409) { const detail = axiosError.response.data?.detail; if (detail?.changed_files) { setDirtyDeleteSession(session); setDirtyDeleteFiles(detail.changed_files); return; } } } finally { setLoadingSessionId(null); } }, [loadingSessionId, onRefresh], ); const handleForceDelete = useCallback( async (session: Session) => { if (loadingSessionId === session.id) return; setLoadingSessionId(session.id); try { await deleteInstance( session.project_id, session.repository_id, session.id, true, ); setDirtyDeleteSession(null); setDirtyDeleteFiles([]); await onRefresh(); } catch { // ignore } finally { setLoadingSessionId(null); } }, [loadingSessionId, onRefresh], ); const handleRecreateTunnel = useCallback( async (session: Session) => { if (loadingSessionId === session.id) return; setLoadingSessionId(session.id); try { await recreateInstanceTunnel( session.project_id, session.repository_id, session.id, ); await onRefresh(); } catch (err) { const message = (err as { response?: { data?: { detail?: string } } })?.response?.data ?.detail || "Failed to recreate tunnel"; alert(message); } finally { setLoadingSessionId(null); } }, [loadingSessionId, onRefresh], ); const handleRename = useCallback( async (session: Session, newName: string) => { if (!newName.trim()) return; setLoadingSessionId(session.id); try { await renameInstance( session.project_id, session.repository_id, session.id, newName.trim(), ); await onRefresh(); } catch { // ignore } finally { setLoadingSessionId(null); } }, [onRefresh], ); const clearDirtyDelete = useCallback(() => { setDirtyDeleteSession(null); setDirtyDeleteFiles([]); }, []); return { loadingSessionId, dirtyDeleteSession, dirtyDeleteFiles, handleOpen, handleStart, handleStop, handleDelete, handleForceDelete, handleRecreateTunnel, handleRename, clearDirtyDelete, }; }