feat(FN-3224): update agents layout documentation
The merge lands a documentation update to `docs/agents.md`, correcting or refining the agents layout section (2 lines changed). No code or behavior changes. Fusion-Task-Id: FN-3224
This commit is contained in:
@@ -86,10 +86,10 @@ These fields can only be set during update (not on create):
|
|||||||
|
|
||||||
The agents surface provides:
|
The agents surface provides:
|
||||||
|
|
||||||
- Agent-first list/board/tree/org collection (primary content appears first)
|
- Agent-first list/board/tree/org collection in the left pane (primary content appears first)
|
||||||
|
- A cross-pane **Overview** strip above the split layout with summary metrics and a disclosure to expand active/running live cards
|
||||||
- A compact **Controls** popup for secondary actions (state filter, Show system agents toggle, Import, and global Heartbeat Speed)
|
- A compact **Controls** popup for secondary actions (state filter, Show system agents toggle, Import, and global Heartbeat Speed)
|
||||||
- Detail/config panels
|
- Detail/config panels
|
||||||
- Runtime metrics and active-agent live cards rendered below the main collection
|
|
||||||
- A per-agent **Token Usage** panel that summarizes cumulative token consumption for the currently displayed agents
|
- A per-agent **Token Usage** panel that summarizes cumulative token consumption for the currently displayed agents
|
||||||
- Run history
|
- Run history
|
||||||
- Task assignment context
|
- Task assignment context
|
||||||
|
|||||||
@@ -83,6 +83,9 @@ describe("Agent CSS classes", () => {
|
|||||||
expect(hasClass(".agents-view-controls")).toBe(true);
|
expect(hasClass(".agents-view-controls")).toBe(true);
|
||||||
expect(hasClass(".agents-view-primary-actions")).toBe(true);
|
expect(hasClass(".agents-view-primary-actions")).toBe(true);
|
||||||
expect(hasClass(".agents-view-content")).toBe(true);
|
expect(hasClass(".agents-view-content")).toBe(true);
|
||||||
|
expect(hasClass(".agents-overview-bar")).toBe(true);
|
||||||
|
expect(hasClass(".agents-overview-bar__toggle")).toBe(true);
|
||||||
|
expect(hasClass(".agents-overview-bar__content")).toBe(true);
|
||||||
expect(hasClass(".agent-controls-trigger")).toBe(true);
|
expect(hasClass(".agent-controls-trigger")).toBe(true);
|
||||||
expect(hasClass(".agent-controls-trigger--active")).toBe(true);
|
expect(hasClass(".agent-controls-trigger--active")).toBe(true);
|
||||||
expect(hasClass(".agent-controls-panel")).toBe(true);
|
expect(hasClass(".agent-controls-panel")).toBe(true);
|
||||||
@@ -142,6 +145,17 @@ describe("Agent CSS classes", () => {
|
|||||||
expect(hasClass(".spin")).toBe(true);
|
expect(hasClass(".spin")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps split layout as the scroll-constrained pane container", () => {
|
||||||
|
const splitLayout = extractRuleBlock(".agents-split-layout");
|
||||||
|
expect(splitLayout).toContain("flex: 1");
|
||||||
|
expect(splitLayout).toContain("min-height: 0");
|
||||||
|
expect(splitLayout).not.toContain("height: 100%");
|
||||||
|
|
||||||
|
const viewContent = extractRuleBlock(".agents-view-content");
|
||||||
|
expect(viewContent).toContain("overflow-y: auto");
|
||||||
|
expect(viewContent).toContain("min-height: 0");
|
||||||
|
});
|
||||||
|
|
||||||
it("should visually group the filter controls", () => {
|
it("should visually group the filter controls", () => {
|
||||||
const filtersBlock = extractRuleBlock(".agent-controls-filters");
|
const filtersBlock = extractRuleBlock(".agent-controls-filters");
|
||||||
expect(filtersBlock).toContain("padding: var(--space-xs) var(--space-sm)");
|
expect(filtersBlock).toContain("padding: var(--space-xs) var(--space-sm)");
|
||||||
@@ -310,7 +324,7 @@ describe("Agent CSS classes", () => {
|
|||||||
expect(hasClass(".live-agent-card")).toBe(true);
|
expect(hasClass(".live-agent-card")).toBe(true);
|
||||||
expect(hasClass(".live-agent-card-header")).toBe(true);
|
expect(hasClass(".live-agent-card-header")).toBe(true);
|
||||||
expect(hasClass(".live-agent-card-name")).toBe(true);
|
expect(hasClass(".live-agent-card-name")).toBe(true);
|
||||||
expect(hasClass(".live-agent-pulse")).toBe(true);
|
expect(hasClass(".status-dot")).toBe(true);
|
||||||
expect(hasClass(".live-agent-task")).toBe(true);
|
expect(hasClass(".live-agent-task")).toBe(true);
|
||||||
expect(hasClass(".live-agent-card-transcript")).toBe(true);
|
expect(hasClass(".live-agent-card-transcript")).toBe(true);
|
||||||
expect(hasClass(".live-agent-card-empty")).toBe(true);
|
expect(hasClass(".live-agent-card-empty")).toBe(true);
|
||||||
|
|||||||
@@ -57,14 +57,6 @@
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.live-agent-pulse {
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
background: var(--color-success);
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: pulse 1.5s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.live-agent-task {
|
.live-agent-task {
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,7 +104,10 @@ function LiveAgentCard({ agent, projectId, onSelect, onOpenTaskLogs }: LiveAgent
|
|||||||
>
|
>
|
||||||
<div className="live-agent-card-header">
|
<div className="live-agent-card-header">
|
||||||
<div className="live-agent-card-name">
|
<div className="live-agent-card-name">
|
||||||
<span className="live-agent-pulse" />
|
<span
|
||||||
|
className={`status-dot ${agent.state === "running" ? "status-dot--pending" : "status-dot--online"}`}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
<span>{agent.name}</span>
|
<span>{agent.name}</span>
|
||||||
</div>
|
</div>
|
||||||
{agent.taskId && (
|
{agent.taskId && (
|
||||||
@@ -189,9 +192,10 @@ interface ActiveAgentsPanelProps {
|
|||||||
projectId?: string;
|
projectId?: string;
|
||||||
onAgentSelect?: (agentId: string) => void;
|
onAgentSelect?: (agentId: string) => void;
|
||||||
onOpenTaskLogs?: (taskId: string) => void;
|
onOpenTaskLogs?: (taskId: string) => void;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ActiveAgentsPanel({ agents, projectId, onAgentSelect, onOpenTaskLogs }: ActiveAgentsPanelProps) {
|
export function ActiveAgentsPanel({ agents, projectId, onAgentSelect, onOpenTaskLogs, className = "" }: ActiveAgentsPanelProps) {
|
||||||
// Dedupe by id defensively. The store should return unique agents but a race
|
// Dedupe by id defensively. The store should return unique agents but a race
|
||||||
// between the initial fetch and an SSE refresh can briefly surface the same
|
// between the initial fetch and an SSE refresh can briefly surface the same
|
||||||
// agent twice — without this guard React floods the console with duplicate
|
// agent twice — without this guard React floods the console with duplicate
|
||||||
@@ -201,7 +205,7 @@ export function ActiveAgentsPanel({ agents, projectId, onAgentSelect, onOpenTask
|
|||||||
if (uniqueAgents.length === 0) return null;
|
if (uniqueAgents.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="active-agents-panel">
|
<div className={`active-agents-panel ${className}`.trim()}>
|
||||||
<div className="active-agents-panel-header">
|
<div className="active-agents-panel-header">
|
||||||
<Activity size={16} />
|
<Activity size={16} />
|
||||||
<span>Active Agents ({uniqueAgents.length})</span>
|
<span>Active Agents ({uniqueAgents.length})</span>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { AgentStats } from "../api";
|
|||||||
|
|
||||||
interface AgentMetricsBarProps {
|
interface AgentMetricsBarProps {
|
||||||
stats: AgentStats | null;
|
stats: AgentStats | null;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const METRIC_CARDS = [
|
const METRIC_CARDS = [
|
||||||
@@ -12,11 +13,11 @@ const METRIC_CARDS = [
|
|||||||
{ icon: Zap, label: "Total Runs", valueKey: "completedRuns", className: "agent-metric-card--runs" },
|
{ icon: Zap, label: "Total Runs", valueKey: "completedRuns", className: "agent-metric-card--runs" },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export function AgentMetricsBar({ stats }: AgentMetricsBarProps) {
|
export function AgentMetricsBar({ stats, className = "" }: AgentMetricsBarProps) {
|
||||||
if (!stats) return null;
|
if (!stats) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="agent-metrics-bar">
|
<div className={`agent-metrics-bar ${className}`.trim()}>
|
||||||
{METRIC_CARDS.map((card) => {
|
{METRIC_CARDS.map((card) => {
|
||||||
const value = card.valueKey === "successRate"
|
const value = card.valueKey === "successRate"
|
||||||
? `${Math.round(stats.successRate * 100)}%`
|
? `${Math.round(stats.successRate * 100)}%`
|
||||||
|
|||||||
71
packages/dashboard/app/components/AgentsOverviewBar.css
Normal file
71
packages/dashboard/app/components/AgentsOverviewBar.css
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
.agents-overview-bar {
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
background: var(--surface);
|
||||||
|
padding: var(--space-md) var(--space-lg);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agents-overview-bar__toggle {
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: var(--card);
|
||||||
|
color: var(--text);
|
||||||
|
padding: var(--space-sm) var(--space-md);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background var(--transition-fast), border-color var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agents-overview-bar__toggle:hover {
|
||||||
|
background: var(--card-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agents-overview-bar__toggle:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: var(--focus-ring-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agents-overview-bar__title-wrap {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agents-overview-bar__title {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agents-overview-bar__meta {
|
||||||
|
font-size: calc(var(--space-sm) + var(--space-xs));
|
||||||
|
}
|
||||||
|
|
||||||
|
.agents-overview-bar__content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agents-overview-bar__metrics {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agents-overview-bar__active-panel {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.agents-overview-bar {
|
||||||
|
padding: var(--space-sm) var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agents-overview-bar__toggle {
|
||||||
|
min-height: calc(var(--space-2xl) + var(--space-sm));
|
||||||
|
}
|
||||||
|
}
|
||||||
56
packages/dashboard/app/components/AgentsOverviewBar.tsx
Normal file
56
packages/dashboard/app/components/AgentsOverviewBar.tsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { ChevronDown, ChevronRight } from "lucide-react";
|
||||||
|
import { AgentMetricsBar } from "./AgentMetricsBar";
|
||||||
|
import { ActiveAgentsPanel } from "./ActiveAgentsPanel";
|
||||||
|
import type { Agent, AgentStats } from "../api";
|
||||||
|
import "./AgentsOverviewBar.css";
|
||||||
|
|
||||||
|
interface AgentsOverviewBarProps {
|
||||||
|
stats: AgentStats | null;
|
||||||
|
activeAgents: Agent[];
|
||||||
|
projectId?: string;
|
||||||
|
isOpen: boolean;
|
||||||
|
onToggle: () => void;
|
||||||
|
onSelectAgent?: (agentId: string) => void;
|
||||||
|
onOpenTaskLogs?: (taskId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AgentsOverviewBar({
|
||||||
|
stats,
|
||||||
|
activeAgents,
|
||||||
|
projectId,
|
||||||
|
isOpen,
|
||||||
|
onToggle,
|
||||||
|
onSelectAgent,
|
||||||
|
onOpenTaskLogs,
|
||||||
|
}: AgentsOverviewBarProps) {
|
||||||
|
return (
|
||||||
|
<section className="agents-overview-bar" aria-label="Agents overview">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="agents-overview-bar__toggle"
|
||||||
|
aria-expanded={isOpen}
|
||||||
|
onClick={onToggle}
|
||||||
|
>
|
||||||
|
<span className="agents-overview-bar__title-wrap">
|
||||||
|
{isOpen ? <ChevronDown size={16} aria-hidden="true" /> : <ChevronRight size={16} aria-hidden="true" />}
|
||||||
|
<span className="agents-overview-bar__title">Overview</span>
|
||||||
|
</span>
|
||||||
|
<span className="agents-overview-bar__meta text-secondary">
|
||||||
|
{stats?.activeCount ?? 0} active · {activeAgents.length} running
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
{isOpen ? (
|
||||||
|
<div className="agents-overview-bar__content">
|
||||||
|
<AgentMetricsBar stats={stats} className="agents-overview-bar__metrics" />
|
||||||
|
<ActiveAgentsPanel
|
||||||
|
agents={activeAgents}
|
||||||
|
projectId={projectId}
|
||||||
|
onAgentSelect={onSelectAgent}
|
||||||
|
onOpenTaskLogs={onOpenTaskLogs}
|
||||||
|
className="agents-overview-bar__active-panel"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -137,7 +137,7 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(calc(var(--space-xl) * 11 + var(--space-xs)), calc(var(--space-xl) * 13 + var(--space-lg))) minmax(0, 1fr);
|
grid-template-columns: minmax(calc(var(--space-xl) * 11 + var(--space-xs)), calc(var(--space-xl) * 13 + var(--space-lg))) minmax(0, 1fr);
|
||||||
gap: 0;
|
gap: 0;
|
||||||
height: 100%;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,6 +164,7 @@
|
|||||||
padding: calc(var(--space-lg) + var(--space-xs));
|
padding: calc(var(--space-lg) + var(--space-xs));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.agents-detail-empty-state {
|
.agents-detail-empty-state {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|||||||
@@ -5,9 +5,8 @@ import type { Agent, AgentCapability, AgentOnboardingSummary, AgentState, OrgTre
|
|||||||
import { updateAgent, updateAgentState, deleteAgent, startAgentRun, fetchOrgTree, fetchSettings, updateSettings } from "../api";
|
import { updateAgent, updateAgentState, deleteAgent, startAgentRun, fetchOrgTree, fetchSettings, updateSettings } from "../api";
|
||||||
|
|
||||||
const AgentDetailView = lazy(() => import("./AgentDetailView").then((m) => ({ default: m.AgentDetailView })));
|
const AgentDetailView = lazy(() => import("./AgentDetailView").then((m) => ({ default: m.AgentDetailView })));
|
||||||
import { ActiveAgentsPanel } from "./ActiveAgentsPanel";
|
|
||||||
import { AgentMetricsBar } from "./AgentMetricsBar";
|
|
||||||
import { AgentTokenStatsPanel } from "./AgentTokenStatsPanel";
|
import { AgentTokenStatsPanel } from "./AgentTokenStatsPanel";
|
||||||
|
import { AgentsOverviewBar } from "./AgentsOverviewBar";
|
||||||
import { AgentEmptyState } from "./AgentEmptyState";
|
import { AgentEmptyState } from "./AgentEmptyState";
|
||||||
import { useAgents } from "../hooks/useAgents";
|
import { useAgents } from "../hooks/useAgents";
|
||||||
import { useConfirm } from "../hooks/useConfirm";
|
import { useConfirm } from "../hooks/useConfirm";
|
||||||
@@ -214,6 +213,7 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
|
|||||||
const [orgTree, setOrgTree] = useState<OrgTreeNode[]>([]);
|
const [orgTree, setOrgTree] = useState<OrgTreeNode[]>([]);
|
||||||
const [isOrgTreeLoading, setIsOrgTreeLoading] = useState(false);
|
const [isOrgTreeLoading, setIsOrgTreeLoading] = useState(false);
|
||||||
const [isControlsPanelOpen, setIsControlsPanelOpen] = useState(false);
|
const [isControlsPanelOpen, setIsControlsPanelOpen] = useState(false);
|
||||||
|
const [isOverviewOpen, setIsOverviewOpen] = useState(false);
|
||||||
const controlsPanelRef = useRef<HTMLDivElement>(null);
|
const controlsPanelRef = useRef<HTMLDivElement>(null);
|
||||||
const { confirm } = useConfirm();
|
const { confirm } = useConfirm();
|
||||||
const controlsTriggerRef = useRef<HTMLButtonElement>(null);
|
const controlsTriggerRef = useRef<HTMLButtonElement>(null);
|
||||||
@@ -849,13 +849,19 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<AgentsOverviewBar
|
||||||
|
stats={stats}
|
||||||
|
activeAgents={displayActiveAgents}
|
||||||
|
projectId={projectId}
|
||||||
|
isOpen={isOverviewOpen}
|
||||||
|
onToggle={() => setIsOverviewOpen((open) => !open)}
|
||||||
|
onSelectAgent={setSelectedAgentId}
|
||||||
|
onOpenTaskLogs={onOpenTaskLogs}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="agents-split-layout">
|
<div className="agents-split-layout">
|
||||||
<div className={`agents-split-sidebar${isMobileDetailOpen ? " agents-split-sidebar--hidden-mobile" : ""}`}>
|
<div className={`agents-split-sidebar${isMobileDetailOpen ? " agents-split-sidebar--hidden-mobile" : ""}`}>
|
||||||
<div className="agents-view-content">
|
<div className="agents-view-content">
|
||||||
<AgentMetricsBar stats={stats} />
|
|
||||||
|
|
||||||
<ActiveAgentsPanel agents={displayActiveAgents} projectId={projectId} onAgentSelect={setSelectedAgentId} onOpenTaskLogs={onOpenTaskLogs} />
|
|
||||||
|
|
||||||
<NewAgentDialog
|
<NewAgentDialog
|
||||||
isOpen={isCreating}
|
isOpen={isCreating}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
|
|||||||
@@ -147,6 +147,17 @@ describe("AgentsView", () => {
|
|||||||
return trigger;
|
return trigger;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openOverviewPanel = async () => {
|
||||||
|
const toggle = await screen.findByRole("button", { name: /Overview/i });
|
||||||
|
if (toggle.getAttribute("aria-expanded") !== "true") {
|
||||||
|
fireEvent.click(toggle);
|
||||||
|
}
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(toggle.getAttribute("aria-expanded")).toBe("true");
|
||||||
|
});
|
||||||
|
return toggle;
|
||||||
|
};
|
||||||
|
|
||||||
describe("rendering", () => {
|
describe("rendering", () => {
|
||||||
it("renders the agents view header", async () => {
|
it("renders the agents view header", async () => {
|
||||||
render(<AgentsView addToast={mockAddToast} />);
|
render(<AgentsView addToast={mockAddToast} />);
|
||||||
@@ -186,14 +197,20 @@ describe("AgentsView", () => {
|
|||||||
expect(screen.getByText("review")).toHaveAttribute("title", "auto::skills/../../.agents/skills/review/SKILL.md");
|
expect(screen.getByText("review")).toHaveAttribute("title", "auto::skills/../../.agents/skills/review/SKILL.md");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders split layout with sidebar and detail pane", async () => {
|
it("renders cross-pane overview above split layout", async () => {
|
||||||
const { container } = render(<AgentsView addToast={mockAddToast} />);
|
const { container } = render(<AgentsView addToast={mockAddToast} />);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
|
expect(container.querySelector(".agents-overview-bar")).toBeTruthy();
|
||||||
expect(container.querySelector(".agents-split-layout")).toBeTruthy();
|
expect(container.querySelector(".agents-split-layout")).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(container.querySelector(".agents-split-sidebar")).toBeTruthy();
|
const overview = container.querySelector(".agents-overview-bar");
|
||||||
|
const splitLayout = container.querySelector(".agents-split-layout");
|
||||||
|
expect(overview?.nextElementSibling).toBe(splitLayout);
|
||||||
|
const sidebar = container.querySelector(".agents-split-sidebar");
|
||||||
|
expect(sidebar).toBeTruthy();
|
||||||
|
expect(sidebar?.querySelector(".agents-overview-bar")).toBeNull();
|
||||||
expect(container.querySelector(".agents-split-detail")).toBeTruthy();
|
expect(container.querySelector(".agents-split-detail")).toBeTruthy();
|
||||||
expect(screen.getByText("Select an agent")).toBeInTheDocument();
|
expect(screen.getByText("Select an agent")).toBeInTheDocument();
|
||||||
expect(screen.getByText("Choose an agent from the sidebar to view details")).toBeInTheDocument();
|
expect(screen.getByText("Choose an agent from the sidebar to view details")).toBeInTheDocument();
|
||||||
@@ -422,24 +439,25 @@ describe("AgentsView", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders metrics, then active panel, then the main collection", async () => {
|
it("keeps metrics and active agents collapsed behind overview disclosure by default", async () => {
|
||||||
const { container } = render(<AgentsView addToast={mockAddToast} />);
|
const { container } = render(<AgentsView addToast={mockAddToast} />);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(container.querySelector(".agent-list")).toBeTruthy();
|
expect(container.querySelector(".agent-list")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
const overviewToggle = screen.getByRole("button", { name: /Overview/i });
|
||||||
|
expect(overviewToggle.getAttribute("aria-expanded")).toBe("false");
|
||||||
|
expect(container.querySelector(".agent-metrics-bar")).toBeNull();
|
||||||
|
expect(container.querySelector(".active-agents-panel")).toBeNull();
|
||||||
|
|
||||||
|
fireEvent.click(overviewToggle);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(overviewToggle.getAttribute("aria-expanded")).toBe("true");
|
||||||
expect(container.querySelector(".agent-metrics-bar")).toBeTruthy();
|
expect(container.querySelector(".agent-metrics-bar")).toBeTruthy();
|
||||||
expect(container.querySelector(".active-agents-panel")).toBeTruthy();
|
expect(container.querySelector(".active-agents-panel")).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
const list = container.querySelector(".agent-list");
|
|
||||||
const metrics = container.querySelector(".agent-metrics-bar");
|
|
||||||
const activePanel = container.querySelector(".active-agents-panel");
|
|
||||||
expect(list && metrics && activePanel).toBeTruthy();
|
|
||||||
// Metrics bar sits at the very top of the view content.
|
|
||||||
expect(metrics!.compareDocumentPosition(activePanel!) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
|
||||||
// Active agents panel sits between metrics and the main agent list,
|
|
||||||
// so live work is visible without scrolling past the full directory.
|
|
||||||
expect(activePanel!.compareDocumentPosition(list!) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("fetches agents only once on mount (regression: no duplicate initial load path)", async () => {
|
it("fetches agents only once on mount (regression: no duplicate initial load path)", async () => {
|
||||||
@@ -451,6 +469,7 @@ describe("AgentsView", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Ensure the single-load path still powers dependent UI sections.
|
// Ensure the single-load path still powers dependent UI sections.
|
||||||
|
await openOverviewPanel();
|
||||||
expect(await screen.findByText("Active Agents (1)")).toBeTruthy();
|
expect(await screen.findByText("Active Agents (1)")).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1833,6 +1852,7 @@ describe("AgentsView", () => {
|
|||||||
it("renders active agents panel when agents are active", async () => {
|
it("renders active agents panel when agents are active", async () => {
|
||||||
// agent-002 is active with taskId FN-001
|
// agent-002 is active with taskId FN-001
|
||||||
render(<AgentsView addToast={mockAddToast} />);
|
render(<AgentsView addToast={mockAddToast} />);
|
||||||
|
await openOverviewPanel();
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText("Active Agents (1)")).toBeTruthy();
|
expect(screen.getByText("Active Agents (1)")).toBeTruthy();
|
||||||
@@ -1845,6 +1865,7 @@ describe("AgentsView", () => {
|
|||||||
|
|
||||||
it("opens AgentDetailView when clicking an active agent card", async () => {
|
it("opens AgentDetailView when clicking an active agent card", async () => {
|
||||||
render(<AgentsView addToast={mockAddToast} />);
|
render(<AgentsView addToast={mockAddToast} />);
|
||||||
|
await openOverviewPanel();
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText("Active Agents (1)")).toBeTruthy();
|
expect(screen.getByText("Active Agents (1)")).toBeTruthy();
|
||||||
@@ -1864,6 +1885,7 @@ describe("AgentsView", () => {
|
|||||||
|
|
||||||
it("opens AgentDetailView when pressing Enter on an active agent card", async () => {
|
it("opens AgentDetailView when pressing Enter on an active agent card", async () => {
|
||||||
render(<AgentsView addToast={mockAddToast} />);
|
render(<AgentsView addToast={mockAddToast} />);
|
||||||
|
await openOverviewPanel();
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText("Active Agents (1)")).toBeTruthy();
|
expect(screen.getByText("Active Agents (1)")).toBeTruthy();
|
||||||
@@ -1884,6 +1906,7 @@ describe("AgentsView", () => {
|
|||||||
|
|
||||||
it("opens AgentDetailView when pressing Space on an active agent card", async () => {
|
it("opens AgentDetailView when pressing Space on an active agent card", async () => {
|
||||||
render(<AgentsView addToast={mockAddToast} />);
|
render(<AgentsView addToast={mockAddToast} />);
|
||||||
|
await openOverviewPanel();
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText("Active Agents (1)")).toBeTruthy();
|
expect(screen.getByText("Active Agents (1)")).toBeTruthy();
|
||||||
@@ -1904,6 +1927,7 @@ describe("AgentsView", () => {
|
|||||||
|
|
||||||
it("live agent cards have proper accessibility attributes", async () => {
|
it("live agent cards have proper accessibility attributes", async () => {
|
||||||
render(<AgentsView addToast={mockAddToast} />);
|
render(<AgentsView addToast={mockAddToast} />);
|
||||||
|
await openOverviewPanel();
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText("Active Agents (1)")).toBeTruthy();
|
expect(screen.getByText("Active Agents (1)")).toBeTruthy();
|
||||||
@@ -1934,9 +1958,10 @@ describe("AgentsView", () => {
|
|||||||
mockFetchAgents.mockResolvedValue(inactiveAgents);
|
mockFetchAgents.mockResolvedValue(inactiveAgents);
|
||||||
|
|
||||||
render(<AgentsView addToast={mockAddToast} />);
|
render(<AgentsView addToast={mockAddToast} />);
|
||||||
|
await openOverviewPanel();
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.queryByText("Active Agents")).toBeNull();
|
expect(screen.queryByText(/^Active Agents \(/)).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1959,6 +1984,7 @@ describe("AgentsView", () => {
|
|||||||
mockFetchAgents.mockResolvedValue(spawnedAgents);
|
mockFetchAgents.mockResolvedValue(spawnedAgents);
|
||||||
|
|
||||||
render(<AgentsView addToast={mockAddToast} />);
|
render(<AgentsView addToast={mockAddToast} />);
|
||||||
|
await openOverviewPanel();
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText("Active Agents (2)")).toBeTruthy();
|
expect(screen.getByText("Active Agents (2)")).toBeTruthy();
|
||||||
|
|||||||
Reference in New Issue
Block a user