import { useState, useEffect, useCallback, useRef, useMemo } from "react"; import { Bot, Heart, Activity, Pause, Play, Square, Trash2, RefreshCw, Settings, FileText, ActivitySquare, X, Copy, ExternalLink, CheckCircle, XCircle, Loader2, GitBranch, ListChecks, ChevronDown, ChevronRight, BarChart3, Star } from "lucide-react"; import type { AgentDetail, AgentState, AgentHeartbeatRun, AgentBudgetStatus } from "../api"; import { fetchAgent, updateAgent, updateAgentState, deleteAgent, fetchAgentLogs, fetchAgentRunLogs, fetchAgentChildren, fetchAgentRuns, fetchAgentRunDetail, startAgentRun, stopAgentRun, updateAgentInstructions, updateAgentSoul, updateAgentMemory, fetchAgentTasks, fetchChainOfCommand, fetchAgentBudgetStatus, resetAgentBudget } from "../api"; import type { Agent } from "../api"; import type { AgentLogEntry, Task } from "@fusion/core"; import { AgentLogViewer } from "./AgentLogViewer"; import { AgentReflectionsTab } from "./AgentReflectionsTab"; import { getAgentHealthStatus } from "../utils/agentHealth"; /** * Simple className utility - joins class names conditionally */ function cn(...classes: (string | boolean | undefined | null)[]): string { return classes.filter(Boolean).join(" "); } /** * Format an ISO timestamp to a relative time string. */ function relativeTime(iso: string): string { const now = Date.now(); const then = new Date(iso).getTime(); const diffMs = now - then; // Future if (diffMs < 0) { const absDiff = Math.abs(diffMs); if (absDiff < 60_000) return "in a moment"; if (absDiff < 3_600_000) return `in ${Math.floor(absDiff / 60_000)}m`; if (absDiff < 86_400_000) return `in ${Math.floor(absDiff / 3_600_000)}h`; return `in ${Math.floor(absDiff / 86_400_000)}d`; } // Past if (diffMs < 60_000) return "just now"; if (diffMs < 3_600_000) return `${Math.floor(diffMs / 60_000)}m ago`; if (diffMs < 86_400_000) return `${Math.floor(diffMs / 3_600_000)}h ago`; return `${Math.floor(diffMs / 86_400_000)}d ago`; } interface AgentDetailViewProps { agentId: string; projectId?: string; onClose: () => void; addToast: (message: string, type?: "success" | "error") => void; onChildClick?: (childId: string) => void; } type TabId = "dashboard" | "logs" | "config" | "runs" | "tasks" | "employees" | "soul" | "memory" | "reflections" | "performance"; const TABS: { id: TabId; label: string; icon: typeof Activity }[] = [ { id: "dashboard", label: "Dashboard", icon: ActivitySquare }, { id: "logs", label: "Logs", icon: FileText }, { id: "runs", label: "Runs", icon: Activity }, { id: "tasks", label: "Tasks", icon: ListChecks }, { id: "employees", label: "Employees", icon: GitBranch }, { id: "soul", label: "Soul", icon: Heart }, { id: "memory", label: "Memory", icon: FileText }, { id: "reflections", label: "Reflections", icon: BarChart3 }, { id: "performance", label: "Performance", icon: Star }, { id: "config", label: "Settings", icon: Settings }, ]; const STATE_COLORS: Record = { idle: { bg: "var(--state-idle-bg)", text: "var(--state-idle-text)", border: "var(--state-idle-border)" }, active: { bg: "var(--state-active-bg)", text: "var(--state-active-text)", border: "var(--state-active-border)" }, running: { bg: "var(--state-active-bg)", text: "var(--state-active-text)", border: "var(--state-active-border)" }, paused: { bg: "var(--state-paused-bg)", text: "var(--state-paused-text)", border: "var(--state-paused-border)" }, error: { bg: "var(--state-error-bg)", text: "var(--state-error-text)", border: "var(--state-error-border)" }, terminated: { bg: "var(--state-error-bg)", text: "var(--state-error-text)", border: "var(--state-error-border)" }, }; const RUN_STATUS_ICONS: Record = { completed: { icon: CheckCircle, color: "var(--color-success, #3fb950)" }, failed: { icon: XCircle, color: "var(--color-error, #f85149)" }, active: { icon: Loader2, color: "var(--in-progress, #bc8cff)" }, terminated: { icon: Square, color: "var(--text-muted, #8b949e)" }, }; export function AgentDetailView({ agentId, projectId, onClose, addToast, onChildClick }: AgentDetailViewProps) { const [agent, setAgent] = useState(null); const [logs, setLogs] = useState([]); const [isLoading, setIsLoading] = useState(true); const [activeTab, setActiveTab] = useState("dashboard"); const [isStreaming, setIsStreaming] = useState(false); const logContainerRef = useRef(null); const onCloseRef = useRef(onClose); const addToastRef = useRef(addToast); const agentRef = useRef(null); onCloseRef.current = onClose; addToastRef.current = addToast; agentRef.current = agent; const loadAgent = useCallback(async () => { const showLoadingSpinner = agentRef.current === null; if (showLoadingSpinner) { setIsLoading(true); } try { const data = await fetchAgent(agentId, projectId); setAgent(data); } catch (err: any) { addToastRef.current(`Failed to load agent: ${err.message}`, "error"); onCloseRef.current(); } finally { setIsLoading(false); } }, [agentId, projectId]); const loadLogs = useCallback(async () => { // Agent logs are tied to tasks, not agents directly. // If the agent has a current task, we could show those logs. // For now, we'll show heartbeat runs as the "activity" for the agent. // If the agent is working on a task, we could show task logs. if (agent?.taskId) { try { const data = await fetchAgentLogs(agent.taskId, projectId); setLogs(data); } catch (err: any) { console.error("Failed to load task logs:", err); } } }, [agent?.taskId, projectId]); useEffect(() => { void loadAgent(); }, [loadAgent]); // Poll for agent updates to keep health status fresh (every 30 seconds) // This ensures health badges stay current while the detail view is open useEffect(() => { const pollInterval = setInterval(() => { void loadAgent(); }, 30_000); return () => { clearInterval(pollInterval); }; }, [loadAgent]); useEffect(() => { if (agent?.taskId) { void loadLogs(); } }, [agent?.taskId, loadLogs]); // Set up SSE for live log streaming when viewing logs tab with a task useEffect(() => { if (activeTab !== "logs" || !agent?.taskId) { setIsStreaming(false); return; } const query = projectId ? `?projectId=${encodeURIComponent(projectId)}` : ""; const es = new EventSource(`/api/tasks/${encodeURIComponent(agent.taskId)}/logs/stream${query}`); const handleAgentLog = (e: MessageEvent) => { try { const entry: AgentLogEntry = JSON.parse(e.data); setLogs(prev => [entry, ...prev]); // Auto-scroll to top for new entries const container = logContainerRef.current; if (container && container.scrollTop < 50) { container.scrollTop = 0; } } catch { // Ignore parse errors } }; es.addEventListener("agent:log", handleAgentLog as EventListener); es.onerror = () => { setIsStreaming(false); }; es.onopen = () => { setIsStreaming(true); }; return () => { es.removeEventListener("agent:log", handleAgentLog as EventListener); es.close(); setIsStreaming(false); }; }, [agent?.taskId, activeTab, projectId]); const handleStateChange = async (newState: AgentState) => { try { await updateAgentState(agentId, newState, projectId); addToast(`Agent state updated to ${newState}`, "success"); void loadAgent(); } catch (err: any) { addToast(`Failed to update state: ${err.message}`, "error"); } }; const handleDelete = async () => { if (!agent || !confirm(`Delete agent "${agent.name}"? This cannot be undone.`)) return; try { await deleteAgent(agentId, projectId); addToast(`Agent "${agent.name}" deleted`, "success"); onClose(); } catch (err: any) { addToast(`Failed to delete agent: ${err.message}`, "error"); } }; // Use centralized health status utility for consistent labels across all views const getHealthStatus = () => { if (!agent) return { label: "Unknown", color: "var(--text-muted, #8b949e)" }; return getAgentHealthStatus(agent); }; const copyAgentId = () => { if (agent) { navigator.clipboard.writeText(agent.id); addToast("Agent ID copied to clipboard", "success"); } }; if (isLoading) { return (
e.target === e.currentTarget && onClose()}>
Loading agent...
); } if (!agent) { return null; } const stateStyle = STATE_COLORS[agent.state]; const health = getHealthStatus(); return (
e.target === e.currentTarget && onClose()}>
{/* Header */}

