feat(FN-1167): add agent org chart and reporting chain views

- Add an Org Chart view mode in AgentsView with persisted view selection, API-backed tree loading, and node selection into AgentDetailView
- Add a Chain of Command section to AgentDetailView dashboard that loads reporting paths and lets users navigate to ancestor agents
- Introduce dedicated styling for org chart cards/connectors, loading states, and chain-of-command pills with responsive behavior
- Expand AgentsView and AgentDetailView test coverage for org chart rendering, loading/empty states, and chain-of-command interactions
This commit is contained in:
gsxdsm
2026-04-08 11:11:06 -07:00
parent 20f6a4998d
commit 2ee3b1d55c
5 changed files with 671 additions and 12 deletions

View File

@@ -6,7 +6,7 @@ import {
ChevronDown, ChevronRight
} from "lucide-react";
import type { AgentDetail, AgentState, AgentHeartbeatRun } from "../api";
import { fetchAgent, updateAgent, updateAgentState, deleteAgent, fetchAgentLogs, fetchAgentRunLogs, fetchAgentChildren, fetchAgentRuns, fetchAgentRunDetail, startAgentRun, updateAgentInstructions, fetchAgentTasks } from "../api";
import { fetchAgent, updateAgent, updateAgentState, deleteAgent, fetchAgentLogs, fetchAgentRunLogs, fetchAgentChildren, fetchAgentRuns, fetchAgentRunDetail, startAgentRun, updateAgentInstructions, fetchAgentTasks, fetchChainOfCommand } from "../api";
import type { Agent } from "../api";
import type { AgentLogEntry, Task } from "@fusion/core";
import { AgentLogViewer } from "./AgentLogViewer";
@@ -361,7 +361,12 @@ export function AgentDetailView({ agentId, projectId, onClose, addToast, onChild
{/* Tab Content */}
<div className="agent-detail-content">
{activeTab === "dashboard" && (
<DashboardTab agent={agent} health={health} />
<DashboardTab
agent={agent}
health={health}
onChildClick={onChildClick}
projectId={projectId}
/>
)}
{activeTab === "logs" && (
@@ -437,13 +442,47 @@ export function AgentDetailView({ agentId, projectId, onClose, addToast, onChild
function DashboardTab({
agent,
health
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<Agent[]>([]);
const [isLoadingChainOfCommand, setIsLoadingChainOfCommand] = useState(true);
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();
@@ -514,6 +553,44 @@ function DashboardTab({
</div>
</div>
<div className="dashboard-section">
<h3>
<GitBranch size={16} style={{ marginRight: "6px", verticalAlign: "-2px" }} />
Chain of Command
</h3>
{isLoadingChainOfCommand ? (
<div className="chain-of-command-loading" role="status" aria-live="polite">
<Loader2 size={14} className="animate-spin" />
<span>Loading reporting chain...</span>
</div>
) : chainOfCommand.length <= 1 ? (
<p className="text-muted">No reporting chain</p>
) : (
<div className="chain-of-command-path" aria-label="Chain of command">
{chainOfCommand.map((chainAgent, index) => {
const isCurrent = index === chainOfCommand.length - 1;
const isAncestor = !isCurrent;
return (
<div key={chainAgent.id} className="chain-of-command-item">
<button
type="button"
className={`chain-of-command-node${isCurrent ? " chain-of-command-node--current" : ""}`}
onClick={() => isAncestor && onChildClick?.(chainAgent.id)}
disabled={!isAncestor || !onChildClick}
title={isCurrent ? "Current agent" : `View ${chainAgent.name}`}
>
{chainAgent.name}
</button>
{!isCurrent && (
<span className="chain-of-command-separator" aria-hidden="true"></span>
)}
</div>
);
})}
</div>
)}
</div>
{/* Stats Cards */}
<div className="dashboard-section">
<h3>Statistics</h3>

View File

@@ -1,8 +1,8 @@
import { useState, useEffect, useCallback, useRef } from "react";
import type { JSX } from "react";
import { Plus, Play, Pause, Square, Activity, Heart, Trash2, RefreshCw, Bot, LayoutGrid, List, ChevronRight, ChevronDown, GitBranch, Filter, Upload } from "lucide-react";
import type { Agent, AgentCapability, AgentState } from "../api";
import { fetchAgents, updateAgent, updateAgentState, deleteAgent, startAgentRun } from "../api";
import { Plus, Play, Pause, Square, Activity, Heart, Trash2, RefreshCw, Bot, LayoutGrid, List, ChevronRight, ChevronDown, GitBranch, Filter, Upload, Network } from "lucide-react";
import type { Agent, AgentCapability, AgentState, OrgTreeNode } from "../api";
import { fetchAgents, updateAgent, updateAgentState, deleteAgent, startAgentRun, fetchOrgTree } from "../api";
import { AgentDetailView } from "./AgentDetailView";
import { ActiveAgentsPanel } from "./ActiveAgentsPanel";
import { AgentMetricsBar } from "./AgentMetricsBar";
@@ -125,6 +125,68 @@ function AgentTreeNode({
);
}
function OrgChartNode({
node,
onSelect,
getHealthStatus,
getRoleIcon,
}: {
node: OrgTreeNode;
onSelect: (id: string) => void;
getHealthStatus: (agent: Agent) => { label: string; icon: JSX.Element; color: string };
getRoleIcon: (role: AgentCapability) => string;
}) {
const { agent, children } = node;
const health = getHealthStatus(agent);
const stateStyle = STATE_COLORS[agent.state];
return (
<div className={`org-chart-node${children.length > 0 ? " org-chart-node--has-children" : ""}`}>
<div
className="org-chart-node-card"
onClick={() => onSelect(agent.id)}
role="button"
tabIndex={0}
onKeyDown={(e) => e.key === "Enter" && onSelect(agent.id)}
>
<div className="org-chart-node__header">
<span className="org-chart-node__icon">{getRoleIcon(agent.role)}</span>
<span className="org-chart-node__name">{agent.name}</span>
</div>
<div className="org-chart-node__meta">
<span
className="org-chart-node__badge"
style={{
background: stateStyle.bg,
color: stateStyle.text,
border: `1px solid ${stateStyle.border}`,
}}
>
{agent.state}
</span>
<span className="org-chart-node__health" style={{ color: health.color }} title={health.label}>
{health.icon}
<span className="text-secondary">{health.label}</span>
</span>
</div>
</div>
{children.length > 0 && (
<div className="org-chart-children" role="group" aria-label={`${agent.name} reports`}>
{children.map((child) => (
<OrgChartNode
key={child.agent.id}
node={child}
onSelect={onSelect}
getHealthStatus={getHealthStatus}
getRoleIcon={getRoleIcon}
/>
))}
</div>
)}
</div>
);
}
export function AgentsView({ addToast, projectId }: AgentsViewProps) {
const { activeAgents, stats } = useAgents(projectId);
const [agents, setAgents] = useState<Agent[]>([]);
@@ -133,15 +195,17 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
const [isImporting, setIsImporting] = useState(false);
const [filterState, setFilterState] = useState<AgentState | "all">("all");
const [selectedAgentId, setSelectedAgentId] = useState<string | null>(null);
const [agentView, setAgentView] = useState<"board" | "list" | "tree">(() => {
const [agentView, setAgentView] = useState<"board" | "list" | "tree" | "org">(() => {
if (typeof window === "undefined") return "list";
const saved = getScopedItem("kb-agent-view", projectId);
return (saved === "board" || saved === "list" || saved === "tree") ? saved : "list";
return (saved === "board" || saved === "list" || saved === "tree" || saved === "org") ? saved : "list";
});
const [orgTree, setOrgTree] = useState<OrgTreeNode[]>([]);
const [isOrgTreeLoading, setIsOrgTreeLoading] = useState(false);
useEffect(() => {
const saved = getScopedItem("kb-agent-view", projectId);
if (saved === "board" || saved === "list" || saved === "tree") {
if (saved === "board" || saved === "list" || saved === "tree" || saved === "org") {
setAgentView(saved);
return;
}
@@ -175,6 +239,34 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
void loadAgents();
}, [loadAgents]);
useEffect(() => {
if (agentView !== "org") return;
let cancelled = false;
setIsOrgTreeLoading(true);
fetchOrgTree(projectId)
.then((data) => {
if (!cancelled) {
setOrgTree(data);
}
})
.catch((err: any) => {
if (!cancelled) {
addToast(`Failed to load org chart: ${err.message}`, "error");
setOrgTree([]);
}
})
.finally(() => {
if (!cancelled) {
setIsOrgTreeLoading(false);
}
});
return () => {
cancelled = true;
};
}, [agentView, projectId, addToast]);
// Refresh agent list on SSE events (independent from useAgents hook state)
useEffect(() => {
const query = projectId ? `?projectId=${encodeURIComponent(projectId)}` : "";
@@ -328,6 +420,15 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
>
<GitBranch size={16} />
</button>
<button
className={`view-toggle-btn${agentView === "org" ? " active" : ""}`}
onClick={() => setAgentView("org")}
title="Org Chart view"
aria-label="Org Chart view"
aria-pressed={agentView === "org"}
>
<Network size={16} />
</button>
</div>
<button
className="btn-icon"
@@ -423,6 +524,31 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
))
)}
</div>
) : agentView === "org" ? (
<div className="agent-org-chart" data-testid="agent-org-chart">
{isOrgTreeLoading ? (
<div className="agent-org-chart__loading" role="status" aria-live="polite">
<RefreshCw size={18} className="spin" />
<span>Loading org chart...</span>
</div>
) : orgTree.length === 0 ? (
<div className="agent-empty">
<Bot size={48} opacity={0.3} />
<p>No agents found</p>
<p className="text-secondary">Create an agent to get started</p>
</div>
) : (
orgTree.map((node) => (
<OrgChartNode
key={node.agent.id}
node={node}
onSelect={setSelectedAgentId}
getHealthStatus={getHealthStatus}
getRoleIcon={getRoleIcon}
/>
))
)}
</div>
) : (
<div className={agentView === "board" ? "agent-board" : "agent-list"}>
{agents.length === 0 ? (

View File

@@ -17,6 +17,7 @@ vi.mock("../../api", () => ({
fetchAgentRunDetail: vi.fn(),
startAgentRun: vi.fn(),
fetchAgentTasks: vi.fn(),
fetchChainOfCommand: vi.fn(),
}));
vi.mock("../AgentLogViewer", () => ({
@@ -27,7 +28,7 @@ vi.mock("../AgentLogViewer", () => ({
),
}));
import { fetchAgent, updateAgent, updateAgentState, fetchAgentRunLogs, fetchAgentRuns, fetchAgentRunDetail, fetchAgentTasks } from "../../api";
import { fetchAgent, updateAgent, updateAgentState, fetchAgentRunLogs, fetchAgentRuns, fetchAgentRunDetail, fetchAgentTasks, fetchChainOfCommand } from "../../api";
const mockFetchAgent = vi.mocked(fetchAgent);
const mockUpdateAgent = vi.mocked(updateAgent);
@@ -36,6 +37,7 @@ const mockFetchAgentRunLogs = vi.mocked(fetchAgentRunLogs);
const mockFetchAgentRuns = vi.mocked(fetchAgentRuns);
const mockFetchAgentRunDetail = vi.mocked(fetchAgentRunDetail);
const mockFetchAgentTasks = vi.mocked(fetchAgentTasks);
const mockFetchChainOfCommand = vi.mocked(fetchChainOfCommand);
describe("AgentDetailView", () => {
const createMockAgent = (overrides: Partial<{
@@ -89,6 +91,7 @@ describe("AgentDetailView", () => {
]);
mockFetchAgentRunDetail.mockResolvedValue(mockAgent.completedRuns[0]);
mockFetchAgentTasks.mockResolvedValue([]);
mockFetchChainOfCommand.mockResolvedValue([mockAgent]);
});
it("shows loading state initially", () => {
@@ -393,6 +396,122 @@ describe("AgentDetailView", () => {
});
});
describe("Chain of Command", () => {
it("renders chain-of-command section and displays agents in order", async () => {
mockFetchChainOfCommand.mockResolvedValue([
{ id: "agent-root", name: "CEO Agent" } as AgentDetail,
{ id: "agent-middle", name: "Director Agent" } as AgentDetail,
{ id: "agent-001", name: "Test Agent" } as AgentDetail,
] as any);
render(
<AgentDetailView
agentId="agent-001"
onClose={vi.fn()}
addToast={vi.fn()}
/>,
);
await waitFor(() => {
expect(screen.getByText("Chain of Command")).toBeInTheDocument();
});
await waitFor(() => {
const nodes = Array.from(document.querySelectorAll(".chain-of-command-node"));
expect(nodes).toHaveLength(3);
expect(nodes.map((node) => node.textContent?.trim())).toEqual([
"CEO Agent",
"Director Agent",
"Test Agent",
]);
expect(nodes[2].className).toContain("chain-of-command-node--current");
});
});
it("navigates to ancestor agent when chain node is clicked", async () => {
const onChildClick = vi.fn();
mockFetchChainOfCommand.mockResolvedValue([
{ id: "agent-root", name: "CEO Agent" } as AgentDetail,
{ id: "agent-middle", name: "Director Agent" } as AgentDetail,
{ id: "agent-001", name: "Test Agent" } as AgentDetail,
] as any);
render(
<AgentDetailView
agentId="agent-001"
onClose={vi.fn()}
addToast={vi.fn()}
onChildClick={onChildClick}
/>,
);
await waitFor(() => {
expect(screen.getByText("CEO Agent")).toBeInTheDocument();
});
fireEvent.click(screen.getByRole("button", { name: "CEO Agent" }));
expect(onChildClick).toHaveBeenCalledWith("agent-root");
});
it("shows no reporting chain for empty or single-element chains", async () => {
mockFetchChainOfCommand.mockResolvedValue([]);
const { rerender } = render(
<AgentDetailView
agentId="agent-001"
onClose={vi.fn()}
addToast={vi.fn()}
/>,
);
await waitFor(() => {
expect(screen.getByText("No reporting chain")).toBeInTheDocument();
});
mockFetchChainOfCommand.mockResolvedValue([{ id: "agent-001", name: "Test Agent" } as AgentDetail] as any);
rerender(
<AgentDetailView
agentId="agent-001"
onClose={vi.fn()}
addToast={vi.fn()}
/>,
);
await waitFor(() => {
expect(screen.getByText("No reporting chain")).toBeInTheDocument();
});
});
it("shows loading state while fetching chain of command", async () => {
let resolveChain: ((agents: AgentDetail[]) => void) | undefined;
mockFetchChainOfCommand.mockImplementation(
() =>
new Promise((resolve) => {
resolveChain = resolve;
}) as any,
);
render(
<AgentDetailView
agentId="agent-001"
onClose={vi.fn()}
addToast={vi.fn()}
/>,
);
await waitFor(() => {
expect(screen.getByText("Loading reporting chain...")).toBeInTheDocument();
});
resolveChain?.([{ id: "agent-001", name: "Test Agent" } as AgentDetail]);
await waitFor(() => {
expect(screen.queryByText("Loading reporting chain...")).not.toBeInTheDocument();
});
});
});
it("displays agent ID in footer", async () => {
render(
<AgentDetailView

View File

@@ -2,7 +2,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { AgentsView } from "../AgentsView";
import * as apiModule from "../../api";
import type { Agent, AgentState, AgentCapability } from "../../api";
import type { Agent, AgentState, AgentCapability, OrgTreeNode } from "../../api";
import { scopedKey } from "../../utils/projectStorage";
// Mock the API module
@@ -14,14 +14,20 @@ vi.mock("../../api", () => ({
updateAgentState: vi.fn(),
deleteAgent: vi.fn(),
startAgentRun: vi.fn(),
fetchOrgTree: vi.fn(),
fetchModels: vi.fn().mockResolvedValue({ models: [] }),
}));
vi.mock("../AgentDetailView", () => ({
AgentDetailView: ({ agentId }: { agentId: string }) => <div data-testid="agent-detail-view">Agent detail: {agentId}</div>,
}));
const mockFetchAgents = vi.mocked(apiModule.fetchAgents);
const mockCreateAgent = vi.mocked(apiModule.createAgent);
const mockUpdateAgentState = vi.mocked(apiModule.updateAgentState);
const mockDeleteAgent = vi.mocked(apiModule.deleteAgent);
const mockStartAgentRun = vi.mocked(apiModule.startAgentRun);
const mockFetchOrgTree = vi.mocked((apiModule as any).fetchOrgTree);
const mockFetchAgentStats = vi.mocked((apiModule as any).fetchAgentStats);
describe("AgentsView", () => {
@@ -84,6 +90,7 @@ describe("AgentsView", () => {
endedAt: null,
status: "active",
});
mockFetchOrgTree.mockResolvedValue([]);
});
describe("rendering", () => {
@@ -231,6 +238,151 @@ describe("AgentsView", () => {
});
});
describe("Org Chart view", () => {
const orgTree: OrgTreeNode[] = [
{
agent: {
id: "agent-root-1",
name: "Chief Agent",
role: "scheduler",
state: "active",
lastHeartbeatAt: new Date().toISOString(),
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
metadata: {},
},
children: [
{
agent: {
id: "agent-child-1",
name: "Director One",
role: "executor",
state: "running",
lastHeartbeatAt: new Date().toISOString(),
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
metadata: {},
},
children: [
{
agent: {
id: "agent-grandchild-1",
name: "Manager Alpha",
role: "reviewer",
state: "idle",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
metadata: {},
},
children: [],
},
],
},
{
agent: {
id: "agent-child-2",
name: "Director Two",
role: "triage",
state: "paused",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
metadata: {},
},
children: [],
},
],
},
{
agent: {
id: "agent-root-2",
name: "Independent Lead",
role: "engineer",
state: "error",
lastError: "Agent stalled",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
metadata: {},
},
children: [],
},
];
it("renders org chart toggle with aria attributes and activates org view", async () => {
mockFetchOrgTree.mockResolvedValue(orgTree);
render(<AgentsView addToast={mockAddToast} projectId={projectId} />);
const orgButton = screen.getByRole("button", { name: "Org Chart view" });
expect(orgButton.getAttribute("aria-pressed")).toBe("false");
fireEvent.click(orgButton);
await waitFor(() => {
expect(orgButton.className).toContain("active");
expect(orgButton.getAttribute("aria-pressed")).toBe("true");
});
await waitFor(() => {
expect(mockFetchOrgTree).toHaveBeenCalledWith(projectId);
});
});
it("renders org chart nodes and opens detail view when clicking a node", async () => {
mockFetchOrgTree.mockResolvedValue(orgTree);
render(<AgentsView addToast={mockAddToast} />);
fireEvent.click(screen.getByRole("button", { name: "Org Chart view" }));
await waitFor(() => {
expect(screen.getByText("Chief Agent")).toBeTruthy();
expect(screen.getByText("Director One")).toBeTruthy();
expect(screen.getByText("Manager Alpha")).toBeTruthy();
expect(screen.getByText("Independent Lead")).toBeTruthy();
expect(screen.getAllByText(/Healthy|Idle|Paused|Unresponsive|Agent stalled/).length).toBeGreaterThan(0);
});
fireEvent.click(screen.getByText("Director One"));
await waitFor(() => {
expect(screen.getByTestId("agent-detail-view")).toHaveTextContent("agent-child-1");
});
});
it("shows org chart empty state when API returns no nodes", async () => {
mockFetchOrgTree.mockResolvedValue([]);
render(<AgentsView addToast={mockAddToast} />);
fireEvent.click(screen.getByRole("button", { name: "Org Chart view" }));
await waitFor(() => {
expect(screen.getByText("No agents found")).toBeTruthy();
expect(screen.getByText("Create an agent to get started")).toBeTruthy();
});
});
it("shows loading state while org chart request is in flight", async () => {
let resolveOrgTree: ((value: OrgTreeNode[]) => void) | undefined;
mockFetchOrgTree.mockImplementation(
() =>
new Promise<OrgTreeNode[]>((resolve) => {
resolveOrgTree = resolve;
}),
);
render(<AgentsView addToast={mockAddToast} />);
fireEvent.click(screen.getByRole("button", { name: "Org Chart view" }));
await waitFor(() => {
expect(screen.getByText("Loading org chart...")).toBeTruthy();
});
resolveOrgTree?.([]);
await waitFor(() => {
expect(screen.queryByText("Loading org chart...")).toBeNull();
});
});
});
describe("filter agents by state", () => {
it("renders the state filter with styled container", async () => {
render(<AgentsView addToast={mockAddToast} />);

View File

@@ -24885,3 +24885,188 @@ body[data-color-theme="terminal"][data-theme="light"]::before {
padding: var(--space-xs);
}
}
/* === FN-1167: Agent Org Chart + Chain of Command === */
.agent-org-chart {
display: flex;
align-items: flex-start;
justify-content: flex-start;
gap: 1.5rem;
padding: 1rem;
overflow-x: auto;
overflow-y: visible;
min-height: 220px;
}
.agent-org-chart__loading,
.chain-of-command-loading {
display: inline-flex;
align-items: center;
gap: 0.5rem;
color: var(--text-secondary);
}
.org-chart-node {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
min-width: 220px;
flex: 0 0 auto;
}
.org-chart-node-card {
width: 100%;
background: var(--surface);
border: 1px solid var(--border-color);
border-radius: 10px;
padding: 0.75rem;
color: var(--text-primary);
cursor: pointer;
transition: border-color 120ms ease, background-color 120ms ease, transform 120ms ease;
position: relative;
}
.org-chart-node-card:hover,
.org-chart-node-card:focus-visible {
border-color: var(--accent);
background: var(--card-hover);
transform: translateY(-1px);
outline: none;
}
.org-chart-node__header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.org-chart-node__icon {
font-size: 1rem;
}
.org-chart-node__name {
font-weight: 600;
line-height: 1.2;
}
.org-chart-node__meta {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
}
.org-chart-node__badge {
display: inline-flex;
align-items: center;
padding: 0.125rem 0.5rem;
border-radius: 999px;
font-size: 0.72rem;
text-transform: capitalize;
}
.org-chart-node__health {
display: inline-flex;
align-items: center;
gap: 0.3rem;
font-size: 0.75rem;
}
.org-chart-node--has-children > .org-chart-node-card::after {
content: "";
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: -16px;
width: 1px;
height: 16px;
background: var(--border-color);
}
.org-chart-children {
position: relative;
display: flex;
align-items: flex-start;
justify-content: center;
gap: 1rem;
padding-top: 1.5rem;
margin-top: 0.25rem;
}
.org-chart-children::before {
content: "";
position: absolute;
top: 0;
left: 24px;
right: 24px;
height: 1px;
background: var(--border-color);
}
.org-chart-children > .org-chart-node::before {
content: "";
position: absolute;
top: -24px;
left: 50%;
transform: translateX(-50%);
width: 1px;
height: 24px;
background: var(--border-color);
}
.chain-of-command-path {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.35rem 0.5rem;
}
.chain-of-command-item {
display: inline-flex;
align-items: center;
gap: 0.4rem;
}
.chain-of-command-node {
display: inline-flex;
align-items: center;
padding: 0.25rem 0.6rem;
border-radius: 999px;
border: 1px solid var(--border-color);
background: var(--surface);
color: var(--text-secondary);
font-size: 0.78rem;
cursor: pointer;
}
.chain-of-command-node:hover:not(:disabled) {
border-color: var(--accent);
color: var(--text-primary);
}
.chain-of-command-node:disabled {
cursor: default;
opacity: 0.9;
}
.chain-of-command-node--current {
border-color: var(--accent);
color: var(--text-primary);
font-weight: 600;
}
.chain-of-command-separator {
color: var(--text-muted);
}
@media (max-width: 768px) {
.agent-org-chart {
padding-bottom: 1.5rem;
}
.org-chart-node {
min-width: 200px;
}
}