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, ChevronDown, ChevronRight } from "lucide-react"; import type { AgentDetail, AgentState, AgentHeartbeatRun } from "../api"; import { fetchAgent, updateAgent, updateAgentState, deleteAgent, fetchAgentLogs, fetchAgentRunLogs, fetchAgentChildren, fetchAgentRuns, fetchAgentRunDetail, startAgentRun, updateAgentInstructions } from "../api"; import type { Agent } from "../api"; import type { AgentLogEntry } from "@fusion/core"; import { AgentLogViewer } from "./AgentLogViewer"; /** * 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" | "children"; 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: "children", label: "Children", icon: GitBranch }, { 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 loadAgent = useCallback(async () => { setIsLoading(true); try { const data = await fetchAgent(agentId, projectId); setAgent(data); } catch (err: any) { addToast(`Failed to load agent: ${err.message}`, "error"); onClose(); } finally { setIsLoading(false); } }, [agentId, addToast, onClose, 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]); 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"); } }; const getHealthStatus = () => { if (!agent) return { label: "Unknown", color: "var(--text-muted, #8b949e)" }; if (agent.state === "terminated") { return { label: "Terminated", color: "var(--state-error-text, #f85149)" }; } if (agent.state === "error") { return { label: agent.lastError ?? "Error", color: "var(--state-error-text, #f85149)" }; } if (agent.state === "paused") { return { label: agent.pauseReason ? `Paused: ${agent.pauseReason}` : "Paused", color: "var(--state-paused-text, #e3b541)" }; } if (agent.state === "running") { return { label: "Running", color: "var(--state-active-text, #3fb950)" }; } if (!agent.lastHeartbeatAt) { return { label: agent.state === "active" ? "Starting..." : "Idle", color: "var(--state-idle-text, #8b949e)" }; } const lastHeartbeat = new Date(agent.lastHeartbeatAt).getTime(); const elapsed = Date.now() - lastHeartbeat; const timeoutMs = (agent as any).runtimeConfig?.heartbeatTimeoutMs ?? 60000; if (elapsed > timeoutMs) { return { label: "Unresponsive", color: "var(--state-error-text, #f85149)" }; } return { label: "Healthy", color: "var(--state-active-text, #3fb950)" }; }; 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.label === "Healthy" && } {health.label === "Unresponsive" && } {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 === "config" && ( )} {activeTab === "children" && ( )}
{/* Footer with agent ID */}
{agent.id} {agent.taskId && ( <> | Working on: {agent.taskId} )}
); } // ── Dashboard Tab ─────────────────────────────────────────────────────────── function DashboardTab({ agent, health }: { agent: AgentDetail; health: { label: string; color: string }; }) { const stateStyle = STATE_COLORS[agent.state]; 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 (
{/* Agent Info Card */}

Agent Information

