fix(FN-2204): restore tokenized styling in agents views

- Replace inline agent state badge and card colors with state-specific CSS classes across list, board, tree, and org chart views
- Add reusable AgentEmptyState with create-agent CTA and wire it into all empty agent view modes
- Polish agents controls and metrics presentation in AgentsView/AgentMetricsBar with corresponding stylesheet token updates
- Extend dashboard tests for agent CSS classes, metrics bar behavior, list view rendering, and mobile view coverage
- Add a patch changeset for @gsxdsm/fusion describing the agent view UX improvements
This commit is contained in:
Fusion
2026-04-22 09:56:26 -07:00
committed by gsxdsm
parent 9fa1ddb006
commit 1fc72d15d8
10 changed files with 461 additions and 155 deletions

View File

@@ -0,0 +1,5 @@
---
"@gsxdsm/fusion": patch
---
Improve the dashboard agents list views with shared empty-state actions, token-based state styling, and clearer board/tree/org-chart presentation.

11
.changeset/config.json Normal file
View File

@@ -0,0 +1,11 @@
{
"$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}

View File

@@ -7,15 +7,28 @@ const stylesContent = fs.readFileSync(stylesPath, "utf-8");
// Agent component file paths to verify inline <style> blocks are removed
const agentsViewContent = fs.readFileSync(path.join(__dirname, "../components/AgentsView.tsx"), "utf-8");
const agentEmptyStateContent = fs.readFileSync(path.join(__dirname, "../components/AgentEmptyState.tsx"), "utf-8");
const agentDetailViewContent = fs.readFileSync(path.join(__dirname, "../components/AgentDetailView.tsx"), "utf-8");
const activeAgentsPanelContent = fs.readFileSync(path.join(__dirname, "../components/ActiveAgentsPanel.tsx"), "utf-8");
const newAgentDialogContent = fs.readFileSync(path.join(__dirname, "../components/NewAgentDialog.tsx"), "utf-8");
/** Check that styles.css contains a CSS class definition for the given selector */
function extractRuleBlock(selector: string): string {
const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const match = stylesContent.match(new RegExp(`${escaped}\\s*\\{([^}]*)\\}`));
return match?.[1] ?? "";
}
function hasClass(cls: string): boolean {
const escaped = cls.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
// Match standalone class or class within a grouped selector list
return new RegExp(`${escaped}\\s*(,|\\{)`).test(stylesContent);
// Match standalone class, grouped selector, or compound selector (e.g. `.foo svg`)
return new RegExp(`${escaped}(?=[\\s,{:.#>+~])`).test(stylesContent);
}
function extractSection(startMarker: string, endMarker: string): string {
const start = stylesContent.indexOf(startMarker);
const end = stylesContent.indexOf(endMarker, start + startMarker.length);
return start >= 0 && end >= 0 ? stylesContent.slice(start, end) : "";
}
describe("Agent CSS classes", () => {
@@ -53,6 +66,10 @@ describe("Agent CSS classes", () => {
it("should define AgentMetricsBar CSS classes", () => {
expect(hasClass(".agent-metrics-bar")).toBe(true);
expect(hasClass(".agent-metric-card")).toBe(true);
expect(hasClass(".agent-metric-card--active")).toBe(true);
expect(hasClass(".agent-metric-card--tasks")).toBe(true);
expect(hasClass(".agent-metric-card--success")).toBe(true);
expect(hasClass(".agent-metric-card--runs")).toBe(true);
expect(hasClass(".agent-metric-info")).toBe(true);
expect(hasClass(".agent-metric-value")).toBe(true);
expect(hasClass(".agent-metric-label")).toBe(true);
@@ -66,13 +83,26 @@ describe("Agent CSS classes", () => {
expect(hasClass(".agents-view-controls")).toBe(true);
expect(hasClass(".agents-view-content")).toBe(true);
expect(hasClass(".agent-controls")).toBe(true);
expect(hasClass(".agent-controls-filters")).toBe(true);
expect(hasClass(".agent-state-filter")).toBe(true);
expect(hasClass(".agent-state-filter-select")).toBe(true);
expect(hasClass(".agent-board")).toBe(true);
expect(hasClass(".agent-board-card")).toBe(true);
expect(hasClass(".agent-board-card--idle")).toBe(true);
expect(hasClass(".agent-board-card--active")).toBe(true);
expect(hasClass(".agent-board-card--running")).toBe(true);
expect(hasClass(".agent-board-card--paused")).toBe(true);
expect(hasClass(".agent-board-card--error")).toBe(true);
expect(hasClass(".agent-board-card--terminated")).toBe(true);
expect(hasClass(".agent-board-header")).toBe(true);
expect(hasClass(".agent-board-icon")).toBe(true);
expect(hasClass(".agent-board-badge")).toBe(true);
expect(hasClass(".agent-badge--idle")).toBe(true);
expect(hasClass(".agent-badge--active")).toBe(true);
expect(hasClass(".agent-badge--running")).toBe(true);
expect(hasClass(".agent-badge--paused")).toBe(true);
expect(hasClass(".agent-badge--error")).toBe(true);
expect(hasClass(".agent-badge--terminated")).toBe(true);
expect(hasClass(".agent-board-health")).toBe(true);
expect(hasClass(".agent-board-name")).toBe(true);
expect(hasClass(".agent-board-id")).toBe(true);
@@ -80,6 +110,12 @@ describe("Agent CSS classes", () => {
expect(hasClass(".agent-board-actions")).toBe(true);
expect(hasClass(".agent-list")).toBe(true);
expect(hasClass(".agent-card")).toBe(true);
expect(hasClass(".agent-card--idle")).toBe(true);
expect(hasClass(".agent-card--active")).toBe(true);
expect(hasClass(".agent-card--running")).toBe(true);
expect(hasClass(".agent-card--paused")).toBe(true);
expect(hasClass(".agent-card--error")).toBe(true);
expect(hasClass(".agent-card--terminated")).toBe(true);
expect(hasClass(".agent-card-header")).toBe(true);
expect(hasClass(".agent-card-body")).toBe(true);
expect(hasClass(".agent-card-actions")).toBe(true);
@@ -99,6 +135,48 @@ describe("Agent CSS classes", () => {
expect(hasClass(".spin")).toBe(true);
});
it("should visually group the filter controls", () => {
const filtersBlock = extractRuleBlock(".agent-controls-filters");
expect(filtersBlock).toContain("padding: var(--space-xs) var(--space-sm)");
expect(filtersBlock).toContain("background: var(--surface)");
expect(filtersBlock).toContain("border: 1px solid var(--border)");
expect(filtersBlock).toContain("border-radius: var(--radius-md)");
});
it("should use design tokens for the updated tree view spacing and transitions", () => {
const treeViewBlock = extractRuleBlock(".agent-tree__view");
expect(treeViewBlock).toContain("gap: var(--space-xs)");
expect(treeViewBlock).toContain("padding: var(--space-sm) 0");
const treeNodeBlock = extractRuleBlock(".agent-tree__node");
expect(treeNodeBlock).toContain("gap: var(--space-sm)");
expect(treeNodeBlock).toContain("padding: var(--space-sm) var(--space-md)");
expect(treeNodeBlock).toContain("border-radius: var(--radius-sm)");
expect(treeNodeBlock).toContain("transition: background-color var(--transition-fast)");
expect(treeNodeBlock).not.toMatch(/gap:\s*4px|padding:\s*8px 12px|border-radius:\s*4px|0\.15s\s+ease/);
expect(extractRuleBlock(".agent-tree__indent--1")).toContain("padding-left: var(--space-xl)");
expect(extractRuleBlock(".agent-tree__indent--2")).toContain("padding-left: calc(var(--space-xl) * 2)");
expect(extractRuleBlock(".agent-tree__indent--3")).toContain("padding-left: calc(var(--space-xl) * 3)");
expect(extractRuleBlock(".agent-tree__indent--4")).toContain("padding-left: calc(var(--space-xl) * 4)");
});
it("should use dashboard tokens in the updated org chart styles", () => {
const orgChartSection = extractSection("/* === FN-1167: Agent Org Chart + Chain of Command === */", "/* === Agent Dialog Mobile Responsive === */");
expect(orgChartSection).toContain("gap: var(--space-xl)");
expect(orgChartSection).toContain("padding: var(--space-lg)");
expect(orgChartSection).toContain("min-height: calc(var(--space-xl) * 9 + var(--space-xs))");
expect(orgChartSection).toContain("border: 1px solid var(--border)");
expect(orgChartSection).toContain("color: var(--text)");
expect(orgChartSection).toContain("color: var(--text-muted)");
expect(orgChartSection).toContain("border-radius: var(--radius-pill)");
expect(orgChartSection).toContain("transition: border-color var(--transition-fast), background-color var(--transition-fast), transform var(--transition-fast)");
expect(orgChartSection).not.toContain("var(--border-color)");
expect(orgChartSection).not.toContain("var(--text-primary)");
expect(orgChartSection).not.toContain("var(--text-secondary)");
expect(orgChartSection).not.toMatch(/1\.5rem|0\.75rem|0\.72rem|0\.78rem|0\.65rem|120ms\s+ease|220px|10px/);
});
// Verify AgentDetailView classes
it("should define AgentDetailView CSS classes", () => {
expect(hasClass(".agent-detail-overlay")).toBe(true);
@@ -211,11 +289,23 @@ describe("Agent CSS classes", () => {
expect(hasClass(".agent-dialog-loading")).toBe(true);
});
it("should define shared AgentEmptyState component primitives", () => {
expect(hasClass(".agent-empty-state__icon")).toBe(true);
expect(hasClass(".agent-empty-state__title")).toBe(true);
expect(hasClass(".agent-empty-state__description")).toBe(true);
expect(agentEmptyStateContent).toContain("Create Agent");
});
// Verify no inline <style> blocks remain in agent components
it("should not have inline <style> blocks in AgentsView", () => {
expect(agentsViewContent).not.toContain("<style>");
});
it("should only keep runtime health color inline styles in AgentsView", () => {
const inlineStyleCount = (agentsViewContent.match(/style=\{\{/g) || []).length;
expect(inlineStyleCount).toBe(4);
});
it("should not have inline <style> blocks in AgentDetailView", () => {
expect(agentDetailViewContent).not.toContain("<style>");
});

View File

@@ -0,0 +1,28 @@
import { Bot } from "lucide-react";
interface AgentEmptyStateProps {
title?: string;
description?: string;
ctaLabel?: string;
onCtaClick?: () => void;
}
export function AgentEmptyState({
title = "No agents found",
description = "Create an agent to get started",
ctaLabel = "Create Agent",
onCtaClick,
}: AgentEmptyStateProps) {
return (
<div className="agent-empty">
<Bot size={48} opacity={0.3} className="agent-empty-state__icon" />
<p className="agent-empty-state__title">{title}</p>
<p className="agent-empty-state__description text-secondary">{description}</p>
{onCtaClick ? (
<button type="button" className="btn btn--primary" onClick={onCtaClick}>
{ctaLabel}
</button>
) : null}
</div>
);
}

View File

@@ -5,27 +5,33 @@ interface AgentMetricsBarProps {
stats: AgentStats | null;
}
const METRIC_CARDS = [
{ icon: Activity, label: "Active Agents", valueKey: "activeCount", className: "agent-metric-card--active" },
{ icon: ListTodo, label: "Assigned Tasks", valueKey: "assignedTaskCount", className: "agent-metric-card--tasks" },
{ icon: CheckCircle, label: "Success Rate", valueKey: "successRate", className: "agent-metric-card--success" },
{ icon: Zap, label: "Total Runs", valueKey: "completedRuns", className: "agent-metric-card--runs" },
] as const;
export function AgentMetricsBar({ stats }: AgentMetricsBarProps) {
if (!stats) return null;
const cards = [
{ icon: Activity, label: "Active Agents", value: stats.activeCount, color: "var(--state-active-text)" },
{ icon: ListTodo, label: "Assigned Tasks", value: stats.assignedTaskCount, color: "var(--in-progress)" },
{ icon: CheckCircle, label: "Success Rate", value: `${Math.round(stats.successRate * 100)}%`, color: "var(--color-success, #3fb950)" },
{ icon: Zap, label: "Total Runs", value: stats.completedRuns, color: "var(--in-progress)" },
];
return (
<div className="agent-metrics-bar">
{cards.map(card => (
<div key={card.label} className="agent-metric-card">
<card.icon size={18} style={{ color: card.color }} />
<div className="agent-metric-info">
<span className="agent-metric-value">{card.value}</span>
<span className="agent-metric-label">{card.label}</span>
{METRIC_CARDS.map((card) => {
const value = card.valueKey === "successRate"
? `${Math.round(stats.successRate * 100)}%`
: stats[card.valueKey];
return (
<div key={card.label} className={`agent-metric-card ${card.className}`}>
<card.icon size={18} />
<div className="agent-metric-info">
<span className="agent-metric-value">{value}</span>
<span className="agent-metric-label">{card.label}</span>
</div>
</div>
</div>
))}
);
})}
</div>
);
}

View File

@@ -5,6 +5,7 @@ import { fetchAgents, updateAgent, updateAgentState, deleteAgent, startAgentRun,
import { AgentDetailView } from "./AgentDetailView";
import { ActiveAgentsPanel } from "./ActiveAgentsPanel";
import { AgentMetricsBar } from "./AgentMetricsBar";
import { AgentEmptyState } from "./AgentEmptyState";
import { useAgents } from "../hooks/useAgents";
import { subscribeSse } from "../sse-bus";
import { useAgentHierarchy } from "../hooks/useAgentHierarchy";
@@ -37,14 +38,41 @@ const AGENT_ROLES: { value: AgentCapability; label: string; icon: string }[] = [
];
const STATE_COLORS: Record<AgentState, { bg: string; text: string; border: string }> = {
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)" },
};
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 "terminated":
return "agent-badge--terminated";
case "idle":
default:
return "agent-badge--idle";
}
}
function getStateCardClass(prefix: "agent-board-card" | "agent-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 "terminated":
return `${prefix}--terminated`;
case "idle":
default:
return `${prefix}--idle`;
}
}
/** Recursive tree node component for agent hierarchy */
function AgentTreeNode({
@@ -70,7 +98,7 @@ function AgentTreeNode({
const childCount = getChildCount(agent.id);
const expanded = isExpanded(agent.id);
const health = getHealthStatus(agent);
const stateStyle = STATE_COLORS[agent.state];
const stateBadgeClass = getStateBadgeClass(agent.state);
return (
<>
@@ -99,12 +127,7 @@ function AgentTreeNode({
<span className="agent-tree__icon">{getRoleIcon(agent.role)}</span>
<span className="agent-tree__name">{agent.name}</span>
<span
className="agent-tree__badge"
style={{
background: stateStyle.bg,
color: stateStyle.text,
border: `1px solid ${stateStyle.border}`,
}}
className={`agent-tree__badge ${stateBadgeClass}`}
>
{agent.state}
</span>
@@ -162,7 +185,7 @@ function OrgChartNode({
}) {
const { agent, children } = node;
const health = getHealthStatus(agent);
const stateStyle = STATE_COLORS[agent.state];
const stateBadgeClass = getStateBadgeClass(agent.state);
return (
<div className={`org-chart-node${children.length > 0 ? " org-chart-node--has-children" : ""}`}>
@@ -179,12 +202,7 @@ function OrgChartNode({
</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}`,
}}
className={`org-chart-node__badge ${stateBadgeClass}`}
>
{agent.state}
</span>
@@ -539,7 +557,8 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
<div className="agents-view-content">
{/* Filter and Create Bar */}
<div className="agent-controls">
<div className="agent-state-filter">
<div className="agent-controls-filters">
<div className="agent-state-filter">
<Filter size={14} />
<select
className="agent-state-filter-select"
@@ -557,7 +576,7 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
</select>
</div>
<label className="checkbox-label agent-system-filter">
<label className="checkbox-label agent-system-filter">
<input
type="checkbox"
checked={showSystemAgents}
@@ -565,7 +584,8 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
aria-label="Show system agents"
/>
Show system agents
</label>
</label>
</div>
<div className="agent-controls-actions">
<button
@@ -609,11 +629,7 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
{agentView === "tree" ? (
<div className="agent-tree__view">
{displayAgents.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>
<AgentEmptyState onCtaClick={() => setIsCreating(true)} />
) : (
hierarchy.rootNodes.map((node) => (
<AgentTreeNode
@@ -638,11 +654,7 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
<span>Loading org chart...</span>
</div>
) : displayOrgTree.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>
<AgentEmptyState onCtaClick={() => setIsCreating(true)} />
) : (
displayOrgTree.map((node) => (
<OrgChartNode
@@ -659,18 +671,15 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
) : (
<div className={agentView === "board" ? "agent-board" : "agent-list"}>
{displayAgents.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>
<AgentEmptyState onCtaClick={() => setIsCreating(true)} />
) : agentView === "board" ? (
// Board view: compact grid layout
displayAgents.map(agent => {
const health = getHealthStatus(agent);
const stateStyle = STATE_COLORS[agent.state];
const stateBadgeClass = getStateBadgeClass(agent.state);
const stateCardClass = getStateCardClass("agent-board-card", agent.state);
return (
<div key={agent.id} className="agent-board-card" style={{ borderColor: stateStyle.border }}>
<div key={agent.id} className={`agent-board-card ${stateCardClass}`}>
<div
className="agent-board-clickable"
onClick={() => setSelectedAgentId(agent.id)}
@@ -681,12 +690,7 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
<div className="agent-board-header">
<span className="agent-board-icon">{getRoleIcon(agent.role)}</span>
<span
className="agent-board-badge"
style={{
background: stateStyle.bg,
color: stateStyle.text,
border: `1px solid ${stateStyle.border}`,
}}
className={`agent-board-badge ${stateBadgeClass}`}
>
{agent.state}
</span>
@@ -847,12 +851,13 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
// List view: detailed card layout
displayAgents.map(agent => {
const health = getHealthStatus(agent);
const stateStyle = STATE_COLORS[agent.state];
const stateBadgeClass = getStateBadgeClass(agent.state);
const stateCardClass = getStateCardClass("agent-card", agent.state);
const configuredIntervalMs = resolveHeartbeatIntervalMs(agent.runtimeConfig?.heartbeatIntervalMs);
const heartbeatOptions = getHeartbeatIntervalOptions(configuredIntervalMs);
const isUpdatingHeartbeat = updatingHeartbeatAgentId === agent.id;
return (
<div key={agent.id} className="agent-card" style={{ borderLeftColor: stateStyle.border }}>
<div key={agent.id} className={`agent-card ${stateCardClass}`}>
<div className="agent-card-header">
<div
className="agent-info agent-info--clickable"
@@ -905,12 +910,7 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
</div>
<div className="agent-badges">
<span
className="badge"
style={{
background: stateStyle.bg,
color: stateStyle.text,
border: `1px solid ${stateStyle.border}`,
}}
className={`badge ${stateBadgeClass}`}
>
{agent.state}
</span>

View File

@@ -108,4 +108,16 @@ describe("AgentMetricsBar", () => {
expect(screen.getByTestId("check-circle-icon")).toBeInTheDocument();
expect(screen.getByTestId("zap-icon")).toBeInTheDocument();
});
it("applies metric variant classes without inline icon styles", () => {
const { container } = render(<AgentMetricsBar stats={makeStats()} />);
expect(container.querySelector(".agent-metric-card--active")).toBeInTheDocument();
expect(container.querySelector(".agent-metric-card--tasks")).toBeInTheDocument();
expect(container.querySelector(".agent-metric-card--success")).toBeInTheDocument();
expect(container.querySelector(".agent-metric-card--runs")).toBeInTheDocument();
const metricsBarMarkup = container.querySelector(".agent-metrics-bar")?.innerHTML ?? "";
expect(metricsBarMarkup).not.toContain("style=");
});
});

View File

@@ -134,6 +134,19 @@ describe("AgentsView", () => {
await waitFor(() => {
expect(screen.getByText("No agents found")).toBeTruthy();
expect(screen.getByText("Create an agent to get started")).toBeTruthy();
expect(screen.getByRole("button", { name: "Create Agent" })).toBeTruthy();
});
});
it("opens the create dialog from the empty state CTA", async () => {
mockFetchAgents.mockResolvedValue([]);
render(<AgentsView addToast={mockAddToast} />);
const cta = await screen.findByRole("button", { name: "Create Agent" });
fireEvent.click(cta);
await waitFor(() => {
expect(screen.getByRole("dialog", { name: "Create new agent" })).toBeTruthy();
});
});
@@ -449,6 +462,7 @@ describe("AgentsView", () => {
await waitFor(() => {
expect(screen.getByText("No agents found")).toBeTruthy();
expect(screen.getByText("Create an agent to get started")).toBeTruthy();
expect(screen.getByRole("button", { name: "Create Agent" })).toBeTruthy();
});
});

View File

@@ -44,6 +44,7 @@ vi.mock("../../api", () => ({
deleteAgent: vi.fn(),
startAgentRun: vi.fn(),
fetchModels: vi.fn(() => Promise.resolve({ models: [] })),
fetchOrgTree: vi.fn(),
}));
import {
@@ -53,6 +54,7 @@ import {
updateAgentState,
deleteAgent,
startAgentRun,
fetchOrgTree,
} from "../../api";
const mockAgents: Agent[] = [
@@ -105,6 +107,7 @@ describe("AgentsView mobile adaptations", () => {
endedAt: null,
status: "active",
});
vi.mocked(fetchOrgTree).mockResolvedValue([]);
});
it("renders board view grid and board cards", async () => {
@@ -136,6 +139,7 @@ describe("AgentsView mobile adaptations", () => {
await waitFor(() => expect(screen.getByText("Agents")).toBeTruthy());
expect(container.querySelector(".agent-controls")).toBeTruthy();
expect(container.querySelector(".agent-controls-filters")).toBeTruthy();
expect(container.querySelector(".agent-state-filter")).toBeTruthy();
expect(container.querySelector(".agent-controls-actions")).toBeTruthy();
});
@@ -195,9 +199,24 @@ describe("agents-view mobile CSS", () => {
expect(block).toContain("flex-wrap: wrap");
});
it("stacks grouped filter controls on mobile", () => {
expect(mobileMediaBlock).toContain(".agent-controls-filters");
const block = extractRuleBlock(mobileMediaBlock, ".agent-controls-filters");
expect(block).toContain("flex-direction: column");
expect(block).toContain("width: 100%");
});
it("defines .agents-view-title with flex-wrap on mobile", () => {
expect(mobileMediaBlock).toContain(".agents-view-title");
const block = extractRuleBlock(mobileMediaBlock, ".agents-view-title");
expect(block).toContain("flex-wrap: wrap");
});
it("defines mobile org chart sizing rules", () => {
expect(mobileMediaBlock).toContain(".org-chart-node");
expect(extractRuleBlock(mobileMediaBlock, ".org-chart-node")).toContain("min-width: calc(var(--space-2xl) * 5)");
expect(extractRuleBlock(mobileMediaBlock, ".org-chart-node-card")).toContain("padding: var(--space-sm)");
expect(extractRuleBlock(mobileMediaBlock, ".agent-org-chart")).toContain("gap: var(--space-sm)");
expect(extractRuleBlock(mobileMediaBlock, ".org-chart-node__badge")).toContain("font-size: calc(var(--space-sm) + var(--space-xs) * 0.625)");
});
});

View File

@@ -872,7 +872,7 @@ body {
border-radius: var(--radius-sm);
color: var(--text-muted);
cursor: pointer;
transition: all var(--transition-fast);
transition: background var(--transition-fast), color var(--transition-fast), border-radius var(--transition-fast), box-shadow var(--transition-fast);
}
.view-toggle-btn:hover {
@@ -887,6 +887,7 @@ body {
.view-toggle-btn.active {
background: var(--todo);
color: var(--bg);
border-radius: var(--radius-md);
}
.view-toggle-btn.active:hover {
@@ -25731,9 +25732,21 @@ html .column.drag-over * {
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius-md);
box-shadow: var(--shadow-sm);
transition: background var(--transition-fast), transform var(--transition-fast), box-shadow var(--transition-fast);
flex: 1;
}
.agent-metric-card:hover {
background: var(--card-hover);
transform: translateY(calc(var(--space-xs) * -0.25));
}
.agent-metric-card--active svg { color: var(--state-active-text); }
.agent-metric-card--tasks svg { color: var(--in-progress); }
.agent-metric-card--success svg { color: var(--color-success); }
.agent-metric-card--runs svg { color: var(--in-progress); }
.agent-metric-info {
display: flex;
flex-direction: column;
@@ -25799,18 +25812,30 @@ html .column.drag-over * {
align-items: center;
gap: var(--space-md);
margin-bottom: var(--space-lg);
flex-wrap: wrap;
}
.agent-controls-filters {
display: flex;
align-items: center;
gap: var(--space-sm);
padding: var(--space-xs) var(--space-sm);
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-md);
box-shadow: var(--shadow-sm);
}
.agent-state-filter {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
gap: var(--space-sm);
padding: var(--space-xs) calc(var(--space-sm) + var(--space-xs));
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
color: var(--text-muted);
transition: border-color var(--transition-fast), color var(--transition-fast);
transition: border-color var(--transition-fast), color var(--transition-fast), background var(--transition-fast);
}
.agent-state-filter:hover {
@@ -25841,8 +25866,11 @@ html .column.drag-over * {
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
gap: var(--space-sm);
user-select: none;
padding: var(--space-xs) var(--space-sm);
border-radius: var(--radius-sm);
transition: color var(--transition-fast), background var(--transition-fast);
}
.agent-system-filter:hover {
@@ -25870,18 +25898,18 @@ html .column.drag-over * {
.agent-tree__view {
display: flex;
flex-direction: column;
gap: 4px;
padding: 8px 0;
gap: var(--space-xs);
padding: var(--space-sm) 0;
}
.agent-tree__node {
position: relative;
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-radius: 4px;
transition: background-color 0.15s ease;
gap: var(--space-sm);
padding: var(--space-sm) var(--space-md);
border-radius: var(--radius-sm);
transition: background-color var(--transition-fast);
}
.agent-tree__node:hover {
@@ -25889,21 +25917,21 @@ html .column.drag-over * {
}
.agent-tree__indent--0 { padding-left: 0; }
.agent-tree__indent--1 { padding-left: 24px; }
.agent-tree__indent--2 { padding-left: 48px; }
.agent-tree__indent--3 { padding-left: 72px; }
.agent-tree__indent--4 { padding-left: 96px; }
.agent-tree__indent--1 { padding-left: var(--space-xl); }
.agent-tree__indent--2 { padding-left: calc(var(--space-xl) * 2); }
.agent-tree__indent--3 { padding-left: calc(var(--space-xl) * 3); }
.agent-tree__indent--4 { padding-left: calc(var(--space-xl) * 4); }
.agent-tree__toggle {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
width: calc(var(--space-md) * 2);
height: calc(var(--space-md) * 2);
cursor: pointer;
border-radius: 4px;
color: var(--text-secondary);
transition: background-color 0.15s ease, color 0.15s ease;
border-radius: var(--radius-sm);
color: var(--text-muted);
transition: background-color var(--transition-fast), color var(--transition-fast);
flex-shrink: 0;
background: none;
border: none;
@@ -25912,7 +25940,7 @@ html .column.drag-over * {
.agent-tree__toggle:hover {
background: var(--card-hover);
color: var(--text-primary);
color: var(--text);
}
.agent-tree__toggle--leaf {
@@ -25928,7 +25956,7 @@ html .column.drag-over * {
.agent-tree__content {
display: flex;
align-items: center;
gap: 8px;
gap: var(--space-sm);
cursor: pointer;
flex: 1;
min-width: 0;
@@ -25969,14 +25997,28 @@ html .column.drag-over * {
}
.agent-is-child {
background: rgba(124, 92, 191, 0.05);
background: color-mix(in srgb, var(--accent) 5%, transparent);
border-left: 2px solid var(--accent);
}
.agent-tree__children {
position: relative;
display: flex;
flex-direction: column;
gap: 2px;
gap: var(--space-xs);
margin-left: calc(var(--space-md) - var(--space-xs));
padding-left: var(--space-md);
border-left: 1px solid var(--border);
}
.agent-tree__children::before {
content: "";
position: absolute;
left: -1px;
top: 0;
bottom: 50%;
width: 1px;
background: var(--border);
}
.agent-board {
@@ -25989,7 +26031,7 @@ html .column.drag-over * {
display: flex;
flex-direction: column;
gap: var(--space-sm);
padding: var(--space-md);
padding: var(--space-lg);
background: var(--surface);
border: 1px solid var(--border);
border-top-width: 3px;
@@ -26051,8 +26093,8 @@ html .column.drag-over * {
.agent-board-actions {
display: flex;
gap: 6px;
margin-top: 4px;
gap: var(--space-xs);
margin-top: var(--space-xs);
padding-top: var(--space-sm);
border-top: 1px solid var(--border);
}
@@ -26215,10 +26257,66 @@ html .column.drag-over * {
display: flex;
flex-direction: column;
align-items: center;
padding: 48px;
gap: var(--space-sm);
padding: calc(var(--space-2xl) + var(--space-lg));
color: var(--text-muted);
text-align: center;
}
.agent-empty-state__icon {
color: var(--text-dim);
}
.agent-empty-state__title,
.agent-empty-state__description {
margin: 0;
}
.agent-empty .btn {
margin-top: var(--space-sm);
}
/* Agent state badge variants */
.agent-badge--idle {
background: var(--state-idle-bg);
color: var(--state-idle-text);
border: 1px solid var(--state-idle-border);
}
.agent-badge--active,
.agent-badge--running {
background: var(--state-active-bg);
color: var(--state-active-text);
border: 1px solid var(--state-active-border);
}
.agent-badge--paused {
background: var(--state-paused-bg);
color: var(--state-paused-text);
border: 1px solid var(--state-paused-border);
}
.agent-badge--error,
.agent-badge--terminated {
background: var(--state-error-bg);
color: var(--state-error-text);
border: 1px solid var(--state-error-border);
}
.agent-board-card--idle { border-top-color: var(--state-idle-border); }
.agent-board-card--active,
.agent-board-card--running { border-top-color: var(--state-active-border); }
.agent-board-card--paused { border-top-color: var(--state-paused-border); }
.agent-board-card--error,
.agent-board-card--terminated { border-top-color: var(--state-error-border); }
.agent-card--idle { border-left-color: var(--state-idle-border); }
.agent-card--active,
.agent-card--running { border-left-color: var(--state-active-border); }
.agent-card--paused { border-left-color: var(--state-paused-border); }
.agent-card--error,
.agent-card--terminated { border-left-color: var(--state-error-border); }
.spin {
animation: spin 1s linear infinite;
}
@@ -28033,7 +28131,7 @@ html .column.drag-over * {
.agent-board-actions .btn,
.agent-card-actions .btn {
min-height: 36px;
min-height: calc(var(--space-2xl) + var(--space-xs));
}
.agent-controls {
@@ -28042,6 +28140,13 @@ html .column.drag-over * {
gap: var(--space-sm);
}
.agent-controls-filters {
flex-direction: column;
align-items: stretch;
width: 100%;
padding: var(--space-sm);
}
.agent-controls-actions {
display: flex;
flex-direction: column;
@@ -28050,12 +28155,13 @@ html .column.drag-over * {
.agent-controls-actions .btn {
width: 100%;
min-height: 36px;
min-height: calc(var(--space-2xl) + var(--space-xs));
}
.agent-state-filter {
.agent-state-filter,
.agent-system-filter {
width: 100%;
min-height: 36px;
min-height: calc(var(--space-2xl) + var(--space-xs));
}
.agent-state-filter-select {
@@ -28211,14 +28317,14 @@ html .column.drag-over * {
min-width: 36px;
}
.agent-tree__indent--1 { padding-left: 16px; }
.agent-tree__indent--2 { padding-left: 32px; }
.agent-tree__indent--3 { padding-left: 48px; }
.agent-tree__indent--4 { padding-left: 64px; }
.agent-tree__indent--1 { padding-left: var(--space-lg); }
.agent-tree__indent--2 { padding-left: calc(var(--space-lg) * 2); }
.agent-tree__indent--3 { padding-left: calc(var(--space-lg) * 3); }
.agent-tree__indent--4 { padding-left: calc(var(--space-lg) * 4); }
.agent-tree__toggle {
min-width: 36px;
min-height: 36px;
min-width: calc(var(--space-lg) * 2 + var(--space-xs));
min-height: calc(var(--space-lg) * 2 + var(--space-xs));
}
/* Performance Tab mobile */
@@ -33229,19 +33335,19 @@ html .column.drag-over * {
display: flex;
align-items: flex-start;
justify-content: flex-start;
gap: 1.5rem;
padding: 1rem;
gap: var(--space-xl);
padding: var(--space-lg);
overflow-x: auto;
overflow-y: visible;
min-height: 220px;
min-height: calc(var(--space-xl) * 9 + var(--space-xs));
}
.agent-org-chart__loading,
.chain-of-command-loading {
display: inline-flex;
align-items: center;
gap: 0.5rem;
color: var(--text-secondary);
gap: var(--space-sm);
color: var(--text-muted);
}
.org-chart-node {
@@ -33249,19 +33355,19 @@ html .column.drag-over * {
display: flex;
flex-direction: column;
align-items: center;
min-width: 220px;
min-width: calc(var(--space-xl) * 9 + var(--space-xs));
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);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: var(--space-md);
color: var(--text);
cursor: pointer;
transition: border-color 120ms ease, background-color 120ms ease, transform 120ms ease;
transition: border-color var(--transition-fast), background-color var(--transition-fast), transform var(--transition-fast);
position: relative;
}
@@ -33269,19 +33375,19 @@ html .column.drag-over * {
.org-chart-node-card:focus-visible {
border-color: var(--accent);
background: var(--card-hover);
transform: translateY(-1px);
transform: translateY(calc(var(--space-xs) * -0.25));
outline: none;
}
.org-chart-node__header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
gap: var(--space-sm);
margin-bottom: var(--space-sm);
}
.org-chart-node__icon {
font-size: 1rem;
font-size: var(--space-lg);
}
.org-chart-node__name {
@@ -33293,23 +33399,23 @@ html .column.drag-over * {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
gap: var(--space-sm);
}
.org-chart-node__badge {
display: inline-flex;
align-items: center;
padding: 0.125rem 0.5rem;
border-radius: 999px;
font-size: 0.72rem;
padding: calc(var(--space-xs) * 0.5) var(--space-sm);
border-radius: var(--radius-pill);
font-size: calc(var(--space-sm) + var(--space-xs) * 0.75);
text-transform: capitalize;
}
.org-chart-node__health {
display: inline-flex;
align-items: center;
gap: 0.3rem;
font-size: 0.75rem;
gap: var(--space-xs);
font-size: var(--space-md);
}
.org-chart-node--has-children > .org-chart-node-card::after {
@@ -33317,10 +33423,10 @@ html .column.drag-over * {
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: -16px;
bottom: calc(var(--space-lg) * -1);
width: 1px;
height: 16px;
background: var(--border-color);
height: var(--space-lg);
background: var(--border);
}
.org-chart-children {
@@ -33328,60 +33434,60 @@ html .column.drag-over * {
display: flex;
align-items: flex-start;
justify-content: center;
gap: 1rem;
padding-top: 1.5rem;
margin-top: 0.25rem;
gap: var(--space-lg);
padding-top: var(--space-xl);
margin-top: var(--space-xs);
}
.org-chart-children::before {
content: "";
position: absolute;
top: 0;
left: 24px;
right: 24px;
left: var(--space-xl);
right: var(--space-xl);
height: 1px;
background: var(--border-color);
background: var(--border);
}
.org-chart-children > .org-chart-node::before {
content: "";
position: absolute;
top: -24px;
top: calc(var(--space-xl) * -1);
left: 50%;
transform: translateX(-50%);
width: 1px;
height: 24px;
background: var(--border-color);
height: var(--space-xl);
background: var(--border);
}
.chain-of-command-path {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.35rem 0.5rem;
gap: var(--space-xs) var(--space-sm);
}
.chain-of-command-item {
display: inline-flex;
align-items: center;
gap: 0.4rem;
gap: var(--space-xs);
}
.chain-of-command-node {
display: inline-flex;
align-items: center;
padding: 0.25rem 0.6rem;
border-radius: 999px;
border: 1px solid var(--border-color);
padding: var(--space-xs) calc(var(--space-sm) + var(--space-xs) * 0.5);
border-radius: var(--radius-pill);
border: 1px solid var(--border);
background: var(--surface);
color: var(--text-secondary);
font-size: 0.78rem;
color: var(--text-muted);
font-size: calc(var(--space-sm) + var(--space-xs));
cursor: pointer;
}
.chain-of-command-node:hover:not(:disabled) {
border-color: var(--accent);
color: var(--text-primary);
color: var(--text);
}
.chain-of-command-node:disabled {
@@ -33391,7 +33497,7 @@ html .column.drag-over * {
.chain-of-command-node--current {
border-color: var(--accent);
color: var(--text-primary);
color: var(--text);
font-weight: 600;
}
@@ -33401,11 +33507,26 @@ html .column.drag-over * {
@media (max-width: 768px) {
.agent-org-chart {
padding-bottom: 1.5rem;
padding: var(--space-sm);
padding-bottom: calc(var(--space-lg) + var(--space-sm));
gap: var(--space-sm);
}
.org-chart-node {
min-width: 200px;
min-width: calc(var(--space-2xl) * 5);
}
.org-chart-node-card {
padding: var(--space-sm);
border-radius: var(--radius-md);
}
.org-chart-node__name {
font-size: var(--space-md);
}
.org-chart-node__badge {
font-size: calc(var(--space-sm) + var(--space-xs) * 0.625);
}
}