import { useState } from "react" import { Plus, X } from "lucide-react" const TEMPLATES: Record> = { service: { run: { runner: "python", program: "", cwd: "", env: {}, }, expose: { http: { internal: { port: 9001 }, health_path: "/health", }, }, proxy: { caddy: { path_prefix: "/" }, }, manage: { systemd: {}, }, }, tool: { behavior: "tool", }, job: { run: { runner: "command", argv: [""], cwd: "", }, schedule: "0 2 * * *", }, empty: {}, } interface AddComponentProps { onAdd: (name: string, config: Record) => Promise existingNames: string[] } export function AddComponent({ onAdd, existingNames }: AddComponentProps) { const [open, setOpen] = useState(false) const [name, setName] = useState("") const [description, setDescription] = useState("") const [template, setTemplate] = useState("service") const [port, setPort] = useState("") const [saving, setSaving] = useState(false) const [error, setError] = useState("") const nameError = name && !/^[a-z0-9][a-z0-9-]*$/.test(name) ? "lowercase letters, numbers, and hyphens" : existingNames.includes(name) ? "already exists" : "" const handleSubmit = async () => { if (!name || nameError) return setSaving(true) setError("") try { const config: Record = JSON.parse( JSON.stringify(TEMPLATES[template] ?? {}) ) if (description) config.description = description // Fill in template-specific fields if (template === "service") { const run = config.run as Record run.program = name run.cwd = name const proxy = (config.proxy as Record>).caddy proxy.path_prefix = `/${name}` if (port) { const expose = (config.expose as Record>>) expose.http.internal.port = parseInt(port, 10) } } else if (template === "tool") { // tool template has behavior preset, no extra config needed } await onAdd(name, config) setName("") setDescription("") setPort("") setOpen(false) } catch (e: unknown) { setError(e instanceof Error ? e.message : String(e)) } finally { setSaving(false) } } if (!open) { return ( ) } return (

New component

{error && (
{error}
)}
setName(e.target.value.toLowerCase())} placeholder="my-service" autoFocus className="w-full bg-black/30 border border-[var(--border)] rounded px-3 py-1.5 text-sm font-mono focus:outline-none focus:border-[var(--primary)]" /> {nameError &&

{nameError}

}
setDescription(e.target.value)} placeholder="What does this component do?" className="w-full bg-black/30 border border-[var(--border)] rounded px-3 py-1.5 text-sm focus:outline-none focus:border-[var(--primary)]" /> {template === "service" && ( setPort(e.target.value)} placeholder="9001" className="w-32 bg-black/30 border border-[var(--border)] rounded px-3 py-1.5 text-sm font-mono focus:outline-none focus:border-[var(--primary)]" /> )}
) } function Field({ label, children }: { label: string; children: React.ReactNode }) { return (
{children}
) }