"use client"; import { useState, useEffect, useCallback, useRef } from "react"; import { api } from "@/lib/api"; import { checkDeploySecrets, type PreflightResult } from "@/lib/deploy-preflight"; import { MissingKeysModal } from "./MissingKeysModal"; interface Template { id: string; name: string; description: string; tier: number; model: string; skills: string[]; skill_count: number; } export interface OrgTemplate { dir: string; name: string; description: string; workspaces: number; } /** Fetch the list of org templates from the platform. Returns [] on error * so the UI shows the empty state instead of crashing. */ export async function fetchOrgTemplates(): Promise { try { return await api.get("/org/templates"); } catch { return []; } } /** Import an org template by directory name. Throws on platform error so the * caller can surface the message in its error state. */ export async function importOrgTemplate(dir: string): Promise { await api.post("/org/import", { dir }); } /** * Section listing org templates (multi-workspace hierarchies). Click "Import" * to instantiate the entire tree via `POST /org/import { dir }`. PLAN.md §20.3. * * Exported separately so the org import flow has a focused unit-test surface * without re-rendering the full palette. */ export function OrgTemplatesSection() { const [orgs, setOrgs] = useState([]); const [loading, setLoading] = useState(false); const [importing, setImporting] = useState(null); const [error, setError] = useState(null); const loadOrgs = useCallback(async () => { setLoading(true); setOrgs(await fetchOrgTemplates()); setLoading(false); }, []); useEffect(() => { loadOrgs(); }, [loadOrgs]); const handleImport = async (org: OrgTemplate) => { setImporting(org.dir); setError(null); try { await importOrgTemplate(org.dir); } catch (e) { setError(e instanceof Error ? e.message : "Import failed"); } finally { setImporting(null); } }; return (

Org Templates

{loading &&
Loading…
} {!loading && orgs.length === 0 && (
No org templates in org-templates/
)} {error && (
{error}
)} {orgs.map((o) => { const isImporting = importing === o.dir; return (
{o.name || o.dir} {o.workspaces}w
{o.description && (

{o.description}

)}
); })}
); } const TIER_LABELS: Record = { 1: { label: "T1", color: "text-zinc-400 bg-zinc-800/60" }, 2: { label: "T2", color: "text-sky-400 bg-sky-950/40" }, 3: { label: "T3", color: "text-violet-400 bg-violet-950/40" }, 4: { label: "T4", color: "text-amber-400 bg-amber-950/40" }, }; function ImportAgentButton({ onImported }: { onImported: () => void }) { const [importing, setImporting] = useState(false); const fileInputRef = useRef(null); const handleFiles = async (fileList: FileList) => { setImporting(true); try { const files: Record = {}; let agentName = ""; for (const file of Array.from(fileList)) { // webkitRelativePath gives us "folder/file.md" const path = file.webkitRelativePath || file.name; // Strip the top-level folder name const parts = path.split("/"); if (!agentName && parts.length > 1) { agentName = parts[0]; } const relPath = parts.length > 1 ? parts.slice(1).join("/") : parts[0]; // Only import text files if (file.size > 1_000_000) continue; // skip files > 1MB try { const content = await file.text(); files[relPath] = content; } catch { // Skip binary files } } if (Object.keys(files).length === 0) { alert("No files found in the selected folder"); return; } const name = agentName || "Imported Agent"; await api.post("/templates/import", { name, files }); onImported(); } catch (e) { alert(e instanceof Error ? e.message : "Import failed"); } finally { setImporting(false); } }; return (
e.target.files && handleFiles(e.target.files)} />
); } export function TemplatePalette() { const [open, setOpen] = useState(false); const [templates, setTemplates] = useState([]); const [loading, setLoading] = useState(false); const [creating, setCreating] = useState(null); const [error, setError] = useState(null); // Missing keys modal state const [missingKeysInfo, setMissingKeysInfo] = useState<{ template: Template; preflight: PreflightResult; } | null>(null); const loadTemplates = useCallback(async () => { setLoading(true); try { const data = await api.get("/templates"); setTemplates(data); } catch { setTemplates([]); } finally { setLoading(false); } }, []); useEffect(() => { if (open) loadTemplates(); }, [open, loadTemplates]); /** Resolve runtime from template ID (e.g., "langgraph", "claude-code-default" → "claude-code") */ const resolveRuntime = (templateId: string): string => { const runtimeMap: Record = { langgraph: "langgraph", "claude-code-default": "claude-code", openclaw: "openclaw", deepagents: "deepagents", crewai: "crewai", autogen: "autogen", }; return runtimeMap[templateId] ?? templateId.replace(/-default$/, ""); }; /** Actually execute the deploy API call */ const executeDeploy = useCallback(async (template: Template) => { setCreating(template.id); setError(null); try { await api.post("/workspaces", { name: template.name, template: template.id, tier: template.tier, canvas: { x: Math.random() * 400 + 100, y: Math.random() * 300 + 100, }, }); setCreating(null); } catch (e) { setError(e instanceof Error ? e.message : "Failed to deploy"); setCreating(null); } }, []); /** Pre-deploy check: validate secrets before deploying */ const handleDeploy = async (template: Template) => { setCreating(template.id); setError(null); const runtime = resolveRuntime(template.id); const preflight = await checkDeploySecrets(runtime); if (!preflight.ok) { // Missing keys — show the modal instead of deploying setMissingKeysInfo({ template, preflight }); setCreating(null); return; } // All keys present — deploy directly await executeDeploy(template); }; return ( <> {/* Toggle button */} {/* Missing Keys Modal */} { if (missingKeysInfo) { const template = missingKeysInfo.template; setMissingKeysInfo(null); executeDeploy(template); } }} onCancel={() => setMissingKeysInfo(null)} /> {/* Sidebar */} {open && (

Templates

Click to deploy a workspace

{loading && (
Loading...
)} {!loading && templates.length === 0 && (
No templates found in
workspace-configs-templates/
)} {error && (
{error}
)} {templates.map((t) => { const tierCfg = TIER_LABELS[t.tier] || TIER_LABELS[1]; const isDeploying = creating === t.id; return ( ); })}
)} ); }