import { createContext, useCallback, useContext, useMemo, useState, type ReactNode, } from "react"; import type { InstanceEventPayload } from "../types/events"; export type OperationType = | "create" | "start" | "stop" | "restart" | "delete" | "recreate-tunnel"; export type OperationStatus = "pending" | "active" | "success" | "error"; export interface Operation { id: string; type: OperationType; instanceId: string; displayName: string; status: OperationStatus; message: string; step: number; createdAt: number; } export interface SessionOperationsContextType { operations: Operation[]; startOperation: ( type: OperationType, instanceId: string, displayName: string, ) => string; updateOperationFromEvent: (event: InstanceEventPayload) => void; completeOperation: ( instanceId: string, type: OperationType, outcome: "success" | "error", message?: string, ) => void; dismissOperation: (id: string) => void; } const SessionOperationsContext = createContext(undefined); let operationIdCounter = 0; function actionLabel(type: OperationType): string { switch (type) { case "create": return "Creating"; case "start": return "Starting"; case "stop": return "Stopping"; case "restart": return "Restarting"; case "delete": return "Deleting"; case "recreate-tunnel": return "Recreating tunnel"; default: return "Working"; } } function messageForEvent( type: OperationType, event: InstanceEventPayload, ): string { if (event.message) return event.message; switch (event.event) { case "instance.created": return "Created"; case "instance.started": return "Starting container"; case "instance.restarted": return "Restarting container"; case "instance.stopped": return "Stopped"; case "instance.deleted": return "Deleted"; case "instance.health_changed": if (event.status === "running") return "Running"; if (event.status === "unhealthy") return "Unhealthy"; return `Status: ${event.status ?? event.event}`; case "instance.error": return event.message ?? "Error"; default: return event.message ?? actionLabel(type); } } function stepForEvent(event: InstanceEventPayload): number { switch (event.event) { case "instance.created": return 1; case "instance.started": case "instance.restarted": return 2; case "instance.health_changed": if (event.status === "running") return 4; if (event.status === "unhealthy") return 4; return 3; case "instance.error": return 4; case "instance.stopped": return 4; case "instance.deleted": return 4; default: return 0; } } export const SessionOperationsProvider = ({ children, }: { children: ReactNode; }) => { const [operations, setOperations] = useState([]); const startOperation = useCallback( (type: OperationType, instanceId: string, displayName: string): string => { const id = `op-${++operationIdCounter}`; const operation: Operation = { id, type, instanceId, displayName, status: "pending", message: actionLabel(type), step: 0, createdAt: Date.now(), }; setOperations((prev) => [operation, ...prev].slice(0, 20)); return id; }, [], ); const updateOperationFromEvent = useCallback( (event: InstanceEventPayload) => { setOperations((prev) => { const matches = prev.filter( (op) => op.instanceId === event.instance_id && op.status !== "success", ); if (matches.length === 0) return prev; const updated = new Map(); for (const op of prev) updated.set(op.id, op); for (const op of matches) { const nextStep = stepForEvent(event); const message = messageForEvent(op.type, event); let nextStatus: OperationStatus = op.status; if (event.event === "instance.error") { nextStatus = "error"; } else if ( event.event === "instance.health_changed" && event.status === "running" ) { nextStatus = "success"; } else if (event.event === "instance.deleted") { nextStatus = "success"; } else if (event.event === "instance.stopped") { nextStatus = "success"; } else if (nextStatus === "pending") { nextStatus = "active"; } updated.set(op.id, { ...op, status: nextStatus, message, step: Math.max(op.step, nextStep), }); } return Array.from(updated.values()); }); }, [], ); const completeOperation = useCallback( ( instanceId: string, type: OperationType, outcome: "success" | "error", message?: string, ) => { setOperations((prev) => { const match = prev.find( (op) => op.instanceId === instanceId && op.type === type, ); if (!match) return prev; return prev.map((op) => op.id === match.id ? { ...op, status: outcome, message: message ?? (outcome === "success" ? "Done" : "Failed"), step: 4, } : op, ); }); }, [], ); const dismissOperation = useCallback((id: string) => { setOperations((prev) => prev.filter((op) => op.id !== id)); }, []); const value = useMemo( () => ({ operations, startOperation, updateOperationFromEvent, completeOperation, dismissOperation, }), [ operations, startOperation, updateOperationFromEvent, completeOperation, dismissOperation, ], ); return ( {children} ); }; export const useSessionOperations = (): SessionOperationsContextType => { const context = useContext(SessionOperationsContext); if (context === undefined) { throw new Error( "useSessionOperations must be used within a SessionOperationsProvider", ); } return context; };