{agent.name}

{agent.state} {health.icon} {health.label}
{/* State-dependent action buttons */} {agent.state === "idle" && ( <> )} {agent.state === "active" && ( <> )} {agent.state === "paused" && ( <> )} {agent.state === "running" && ( <> )} {agent.state === "error" && ( <> )} {agent.state === "terminated" && ( <> )}
{/* Tabs */}
{TABS.map(tab => ( ))}
{/* Tab Content */}
{activeTab === "dashboard" && ( )} {activeTab === "logs" && ( )} {activeTab === "runs" && ( )} {activeTab === "tasks" && ( )} {activeTab === "employees" && ( )} {activeTab === "soul" && ( )} {activeTab === "memory" && ( )} {activeTab === "reflections" && ( )} {activeTab === "performance" && ( )} {activeTab === "config" && ( )}
{/* Footer with agent ID */}
{agent.id} {agent.taskId && ( <> | Working on: {agent.taskId} )}
); } // ── Dashboard Tab ─────────────────────────────────────────────────────────── function DashboardTab({ agent, health, onChildClick, projectId, }: { agent: AgentDetail; health: { label: string; color: string }; onChildClick?: (childId: string) => void; projectId?: string; }) { const stateStyle = STATE_COLORS[agent.state]; const [chainOfCommand, setChainOfCommand] = useState([]); const [isLoadingChainOfCommand, setIsLoadingChainOfCommand] = useState(true); const [budgetStatus, setBudgetStatus] = useState(null); // Fetch budget status on mount useEffect(() => { fetchAgentBudgetStatus(agent.id, projectId) .then(setBudgetStatus) .catch(() => setBudgetStatus(null)); }, [agent.id, projectId]); useEffect(() => { let cancelled = false; setIsLoadingChainOfCommand(true); void fetchChainOfCommand(agent.id, projectId) .then((chain) => { if (cancelled) return; const normalized = chain.length > 0 && chain[0]?.id === agent.id ? [...chain].reverse() : chain; setChainOfCommand(normalized); }) .catch(() => { if (!cancelled) { setChainOfCommand([]); } }) .finally(() => { if (!cancelled) { setIsLoadingChainOfCommand(false); } }); return () => { cancelled = true; }; }, [agent.id, projectId]); const stats = useMemo(() => { const runs = (agent as any).completedRuns || []; const today = new Date(); today.setHours(0, 0, 0, 0); const todayRuns = runs.filter((r: AgentHeartbeatRun) => new Date(r.startedAt) >= today ); const successfulRuns = runs.filter((r: AgentHeartbeatRun) => r.status === "completed" ); return { totalRuns: runs.length, todayRuns: todayRuns.length, successfulRuns: successfulRuns.length, successRate: runs.length > 0 ? Math.round((successfulRuns.length / runs.length) * 100) : 0, }; }, [agent]); return (
{/* Budget Exhausted Warning */} {budgetStatus?.isOverBudget && (
⚠️ Budget Exhausted: This agent has exceeded its token budget and may be operating with limited functionality.
)} {/* Agent Info Card */}

