import { useState } from "react"; import { AGENT_ADAPTER_TYPES } from "@paperclip/shared"; import type { Agent } from "@paperclip/shared"; import type { AdapterModel } from "../api/agents"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { FolderOpen, Heart, ChevronDown } from "lucide-react"; import { cn } from "../lib/utils"; import { Field, ToggleField, ToggleWithNumber, CollapsibleSection, AutoExpandTextarea, DraftInput, DraftTextarea, DraftNumberInput, HintIcon, help, adapterLabels, } from "./agent-config-primitives"; /* ---- Create mode values ---- */ export interface CreateConfigValues { adapterType: string; cwd: string; promptTemplate: string; model: string; dangerouslySkipPermissions: boolean; search: boolean; dangerouslyBypassSandbox: boolean; command: string; args: string; url: string; bootstrapPrompt: string; maxTurnsPerRun: number; heartbeatEnabled: boolean; intervalSec: number; } export const defaultCreateValues: CreateConfigValues = { adapterType: "claude_local", cwd: "", promptTemplate: "", model: "", dangerouslySkipPermissions: false, search: false, dangerouslyBypassSandbox: false, command: "", args: "", url: "", bootstrapPrompt: "", maxTurnsPerRun: 80, heartbeatEnabled: false, intervalSec: 300, }; /* ---- Props ---- */ type AgentConfigFormProps = { adapterModels?: AdapterModel[]; } & ( | { mode: "create"; values: CreateConfigValues; onChange: (patch: Partial) => void; } | { mode: "edit"; agent: Agent; onSave: (patch: Record) => void; } ); /* ---- Shared input class ---- */ const inputClass = "w-full rounded-md border border-border px-2.5 py-1.5 bg-transparent outline-none text-sm font-mono placeholder:text-muted-foreground/40"; /* ---- Form ---- */ export function AgentConfigForm(props: AgentConfigFormProps) { const { mode, adapterModels } = props; const isCreate = mode === "create"; // Resolve adapter type + config + heartbeat from props const adapterType = isCreate ? props.values.adapterType : props.agent.adapterType; const isLocal = adapterType === "claude_local" || adapterType === "codex_local"; // Edit mode: extract from agent const config = !isCreate ? ((props.agent.adapterConfig ?? {}) as Record) : {}; const runtimeConfig = !isCreate ? ((props.agent.runtimeConfig ?? {}) as Record) : {}; const heartbeat = !isCreate ? ((runtimeConfig.heartbeat ?? {}) as Record) : {}; // Section toggle state const [adapterAdvancedOpen, setAdapterAdvancedOpen] = useState(!isCreate); const [heartbeatOpen, setHeartbeatOpen] = useState(!isCreate); // Popover states const [modelOpen, setModelOpen] = useState(false); // Create mode helpers const val = isCreate ? props.values : null; const set = isCreate ? (patch: Partial) => props.onChange(patch) : null; // Edit mode helpers const saveConfig = !isCreate ? (field: string, value: unknown) => props.onSave({ adapterConfig: { ...config, [field]: value } }) : null; const saveHeartbeat = !isCreate ? (field: string, value: unknown) => props.onSave({ runtimeConfig: { ...runtimeConfig, heartbeat: { ...heartbeat, [field]: value } }, }) : null; const saveIdentity = !isCreate ? (field: string, value: unknown) => props.onSave({ [field]: value }) : null; // Current model for display const currentModelId = isCreate ? val!.model : String(config.model ?? ""); const selectedModel = (adapterModels ?? []).find((m) => m.id === currentModelId); return (
{/* ---- Identity (edit only) ---- */} {!isCreate && (
Identity
saveIdentity!("name", v)} className={inputClass} placeholder="Agent name" /> saveIdentity!("title", v || null)} className={inputClass} placeholder="e.g. VP of Engineering" /> saveIdentity!("capabilities", v || null)} placeholder="Describe what this agent can do..." minRows={2} />
)} {/* ---- Adapter type ---- */}
isCreate ? set!({ adapterType: t }) : props.onSave({ adapterType: t }) } />
{/* ---- Adapter Configuration ---- */}
Adapter Configuration
{/* Working directory */} {isLocal && (
isCreate ? set!({ cwd: v }) : saveConfig!("cwd", v || undefined) } immediate={isCreate} className="w-full bg-transparent outline-none text-sm font-mono placeholder:text-muted-foreground/40" placeholder="/path/to/project" />
)} {/* Prompt template */} {isLocal && ( {isCreate ? ( set!({ promptTemplate: v })} minRows={4} /> ) : ( saveConfig!("promptTemplate", v || undefined)} placeholder="You are agent {{ agent.name }}. Your role is {{ agent.role }}..." minRows={4} /> )} )} {/* Claude-specific: Skip permissions */} {adapterType === "claude_local" && ( isCreate ? set!({ dangerouslySkipPermissions: v }) : saveConfig!("dangerouslySkipPermissions", v) } /> )} {/* Codex-specific: Bypass sandbox + Search */} {adapterType === "codex_local" && ( <> isCreate ? set!({ dangerouslyBypassSandbox: v }) : saveConfig!("dangerouslyBypassApprovalsAndSandbox", v) } /> isCreate ? set!({ search: v }) : saveConfig!("search", v) } /> )} {/* Process-specific */} {adapterType === "process" && ( <> isCreate ? set!({ command: v }) : saveConfig!("command", v || undefined) } immediate={isCreate} className={inputClass} placeholder="e.g. node, python" /> isCreate ? set!({ args: v }) : saveConfig!( "args", v ? v .split(",") .map((a) => a.trim()) .filter(Boolean) : undefined, ) } immediate={isCreate} className={inputClass} placeholder="e.g. script.js, --flag" /> )} {/* HTTP-specific */} {adapterType === "http" && ( isCreate ? set!({ url: v }) : saveConfig!("url", v || undefined) } immediate={isCreate} className={inputClass} placeholder="https://..." /> )} {/* Advanced adapter section */} {isLocal && ( <> {isCreate ? ( setAdapterAdvancedOpen(!adapterAdvancedOpen)} >
set!({ model: v })} open={modelOpen} onOpenChange={setModelOpen} /> set!({ bootstrapPrompt: v })} minRows={2} /> {adapterType === "claude_local" && ( set!({ maxTurnsPerRun: Number(e.target.value) })} /> )}
) : ( /* Edit mode: show advanced fields inline (no collapse) */
Advanced
saveConfig!("model", v || undefined)} open={modelOpen} onOpenChange={setModelOpen} /> saveConfig!("bootstrapPromptTemplate", v || undefined)} placeholder="Optional initial setup prompt for the first run" minRows={2} /> {adapterType === "claude_local" && ( saveConfig!("maxTurnsPerRun", v || 80)} className={inputClass} /> )} saveConfig!("timeoutSec", v)} className={inputClass} /> saveConfig!("graceSec", v)} className={inputClass} />
)} )}
{/* ---- Heartbeat Policy ---- */} {isCreate ? ( } open={heartbeatOpen} onToggle={() => setHeartbeatOpen(!heartbeatOpen)} bordered >
set!({ heartbeatEnabled: v })} number={val!.intervalSec} onNumberChange={(v) => set!({ intervalSec: v })} numberLabel="sec" numberPrefix="Run heartbeat every" numberHint={help.intervalSec} showNumber={val!.heartbeatEnabled} />
) : (
Heartbeat Policy
saveHeartbeat!("enabled", v)} number={Number(heartbeat.intervalSec ?? 300)} onNumberChange={(v) => saveHeartbeat!("intervalSec", v)} numberLabel="sec" numberPrefix="Run heartbeat every" numberHint={help.intervalSec} showNumber={heartbeat.enabled !== false} /> {/* Edit-only: wake-on-* and cooldown */}
Advanced
saveHeartbeat!("wakeOnAssignment", v)} /> saveHeartbeat!("wakeOnOnDemand", v)} /> saveHeartbeat!("wakeOnAutomation", v)} /> saveHeartbeat!("cooldownSec", v)} className={inputClass} />
)} {/* ---- Runtime (edit only) ---- */} {!isCreate && (
Runtime
{props.agent.contextMode}
saveIdentity!("budgetMonthlyCents", v)} className={inputClass} />
)}
); } /* ---- Internal sub-components ---- */ function AdapterTypeDropdown({ value, onChange, }: { value: string; onChange: (type: string) => void; }) { return ( {AGENT_ADAPTER_TYPES.map((t) => ( ))} ); } function ModelDropdown({ models, value, onChange, open, onOpenChange, }: { models: AdapterModel[]; value: string; onChange: (id: string) => void; open: boolean; onOpenChange: (open: boolean) => void; }) { const selected = models.find((m) => m.id === value); return ( {models.map((m) => ( ))} ); }