import "./AgentsView.css";
import { useState, useEffect, useCallback, useRef, useMemo, useId, lazy, Suspense, type CSSProperties } from "react";
import { Plus, Play, Pause, Activity, Trash2, RefreshCw, Bot, List, ChevronRight, Filter, Upload, Network, SlidersHorizontal, ZoomIn, ZoomOut, Minimize2, Info } from "lucide-react";
import type { Agent, AgentCapability, AgentOnboardingSummary, AgentState, OrgTreeNode } from "../api";
import { updateAgent, updateAgentState, deleteAgent, startAgentRun, fetchOrgTree, fetchSettings, updateSettings } from "../api";
const AgentDetailView = lazy(() => import("./AgentDetailView").then((m) => ({ default: m.AgentDetailView })));
import { AgentTokenStatsPanel } from "./AgentTokenStatsPanel";
import { AgentsOverviewBar } from "./AgentsOverviewBar";
import { AgentEmptyState } from "./AgentEmptyState";
import { useAgents } from "../hooks/useAgents";
import { useConfirm } from "../hooks/useConfirm";
import { NewAgentDialog } from "./NewAgentDialog";
import { AgentImportModal } from "./AgentImportModal";
import { getScopedItem, setScopedItem } from "../utils/projectStorage";
import { useViewportMode } from "../hooks/useViewportMode";
import { getAgentHealthStatus } from "../utils/agentHealth";
import type { AgentHealthStatus } from "../utils/agentHealth";
import {
formatHeartbeatInterval,
getHeartbeatIntervalOptions,
resolveHeartbeatIntervalMs,
MIN_HEARTBEAT_INTERVAL_MS,
HEARTBEAT_INTERVAL_PRESETS,
} from "../utils/heartbeatIntervals";
import { isEphemeralAgent, getErrorMessage } from "@fusion/core";
import { formatAgentSkillBadgeLabel } from "../utils/agentSkills";
import { resolveOrgChartLayoutMode, type OrgChartLayoutMode } from "./agentsOrgChartLayout";
import { AgentAvatar } from "./AgentAvatar";
import { AgentErrorIndicator } from "./AgentErrorDetailsModal";
export interface AgentsViewProps {
addToast: (message: string, type?: "success" | "error") => void;
projectId?: string;
onOpenTaskLogs?: (taskId: string) => void;
agentOnboardingEnabled?: boolean;
}
const AGENT_ROLES: { value: AgentCapability; label: string; icon: string }[] = [
{ value: "triage", label: "Triage", icon: "⊕" },
{ value: "executor", label: "Executor", icon: "▶" },
{ value: "reviewer", label: "Reviewer", icon: "⊙" },
{ value: "merger", label: "Merger", icon: "⊞" },
{ value: "scheduler", label: "Scheduler", icon: "◷" },
{ value: "engineer", label: "Engineer", icon: "⎔" },
{ value: "custom", label: "Custom", icon: "✦" },
];
const HEARTBEAT_MULTIPLIER_PRESETS = [0.1, 0.25, 0.5, 1, 2, 3, 5, 10] as const;
const ORG_CHART_ZOOM_LEVELS = [0.75, 1, 1.25, 1.5] as const;
function getStateBadgeClass(state: AgentState): string {
switch (state) {
case "running":
return "agent-badge--running";
case "active":
return "agent-badge--active";
case "paused":
return "agent-badge--paused";
case "error":
return "agent-badge--error";
case "idle":
default:
return "agent-badge--idle";
}
}
function getStateCardClass(
prefix: "agent-card" | "agent-board-card" | "org-chart-node-card",
state: AgentState,
): string {
switch (state) {
case "running":
return `${prefix}--running`;
case "active":
return `${prefix}--active`;
case "paused":
return `${prefix}--paused`;
case "error":
return `${prefix}--error`;
case "idle":
default:
return `${prefix}--idle`;
}
}
function getOrgChartLeafCount(node: OrgTreeNode): number {
if (node.children.length === 0) {
return 1;
}
return node.children.reduce((sum, child) => sum + getOrgChartLeafCount(child), 0);
}
function getHealthSummary(agent: Agent, health: AgentHealthStatus): { title: string | undefined; label: string | null } {
if (agent.state === "error") {
return { title: undefined, label: "Error" };
}
return {
title: health.reason ?? health.label,
label: health.stateDerived ? null : health.label,
};
}
function OrgChartNode({
node,
onSelect,
getHealthStatus,
getRoleIcon,
selectedAgentId,
}: {
node: OrgTreeNode;
onSelect: (id: string) => void;
getHealthStatus: (agent: Agent) => AgentHealthStatus;
getRoleIcon: (role: AgentCapability) => string;
selectedAgentId: string | null;
}) {
const { agent, children } = node;
const health = getHealthStatus(agent);
const healthSummary = getHealthSummary(agent, health);
const stateBadgeClass = getStateBadgeClass(agent.state);
const stateNodeClass = getStateCardClass("org-chart-node-card", agent.state);
const subtreeLeafCount = getOrgChartLeafCount(node);
const nodeStyle = { "--org-chart-subtree-leaves": String(subtreeLeafCount) } as CSSProperties;
const firstChildLeafCount = children.length > 0 ? getOrgChartLeafCount(children[0]) : 1;
const lastChildLeafCount = children.length > 0 ? getOrgChartLeafCount(children[children.length - 1]) : 1;
const childrenStyle = {
"--org-chart-first-child-leaves": String(firstChildLeafCount),
"--org-chart-last-child-leaves": String(lastChildLeafCount),
} as CSSProperties;
return (
0 ? " org-chart-node--has-children" : ""}`}
style={nodeStyle}
>
onSelect(agent.id)}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
if (e.key === " ") {
e.preventDefault();
}
onSelect(agent.id);
}
}}
>
{agent.state}
{health.icon}
{healthSummary.label && {healthSummary.label}}
{children.length > 0 && (
{children.map((child) => (
))}
)}
);
}
export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardingEnabled = false }: AgentsViewProps) {
const [showSystemAgents, setShowSystemAgents] = useState(false);
const viewportMode = useViewportMode();
const isMobileViewport = viewportMode === "mobile";
const [filterState, setFilterState] = useState("all");
const { agents, stats, isLoading, loadAgents, refreshAgents } = useAgents(projectId, {
filterState,
showSystemAgents,
});
const [isCreating, setIsCreating] = useState(false);
const [onboardingDraft, setOnboardingDraft] = useState(null);
const [isImporting, setIsImporting] = useState(false);
const [selectedAgentId, setSelectedAgentId] = useState(null);
const [selectedOrgChartAgentId, setSelectedOrgChartAgentId] = useState(null);
const isMobileDetailOpen = isMobileViewport && !!selectedAgentId;
const [selectedAgentInitialTab, setSelectedAgentInitialTab] = useState<"dashboard" | "runs">("dashboard");
const [selectedAgentInitialRunId, setSelectedAgentInitialRunId] = useState(null);
const [selectedAgentPreferActiveRun, setSelectedAgentPreferActiveRun] = useState(false);
const [agentView, setAgentView] = useState<"list" | "board" | "org">(() => {
if (typeof window === "undefined") return "list";
const saved = getScopedItem("fn-agent-view", projectId);
return (saved === "list" || saved === "board" || saved === "org") ? saved : "list";
});
const [orgTree, setOrgTree] = useState([]);
const [isOrgTreeLoading, setIsOrgTreeLoading] = useState(false);
const [orgChartViewportWidth, setOrgChartViewportWidth] = useState(0);
const [isControlsPanelOpen, setIsControlsPanelOpen] = useState(false);
const [isOverviewOpen, setIsOverviewOpen] = useState(false);
const [orgChartZoomIndex, setOrgChartZoomIndex] = useState(1);
const controlsPanelRef = useRef(null);
const orgChartViewportRef = useRef(null);
const { confirm } = useConfirm();
const controlsTriggerRef = useRef(null);
const controlsPanelId = useId();
useEffect(() => {
const saved = getScopedItem("fn-agent-view", projectId);
if (saved === "list" || saved === "board" || saved === "org") {
setAgentView(saved);
return;
}
setAgentView("list");
}, [projectId]);
// Persist view preference to localStorage
useEffect(() => {
setScopedItem("fn-agent-view", agentView, projectId);
}, [agentView, projectId]);
const [editingRoleForAgent, setEditingRoleForAgent] = useState(null);
const roleSelectRef = useRef(null);
const [updatingHeartbeatAgentId, setUpdatingHeartbeatAgentId] = useState(null);
/** Agent ID currently showing custom heartbeat input */
const [customHeartbeatAgentId, setCustomHeartbeatAgentId] = useState(null);
/** Custom minutes input value for each agent */
const [customHeartbeatMinutes, setCustomHeartbeatMinutes] = useState>({});
/** Global heartbeat multiplier loaded from project settings */
const [heartbeatMultiplier, setHeartbeatMultiplier] = useState(1);
/** Whether the heartbeat multiplier is currently being saved */
const [isSavingMultiplier, setIsSavingMultiplier] = useState(false);
/** Agent IDs with an in-flight state transition (for optimistic update guard) */
const [transitioningAgentIds, setTransitioningAgentIds] = useState>(new Set());
/** Optimistic state overrides keyed by agent ID while pause/resume/start API call is in-flight */
const [optimisticStateOverrides, setOptimisticStateOverrides] = useState