Agent Information

Name {agent.name}
Role {agent.role}
State {agent.state}
Health {health.label}
{budgetStatus?.budgetLimit != null && (
Budget {budgetStatus.isOverBudget ? "⚠ Budget Exhausted" : `${Math.round(budgetStatus.usagePercent ?? 0)}% used`}
)}
Created {new Date(agent.createdAt).toLocaleDateString()}
Last Heartbeat {agent.lastHeartbeatAt ? relativeTime(agent.lastHeartbeatAt) : "Never" }

Chain of Command

{isLoadingChainOfCommand ? (
Loading reporting chain...
) : chainOfCommand.length <= 1 ? (

No reporting chain

) : (
{chainOfCommand.map((chainAgent, index) => { const isCurrent = index === chainOfCommand.length - 1; const isAncestor = !isCurrent; return (
{!isCurrent && ( )}
); })}
)}
{/* Stats Cards */}

Statistics

{stats.totalRuns}
Total Runs
{stats.todayRuns}
Runs Today
{stats.successRate}%
Success Rate
{/* Current Task */} {agent.taskId && (

Current Task

{agent.taskId} View Task
)} {/* Metadata */} {agent.metadata && Object.keys(agent.metadata).length > 0 && (

Metadata

            {JSON.stringify(agent.metadata, null, 2)}
          
)}
); } // ── Logs Tab ────────────────────────────────────────────────────────────── function LogsTab({ logs, isStreaming, containerRef, hasTask }: { logs: AgentLogEntry[]; isStreaming: boolean; containerRef: React.RefObject; hasTask: boolean; }) { if (!hasTask) { return (

