import { useState, useRef, useEffect, useCallback } from "react"; import { Tooltip, TooltipTrigger, TooltipContent, } from "@/components/ui/tooltip"; import { HelpCircle, ChevronDown, ChevronRight } from "lucide-react"; import { cn } from "../lib/utils"; /* ---- Help text for (?) tooltips ---- */ export const help: Record = { name: "Display name for this agent.", title: "Job title shown in the org chart.", role: "Organizational role. Determines position and capabilities.", reportsTo: "The agent this one reports to in the org hierarchy.", capabilities: "Describes what this agent can do. Shown in the org chart and used for task routing.", adapterType: "How this agent runs: local CLI (Claude/Codex), spawned process, or HTTP webhook.", cwd: "The working directory where the agent operates. Use an absolute path on the machine running Paperclip.", promptTemplate: "The prompt sent to the agent on each heartbeat. Supports {{ agent.id }}, {{ agent.name }}, {{ agent.role }} variables.", model: "Override the default model used by the adapter.", thinkingEffort: "Control model reasoning depth. Supported values vary by adapter/model.", dangerouslySkipPermissions: "Run Claude without permission prompts. Required for unattended operation.", dangerouslyBypassSandbox: "Run Codex without sandbox restrictions. Required for filesystem/network access.", search: "Enable Codex web search capability during runs.", bootstrapPrompt: "Prompt used only on the first run (no existing session). Used for initial agent setup.", maxTurnsPerRun: "Maximum number of agentic turns (tool calls) per heartbeat run.", command: "The command to execute (e.g. node, python).", localCommand: "Override the local CLI command (e.g. claude, /usr/local/bin/claude, codex).", args: "Command-line arguments, comma-separated.", extraArgs: "Extra CLI arguments for local adapters, comma-separated.", envVars: "Environment variables injected into the adapter process. Use plain values or secret references.", webhookUrl: "The URL that receives POST requests when the agent is invoked.", heartbeatInterval: "Run this agent automatically on a timer. Useful for periodic tasks like checking for new work.", intervalSec: "Seconds between automatic heartbeat invocations.", timeoutSec: "Maximum seconds a run can take before being terminated. 0 means no timeout.", graceSec: "Seconds to wait after sending interrupt before force-killing the process.", wakeOnDemand: "Allow this agent to be woken by assignments, API calls, UI actions, or automated systems.", cooldownSec: "Minimum seconds between consecutive heartbeat runs.", maxConcurrentRuns: "Maximum number of heartbeat runs that can execute simultaneously for this agent.", budgetMonthlyCents: "Monthly spending limit in cents. 0 means no limit.", }; export const adapterLabels: Record = { claude_local: "Claude (local)", codex_local: "Codex (local)", process: "Process", http: "HTTP", }; export const roleLabels: Record = { ceo: "CEO", cto: "CTO", cmo: "CMO", cfo: "CFO", engineer: "Engineer", designer: "Designer", pm: "PM", qa: "QA", devops: "DevOps", researcher: "Researcher", general: "General", }; /* ---- Primitive components ---- */ export function HintIcon({ text }: { text: string }) { return ( {text} ); } export function Field({ label, hint, children }: { label: string; hint?: string; children: React.ReactNode }) { return (
{hint && }
{children}
); } export function ToggleField({ label, hint, checked, onChange, }: { label: string; hint?: string; checked: boolean; onChange: (v: boolean) => void; }) { return (
{label} {hint && }
); } export function ToggleWithNumber({ label, hint, checked, onCheckedChange, number, onNumberChange, numberLabel, numberHint, numberPrefix, showNumber, }: { label: string; hint?: string; checked: boolean; onCheckedChange: (v: boolean) => void; number: number; onNumberChange: (v: number) => void; numberLabel: string; numberHint?: string; numberPrefix?: string; showNumber: boolean; }) { return (
{label} {hint && }
{showNumber && (
{numberPrefix && {numberPrefix}} onNumberChange(Number(e.target.value))} /> {numberLabel} {numberHint && }
)}
); } export function CollapsibleSection({ title, icon, open, onToggle, bordered, children, }: { title: string; icon?: React.ReactNode; open: boolean; onToggle: () => void; bordered?: boolean; children: React.ReactNode; }) { return (
{open &&
{children}
}
); } export function AutoExpandTextarea({ value, onChange, onBlur, placeholder, minRows, }: { value: string; onChange: (v: string) => void; onBlur?: () => void; placeholder?: string; minRows?: number; }) { const textareaRef = useRef(null); const rows = minRows ?? 3; const lineHeight = 20; const minHeight = rows * lineHeight; const adjustHeight = useCallback(() => { const el = textareaRef.current; if (!el) return; el.style.height = "auto"; el.style.height = `${Math.max(minHeight, el.scrollHeight)}px`; }, [minHeight]); useEffect(() => { adjustHeight(); }, [value, adjustHeight]); return (