import { useCallback, useEffect, useState } from "react"; import { ErrorState, LoadingState } from "../components/data-states"; import { Icon } from "../components/icon"; import { useMobileViewport } from "../hooks/use-mobile-viewport"; import { extractErrorMessage } from "../utils/errors"; import { MobileListView } from "../components/mobile-list-view"; import { MobileDetailView } from "../components/mobile-detail-view"; import { MobileEditView } from "../components/mobile-edit-view"; import { MobileFAB } from "../components/mobile-fab"; import { createToolType, deleteToolType, listToolTypes, updateToolType, type CreateToolTypeRequest, type ReadinessProbe, type ToolType, type UpdateToolTypeRequest, } from "../api/tool_types"; import { createToolDefinition, getToolDefinition, listToolDefinitions, updateToolDefinition, type ToolDefinitionManifest, } from "../api/tool_definitions"; import { ManifestEditor } from "../components/manifest-editor"; type Status = "loading" | "ready" | "error"; type MobileView = "list" | "detail" | "edit"; export const ToolWorkshopPage = () => { const isMobile = useMobileViewport(); const [mobileView, setMobileView] = useState("list"); const [status, setStatus] = useState("loading"); const [toolTypes, setToolTypes] = useState([]); const [baseDefinitions, setBaseDefinitions] = useState< ToolDefinitionManifest[] >([]); const [manifestData, setManifestData] = useState | null>(null); const [manifestDefinitionId, setManifestDefinitionId] = useState< string | null >(null); // Selection state const [selectedToolTypeId, setSelectedToolTypeId] = useState( null, ); const [isCreating, setIsCreating] = useState(false); // Tool Type form state const [toolTypeForm, setToolTypeForm] = useState({ name: "", display_name: "", description: "", category: "", interface_type: "web" as "web" | "terminal", requires_port: true, default_port: "", definition_type: "compose" as "compose" | "dockerfile" | "manifest", compose_template: "", dockerfile_template: "", readiness_command: "", readiness_timeout: "30", readiness_interval: "2", required_variables: "", startup_command: "", }); const [toolTypeError, setToolTypeError] = useState(null); const [toolTypeDirty, setToolTypeDirty] = useState(false); const selectedToolType = (toolTypes || []).find((t) => t.id === selectedToolTypeId) || null; const loadData = useCallback(async () => { setStatus("loading"); try { const [types, defs] = await Promise.all([ listToolTypes(), listToolDefinitions(), ]); setToolTypes(types || []); setBaseDefinitions((defs || []).filter((d) => d.is_base)); setStatus("ready"); } catch { setStatus("error"); } }, []); useEffect(() => { void loadData(); }, [loadData]); const resetToolTypeForm = () => { setToolTypeForm({ name: "", display_name: "", description: "", category: "", interface_type: "web", requires_port: true, default_port: "", definition_type: "compose", compose_template: "", dockerfile_template: "", readiness_command: "", readiness_timeout: "30", readiness_interval: "2", required_variables: "", startup_command: "", }); setToolTypeError(null); setToolTypeDirty(false); setManifestData(null); setManifestDefinitionId(null); }; const populateToolTypeForm = async (toolType: ToolType) => { setToolTypeForm({ name: toolType.name, display_name: toolType.display_name, description: toolType.description || "", category: toolType.category || "", interface_type: (toolType.interface_type as "web" | "terminal") || "web", requires_port: toolType.requires_port ?? true, default_port: toolType.default_port?.toString() || "", definition_type: (toolType.definition_type as "compose" | "dockerfile" | "manifest") || "compose", compose_template: toolType.compose_template || "", dockerfile_template: toolType.dockerfile_template || "", readiness_command: toolType.readiness_probe?.command || "", readiness_timeout: toolType.readiness_probe?.timeout?.toString() || "30", readiness_interval: toolType.readiness_probe?.interval?.toString() || "2", required_variables: toolType.required_variables?.join(", ") || "", startup_command: toolType.startup_command || "", }); setToolTypeError(null); setToolTypeDirty(false); setManifestDefinitionId(toolType.manifest_id || null); if (toolType.definition_type === "manifest" && toolType.manifest_id) { try { const defn = await getToolDefinition(toolType.manifest_id); setManifestData(defn.manifest); } catch { setManifestData(null); } } else { setManifestData(null); } }; const handleSelectToolType = (toolType: ToolType | null) => { if (toolTypeDirty) { if (!window.confirm("You have unsaved changes. Discard them?")) { return; } } if (toolType) { setSelectedToolTypeId(toolType.id); setIsCreating(false); populateToolTypeForm(toolType); } else { setSelectedToolTypeId(null); } }; const handleCreateNew = () => { if (toolTypeDirty) { if (!window.confirm("You have unsaved changes. Discard them?")) { return; } } setSelectedToolTypeId(null); setIsCreating(true); resetToolTypeForm(); }; const handleToolTypeSubmit = async (e: React.FormEvent) => { e.preventDefault(); setToolTypeError(null); if (!toolTypeForm.name.trim() || !toolTypeForm.display_name.trim()) { setToolTypeError("Name and display name are required"); return; } if ( toolTypeForm.requires_port && (!toolTypeForm.default_port.trim() || isNaN(Number(toolTypeForm.default_port))) ) { setToolTypeError("Default port is required and must be a number"); return; } if (toolTypeForm.definition_type !== "manifest") { const template = toolTypeForm.definition_type === "compose" ? toolTypeForm.compose_template : toolTypeForm.dockerfile_template; if (!template.trim()) { setToolTypeError( `${toolTypeForm.definition_type === "compose" ? "Compose" : "Dockerfile"} template is required`, ); return; } } else if (!manifestData) { setToolTypeError( "Manifest data is required for manifest definition type", ); return; } const variables = toolTypeForm.required_variables .split(",") .map((v) => v.trim()) .filter((v) => v.length > 0); const readinessProbe: ReadinessProbe | undefined = toolTypeForm.readiness_command.trim() ? { command: toolTypeForm.readiness_command.trim(), timeout: parseInt(toolTypeForm.readiness_timeout) || 30, interval: parseInt(toolTypeForm.readiness_interval) || 2, } : undefined; const template = toolTypeForm.definition_type === "compose" ? toolTypeForm.compose_template : toolTypeForm.dockerfile_template; try { if (isCreating) { let manifestId: string | undefined; if (toolTypeForm.definition_type === "manifest" && manifestData) { const manifestPayload = { name: toolTypeForm.name.trim(), display_name: toolTypeForm.display_name.trim(), description: toolTypeForm.description.trim() || undefined, category: toolTypeForm.category.trim() || undefined, interface_type: toolTypeForm.interface_type, base_image: (manifestData.base_image as string) || undefined, base_definition_id: (manifestData.base_definition_id as string) || undefined, manifest: manifestData, }; const newManifest = await createToolDefinition(manifestPayload); manifestId = newManifest.id; } const input: CreateToolTypeRequest = { name: toolTypeForm.name.trim(), display_name: toolTypeForm.display_name.trim(), description: toolTypeForm.description.trim() || undefined, category: toolTypeForm.category.trim() || undefined, interface_type: toolTypeForm.interface_type, requires_port: toolTypeForm.requires_port, default_port: toolTypeForm.requires_port ? Number(toolTypeForm.default_port) : 0, definition_type: toolTypeForm.definition_type, manifest_id: manifestId, compose_template: toolTypeForm.definition_type === "compose" ? template : undefined, dockerfile_template: toolTypeForm.definition_type === "dockerfile" ? template : undefined, readiness_probe: readinessProbe, required_variables: variables, startup_command: toolTypeForm.startup_command.trim() || undefined, }; const newTool = await createToolType(input); setIsCreating(false); setSelectedToolTypeId(newTool.id); setToolTypeDirty(false); } else if (selectedToolType) { let manifestId = selectedToolType.manifest_id || undefined; if (toolTypeForm.definition_type === "manifest" && manifestData) { if (manifestId) { await updateToolDefinition(manifestId, { display_name: toolTypeForm.display_name.trim(), description: toolTypeForm.description.trim() || undefined, category: toolTypeForm.category.trim() || undefined, manifest: manifestData, }); } else { const manifestPayload = { name: toolTypeForm.name.trim(), display_name: toolTypeForm.display_name.trim(), description: toolTypeForm.description.trim() || undefined, category: toolTypeForm.category.trim() || undefined, interface_type: toolTypeForm.interface_type, base_image: (manifestData.base_image as string) || undefined, base_definition_id: (manifestData.base_definition_id as string) || undefined, manifest: manifestData, }; const newManifest = await createToolDefinition(manifestPayload); manifestId = newManifest.id; } } const input: UpdateToolTypeRequest = { display_name: toolTypeForm.display_name.trim(), description: toolTypeForm.description.trim() || undefined, category: toolTypeForm.category.trim() || undefined, interface_type: toolTypeForm.interface_type, requires_port: toolTypeForm.requires_port, default_port: toolTypeForm.requires_port ? Number(toolTypeForm.default_port) : 0, definition_type: toolTypeForm.definition_type, manifest_id: toolTypeForm.definition_type === "manifest" ? manifestId : undefined, compose_template: toolTypeForm.definition_type === "compose" ? template : undefined, dockerfile_template: toolTypeForm.definition_type === "dockerfile" ? template : undefined, readiness_probe: readinessProbe, required_variables: variables, startup_command: toolTypeForm.startup_command.trim() || undefined, }; await updateToolType(selectedToolType.id, input); setToolTypeDirty(false); } await loadData(); } catch (err) { setToolTypeError(extractErrorMessage(err)); } }; const handleDeleteToolType = async (id: string) => { if ( !window.confirm( "Delete this tool type? All associated configs will be removed.", ) ) return; try { await deleteToolType(id); if (selectedToolTypeId === id) { setSelectedToolTypeId(null); setIsCreating(false); resetToolTypeForm(); } await loadData(); } catch { alert("Failed to delete tool type"); } }; if (status === "loading") { return (
); } if (status === "error") { return (
); } if (isMobile) { if (mobileView === "list") { return (

Tool Workshop

{toolTypes.length} tool types
({ id: t.id, title: t.display_name, subtitle: `${t.category || "Uncategorized"} · ${t.interface_type === "web" ? `Port ${t.default_port}` : "Terminal"}`, }))} onItemClick={(id) => { setSelectedToolTypeId(id); setIsCreating(false); setMobileView("detail"); }} emptyMessage="No tool types yet" /> { setSelectedToolTypeId(null); setIsCreating(true); resetToolTypeForm(); setMobileView("edit"); }} />
); } if (mobileView === "detail" && selectedToolType) { return ( { populateToolTypeForm(selectedToolType); setIsCreating(false); setMobileView("edit"); }} onDelete={() => { handleDeleteToolType(selectedToolType.id); setMobileView("list"); }} onBack={() => { setSelectedToolTypeId(null); setMobileView("list"); }} /> ); } if (mobileView === "edit") { return ( { if (toolTypeDirty) { if (!window.confirm("You have unsaved changes. Discard them?")) { return; } } setMobileView(isCreating ? "list" : "detail"); }} onSave={() => { // Create a synthetic form event to call handleToolTypeSubmit const syntheticEvent = { preventDefault: () => {}, } as React.FormEvent; void handleToolTypeSubmit(syntheticEvent); if (!toolTypeError) { setMobileView("list"); } }} isSaving={false} > {/* Tool Type Form Fields */}
setToolTypeForm({ ...toolTypeForm, name: e.target.value }) } className="mobile-form-input" placeholder="e.g., my-tool" />
setToolTypeForm({ ...toolTypeForm, display_name: e.target.value, }) } className="mobile-form-input" placeholder="e.g., My Tool" />