No task assigned

Agent logs are available when the agent is assigned to a task

); } return (
{logs.length} entries {isStreaming && ( Live )}
{logs.length === 0 ? (

No log entries yet

{isStreaming ? "Waiting for activity..." : "Logs will appear here when the agent is active"}

) : ( logs.map((entry, i) => { const prevEntry = i > 0 ? logs[i - 1] : undefined; const showTimestamp = !prevEntry || prevEntry.agent !== entry.agent; return ( ); }) )}
); } function LogEntry({ entry, showTimestamp }: { entry: AgentLogEntry; showTimestamp: boolean }) { const getEntryStyles = () => { switch (entry.type) { case "tool": return { color: "var(--accent)", borderLeft: "3px solid var(--accent)", background: "var(--log-tool-bg)", }; case "tool_result": return { color: "var(--color-success)", borderLeft: "3px solid var(--color-success)", background: "var(--log-success-bg)", }; case "tool_error": return { color: "var(--color-error)", borderLeft: "3px solid var(--color-error)", background: "var(--log-error-bg)", }; case "thinking": return { color: "var(--text-muted)", fontStyle: "italic" as const, opacity: 0.7, }; default: return { color: "var(--text-primary)", }; } }; const styles = getEntryStyles(); const timestamp = new Date(entry.timestamp).toLocaleTimeString(); return (
{showTimestamp && ( [{timestamp}] )} {entry.agent && ( [{entry.agent}] )} {entry.type === "tool" && } {entry.type === "tool_result" && } {entry.type === "tool_error" && } {entry.text} {entry.detail && ( — {entry.detail} )}
); } // ── Runs Tab ─────────────────────────────────────────────────────────────── function RunsTab({ addToast, agentId, projectId, agentState, agentName, }: { addToast: (msg: string, type?: "success" | "error") => void; agentId: string; projectId?: string; agentState?: AgentState; agentName?: string; }) { const [runs, setRuns] = useState([]); const [isLoadingRuns, setIsLoadingRuns] = useState(true); const [selectedRunId, setSelectedRunId] = useState(null); const [runLogs, setRunLogs] = useState([]); const [isLoadingLogs, setIsLoadingLogs] = useState(false); const [detailRun, setDetailRun] = useState(null); const [isLoadingDetail, setIsLoadingDetail] = useState(false); // Load runs on mount const loadRuns = useCallback(async () => { try { const data = await fetchAgentRuns(agentId, 50, projectId); setRuns(data); } catch (err: any) { addToast(`Failed to load runs: ${err.message}`, "error"); } finally { setIsLoadingRuns(false); } }, [agentId, projectId, addToast]); useEffect(() => { void loadRuns(); }, [loadRuns]); // Poll for active runs const hasActiveRun = runs.some(r => r.status === "active"); useEffect(() => { if (!hasActiveRun) return; const interval = setInterval(() => { void loadRuns(); }, 5000); return () => clearInterval(interval); }, [hasActiveRun, loadRuns]); // Load run detail when a run is selected const handleRunClick = useCallback(async (runId: string) => { if (selectedRunId === runId) { setSelectedRunId(null); setRunLogs([]); setDetailRun(null); return; } setSelectedRunId(runId); setIsLoadingLogs(true); setIsLoadingDetail(true); setRunLogs([]); setDetailRun(null); try { const [logs, detail] = await Promise.all([ fetchAgentRunLogs(agentId, runId, projectId), fetchAgentRunDetail(agentId, runId, projectId), ]); setRunLogs(logs); setDetailRun(detail); } catch (err: any) { addToast(`Failed to load run details: ${err.message}`, "error"); setRunLogs([]); setDetailRun(null); } finally { setIsLoadingLogs(false); setIsLoadingDetail(false); } }, [selectedRunId, agentId, projectId, addToast]); const handleRunHeartbeat = async () => { try { await startAgentRun(agentId, projectId, { source: "on_demand", triggerDetail: "Triggered from dashboard" }); addToast(`Heartbeat run started for ${agentName ?? agentId}`, "success"); setIsLoadingRuns(true); void loadRuns(); } catch (err: any) { addToast(`Failed to start heartbeat run: ${err.message}`, "error"); } }; const handleStopRun = async () => { if (!confirm("Stop the active run? The agent's work will be interrupted.")) { return; } try { await stopAgentRun(agentId, projectId); addToast("Run stopped", "success"); setIsLoadingRuns(true); void loadRuns(); } catch (err: any) { addToast(`Failed to stop run: ${err.message}`, "error"); } }; const canRunHeartbeat = agentState === "active" || agentState === "idle"; if (isLoadingRuns && runs.length === 0) { return (
Loading runs...
); } if (runs.length === 0) { return (
{canRunHeartbeat && (
)}

No runs yet

Heartbeat runs will appear here

); } const sortedRuns = [...runs].sort( (a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime() ); const activeRuns = sortedRuns.filter(r => r.status === "active"); const completedRuns = sortedRuns.filter(r => r.status !== "active"); const renderUsage = (usage: { inputTokens: number; outputTokens: number; cachedTokens: number } | undefined) => { if (!usage) return null; return (
Input: {usage.inputTokens.toLocaleString()} Output: {usage.outputTokens.toLocaleString()} {usage.cachedTokens > 0 && Cached: {usage.cachedTokens.toLocaleString()}}
); }; const renderRunCard = (run: AgentHeartbeatRun, index: number, isActive: boolean) => { const statusInfo = RUN_STATUS_ICONS[run.status] || RUN_STATUS_ICONS.completed; const StatusIcon = statusInfo.icon; const duration = run.endedAt ? formatDuration(new Date(run.startedAt), new Date(run.endedAt)) : "In progress"; const isSelected = selectedRunId === run.id; return (
void handleRunClick(run.id)} style={{ cursor: "pointer" }} role="button" tabIndex={0} aria-expanded={isSelected} aria-label={`${isActive ? "Active" : ""} run ${run.id.slice(0, 8)}, ${run.status}`} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); void handleRunClick(run.id); } }} >
{isSelected ? : } {isActive ? ( Live Run ) : ( #{index + 1} {run.id.slice(0, 8)} )}
{run.invocationSource && ( {run.invocationSource} )} {isActive && ( )} {run.status}
Started {relativeTime(run.startedAt)} {duration} {run.triggerDetail && ( <> {run.triggerDetail} )}
{isSelected && (
{/* Execution Details */} {isLoadingDetail ? (
Loading details...
) : detailRun && (
{/* Token Usage */} {detailRun.usageJson && (
Token Usage
{renderUsage(detailRun.usageJson)}
)} {/* Output */} {detailRun.stdoutExcerpt && (
Output
                      {detailRun.stdoutExcerpt.length > 2000
                        ? `${detailRun.stdoutExcerpt.slice(0, 2000)}\n\n... (truncated, ${detailRun.stdoutExcerpt.length} chars total)`
                        : detailRun.stdoutExcerpt}
                    
)} {/* Errors */} {detailRun.stderrExcerpt && (
Errors
                      {detailRun.stderrExcerpt}
                    
)} {/* Result */} {detailRun.resultJson && (
Result
                      {JSON.stringify(detailRun.resultJson, null, 2)}
                    
)} {/* Context */} {detailRun.contextSnapshot && Object.keys(detailRun.contextSnapshot).length > 0 && (
Context
{Object.entries(detailRun.contextSnapshot).map(([key, value]) => ( {key}:{" "} {String(value)} ))}
)} {/* No output state */} {!detailRun.stdoutExcerpt && !detailRun.stderrExcerpt && !detailRun.resultJson && (
No output captured
)}
)} {/* Run Logs */}
Agent Logs
{isLoadingLogs ? (
Loading logs...
) : runLogs.length === 0 ? (
No logs available for this run
) : ( )}
)}
); }; return (
{canRunHeartbeat && (
{runs.length} run{runs.length !== 1 ? "s" : ""} {hasActiveRun && Live}
{hasActiveRun && ( )}
)} {activeRuns.map((run, i) => renderRunCard(run, i, true))} {completedRuns.map((run, i) => renderRunCard(run, activeRuns.length + i, false))}
); } function formatDuration(start: Date, end: Date): string { const diff = Math.floor((end.getTime() - start.getTime()) / 1000); if (diff < 60) return `${diff}s`; if (diff < 3600) return `${Math.floor(diff / 60)}m ${diff % 60}s`; return `${Math.floor(diff / 3600)}h ${Math.floor((diff % 3600) / 60)}m`; } const TASK_COLUMN_LABELS: Record = { triage: "Triage", todo: "Todo", "in-progress": "In Progress", "in-review": "In Review", done: "Done", archived: "Archived", }; function truncateTaskLabel(task: Task): string { const source = task.title?.trim() || task.description?.trim() || task.id; return source.length > 80 ? `${source.slice(0, 77)}...` : source; } function TasksTab({ agentId, projectId, addToast, }: { agentId: string; projectId?: string; addToast: (msg: string, type?: "success" | "error") => void; }) { const [tasks, setTasks] = useState([]); const [isLoading, setIsLoading] = useState(true); useEffect(() => { let cancelled = false; setIsLoading(true); void fetchAgentTasks(agentId, projectId) .then((assignedTasks) => { if (!cancelled) { setTasks(assignedTasks); } }) .catch((err: any) => { if (!cancelled) { setTasks([]); addToast(`Failed to load assigned tasks: ${err.message}`, "error"); } }) .finally(() => { if (!cancelled) { setIsLoading(false); } }); return () => { cancelled = true; }; }, [agentId, projectId, addToast]); if (isLoading) { return (

Loading assigned tasks...

); } if (tasks.length === 0) { return (

No tasks assigned to this agent

); } return ( ); } // ── Config Tab ───────────────────────────────────────────────────────────── /** Shape of a single advanced setting field stored in agent.metadata */ interface AdvancedSettingField { key: string; label: string; type: "text" | "number" | "select"; placeholder?: string; hint?: string; options?: Array<{ value: string; label: string }>; /** Minimum value for number fields */ min?: number; /** Maximum value for number fields */ max?: number; } /** Well-known advanced setting definitions backed by agent.metadata */ const ADVANCED_SETTINGS: AdvancedSettingField[] = [ { key: "maxRetries", label: "Max Retries", type: "number", placeholder: "3", hint: "Maximum number of automatic retries on task failure (0–10, default 3)", min: 0, max: 10, }, { key: "timeoutMs", label: "Task Timeout (ms)", type: "number", placeholder: "600000", hint: "Maximum time in ms before a task is considered timed out (minimum 60000ms, default 600000ms)", min: 60000, max: 86400000, }, { key: "logLevel", label: "Log Level", type: "select", hint: "Verbosity of agent log output", options: [ { value: "debug", label: "Debug" }, { value: "info", label: "Info" }, { value: "warn", label: "Warning" }, { value: "error", label: "Error" }, ], }, ]; /** Validation errors keyed by setting key */ type ValidationErrors = Record; function validateAdvancedSettings( values: Record, ): ValidationErrors { const errors: ValidationErrors = {}; for (const field of ADVANCED_SETTINGS) { const raw = values[field.key]?.trim(); // Empty is fine — it means "use default" if (!raw) continue; if (field.type === "number") { const num = Number(raw); if (Number.isNaN(num) || !Number.isFinite(num)) { errors[field.key] = `"${field.label}" must be a valid number`; continue; } if (field.min !== undefined && num < field.min) { errors[field.key] = `"${field.label}" must be at least ${field.min.toLocaleString()}`; } if (field.max !== undefined && num > field.max) { errors[field.key] = `"${field.label}" must be at most ${field.max.toLocaleString()}`; } } if (field.type === "select") { const validOptions = field.options?.map((o) => o.value) ?? []; if (validOptions.length > 0 && !validOptions.includes(raw)) { errors[field.key] = `"${field.label}" must be one of: ${validOptions.join(", ")}`; } } } return errors; } function SoulTab({ agent, projectId, addToast, onSaved, }: { agent: AgentDetail; projectId?: string; addToast: (message: string, type?: "success" | "error") => void; onSaved: () => Promise; }) { const [soul, setSoul] = useState(agent.soul ?? ""); const [isSaving, setIsSaving] = useState(false); const [justSaved, setJustSaved] = useState(false); useEffect(() => { setSoul(agent.soul ?? ""); setJustSaved(false); }, [agent.id, agent.soul]); const hasChanges = soul !== (agent.soul ?? ""); const handleSave = async () => { if (soul.length > 10000) { addToast("Soul must be at most 10,000 characters", "error"); return; } setIsSaving(true); try { await updateAgentSoul(agent.id, soul, projectId); addToast("Soul saved", "success"); setJustSaved(true); setTimeout(() => setJustSaved(false), 3000); await onSaved(); } catch (err: any) { addToast(`Failed to save soul: ${err.message}`, "error"); } finally { setIsSaving(false); } }; return (

Soul

Define this agent's personality and identity.