Name {agent.name}
Role {agent.role}
State {agent.state}
Health {health.label}
Created {new Date(agent.createdAt).toLocaleDateString()}
Last Heartbeat {agent.lastHeartbeatAt ? relativeTime(agent.lastHeartbeatAt) : "Never" }
{/* 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 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} )} {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}
)} {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`; } // ── 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 ConfigTab({ agent, projectId, addToast, onSaved, }: { agent: AgentDetail; projectId?: string; addToast: (message: string, type?: "success" | "error") => void; onSaved: () => Promise; }) { // Local form state initialised from agent.metadata const [formValues, setFormValues] = useState>(() => { const initial: Record = {}; for (const field of ADVANCED_SETTINGS) { const raw = agent.metadata[field.key]; if (raw !== undefined && raw !== null) { initial[field.key] = String(raw); } } return initial; }); // Heartbeat config state initialised from agent.runtimeConfig const [heartbeatValues, setHeartbeatValues] = useState>(() => { const rc = agent.runtimeConfig ?? {}; const initial: Record = {}; if (rc.heartbeatIntervalMs !== undefined && rc.heartbeatIntervalMs !== null) { initial.heartbeatIntervalMs = String(rc.heartbeatIntervalMs); } if (rc.heartbeatTimeoutMs !== undefined && rc.heartbeatTimeoutMs !== null) { initial.heartbeatTimeoutMs = String(rc.heartbeatTimeoutMs); } if (rc.maxConcurrentRuns !== undefined && rc.maxConcurrentRuns !== null) { initial.maxConcurrentRuns = String(rc.maxConcurrentRuns); } if (rc.messageResponseMode === "immediate" || rc.messageResponseMode === "on-heartbeat") { initial.messageResponseMode = rc.messageResponseMode; } return initial; }); const [isSaving, setIsSaving] = useState(false); const [isSavingInstructions, setIsSavingInstructions] = useState(false); const [errors, setErrors] = useState({}); const [justSaved, setJustSaved] = useState(false); const [justSavedInstructions, setJustSavedInstructions] = useState(false); // Custom instructions state const [instructionsText, setInstructionsText] = useState(agent.instructionsText ?? ""); const [instructionsPath, setInstructionsPath] = useState(agent.instructionsPath ?? ""); /** Detect whether any local value differs from the persisted metadata */ const hasChanges = (() => { for (const field of ADVANCED_SETTINGS) { const current = formValues[field.key]?.trim() ?? ""; const persisted = agent.metadata[field.key] !== undefined && agent.metadata[field.key] !== null ? String(agent.metadata[field.key]) : ""; if (current !== persisted) return true; } // Check heartbeat values const rc = agent.runtimeConfig ?? {}; for (const key of ["heartbeatIntervalMs", "heartbeatTimeoutMs", "maxConcurrentRuns", "messageResponseMode"] as const) { const current = heartbeatValues[key]?.trim() ?? ""; const persisted = rc[key] !== undefined && rc[key] !== null ? String(rc[key]) : ""; if (current !== persisted) return true; } return false; })(); const hasInstructionsChanges = (() => { const currentText = instructionsText ?? ""; const persistedText = agent.instructionsText ?? ""; const currentPath = instructionsPath?.trim() ?? ""; const persistedPath = agent.instructionsPath?.trim() ?? ""; return currentText !== persistedText || currentPath !== persistedPath; })(); const handleFieldChange = (key: string, value: string) => { setFormValues((prev) => ({ ...prev, [key]: value })); setJustSaved(false); // Clear individual field error on change if (errors[key]) { setErrors((prev) => { const next = { ...prev }; delete next[key]; return next; }); } }; const handleHeartbeatFieldChange = (key: string, value: string) => { setHeartbeatValues((prev) => ({ ...prev, [key]: value })); setJustSaved(false); if (errors[key]) { setErrors((prev) => { const next = { ...prev }; delete next[key]; return next; }); } }; const handleSave = async () => { // Validate advanced settings const validationErrors = validateAdvancedSettings(formValues); // Validate heartbeat settings for (const [key, config] of Object.entries({ heartbeatIntervalMs: { label: "Heartbeat Interval", min: 1000 }, heartbeatTimeoutMs: { label: "Heartbeat Timeout", min: 5000 }, maxConcurrentRuns: { label: "Max Concurrent Runs", min: 1 }, })) { const raw = heartbeatValues[key]?.trim(); if (!raw) continue; const num = Number(raw); if (Number.isNaN(num) || !Number.isFinite(num)) { validationErrors[key] = `"${config.label}" must be a valid number`; } else if (num < config.min) { validationErrors[key] = `"${config.label}" must be at least ${config.min.toLocaleString()}`; } } const messageResponseModeForValidation = heartbeatValues.messageResponseMode?.trim(); if (messageResponseModeForValidation && !["immediate", "on-heartbeat"].includes(messageResponseModeForValidation)) { validationErrors.messageResponseMode = "\"Message Response Mode\" must be either immediate or on-heartbeat"; } if (Object.keys(validationErrors).length > 0) { setErrors(validationErrors); addToast("Please fix validation errors before saving", "error"); return; } // Build the metadata payload — only include non-empty values const newMetadata: Record = { ...agent.metadata }; for (const field of ADVANCED_SETTINGS) { const raw = formValues[field.key]?.trim(); if (!raw) { // Remove the key to use system default delete newMetadata[field.key]; } else if (field.type === "number") { newMetadata[field.key] = Number(raw); } else { newMetadata[field.key] = raw; } } // Build the runtimeConfig payload — only include non-empty values const newRuntimeConfig: Record = { ...agent.runtimeConfig }; for (const key of ["heartbeatIntervalMs", "heartbeatTimeoutMs", "maxConcurrentRuns"] as const) { const raw = heartbeatValues[key]?.trim(); if (!raw) { delete newRuntimeConfig[key]; } else { newRuntimeConfig[key] = Number(raw); } } const messageResponseMode = heartbeatValues.messageResponseMode?.trim(); if (!messageResponseMode) { delete newRuntimeConfig.messageResponseMode; } else { newRuntimeConfig.messageResponseMode = messageResponseMode; } setIsSaving(true); try { await updateAgent(agent.id, { metadata: newMetadata, runtimeConfig: newRuntimeConfig }, projectId); addToast("Settings saved", "success"); setJustSaved(true); // Auto-hide the saved indicator after 3 seconds setTimeout(() => setJustSaved(false), 3000); await onSaved(); } catch (err: any) { addToast(`Failed to save settings: ${err.message}`, "error"); } finally { setIsSaving(false); } }; const handleSaveInstructions = async () => { setIsSavingInstructions(true); try { await updateAgentInstructions( agent.id, { instructionsText: instructionsText || undefined, instructionsPath: instructionsPath.trim() || undefined, }, projectId, ); addToast("Instructions saved", "success"); setJustSavedInstructions(true); setTimeout(() => setJustSavedInstructions(false), 3000); await onSaved(); } catch (err: any) { addToast(`Failed to save instructions: ${err.message}`, "error"); } finally { setIsSavingInstructions(false); } }; return (

Agent Configuration

Configure agent settings and behavior.

Name changes coming soon
Role changes coming soon

Heartbeat Settings

Configure how this agent's heartbeat is monitored. Leave a field empty to use system defaults.

handleHeartbeatFieldChange("heartbeatIntervalMs", e.target.value)} /> {errors.heartbeatIntervalMs ? ( {errors.heartbeatIntervalMs} ) : ( How often heartbeats are checked. Leave empty for system default (30000ms) )}
handleHeartbeatFieldChange("heartbeatTimeoutMs", e.target.value)} /> {errors.heartbeatTimeoutMs ? ( {errors.heartbeatTimeoutMs} ) : ( Time without heartbeat before agent is considered unresponsive. Leave empty for system default (60000ms) )}
handleHeartbeatFieldChange("maxConcurrentRuns", e.target.value)} /> {errors.maxConcurrentRuns ? ( {errors.maxConcurrentRuns} ) : ( Maximum simultaneous heartbeat runs for this agent. Leave empty for system default (1). )}
{errors.messageResponseMode ? ( {errors.messageResponseMode} ) : ( How this agent responds to incoming messages. 'Immediate' wakes the agent as soon as a message arrives. 'On Heartbeat' defers processing to the next scheduled heartbeat. )}

Advanced Settings

Advanced configuration options for this agent. Leave a field empty to use system defaults.

{ADVANCED_SETTINGS.map((field) => { const hasError = !!errors[field.key]; return (
{field.type === "select" ? ( ) : ( handleFieldChange(field.key, e.target.value)} /> )} {hasError && ( {errors[field.key]} )} {!hasError && field.hint && ( {field.hint} )}
); })}
{!hasChanges && justSaved && ( Settings saved )}

Custom Instructions

Append custom instructions to this agent's system prompt at execution time. Use this to customize behavior, coding style, or project conventions without modifying built-in prompts.