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:
5
.changeset/agents-list-view-ux-pass.md
Normal file
5
.changeset/agents-list-view-ux-pass.md
Normal 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
11
.changeset/config.json
Normal 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": []
|
||||||
|
}
|
||||||
@@ -7,15 +7,28 @@ const stylesContent = fs.readFileSync(stylesPath, "utf-8");
|
|||||||
|
|
||||||
// Agent component file paths to verify inline <style> blocks are removed
|
// Agent component file paths to verify inline <style> blocks are removed
|
||||||
const agentsViewContent = fs.readFileSync(path.join(__dirname, "../components/AgentsView.tsx"), "utf-8");
|
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 agentDetailViewContent = fs.readFileSync(path.join(__dirname, "../components/AgentDetailView.tsx"), "utf-8");
|
||||||
const activeAgentsPanelContent = fs.readFileSync(path.join(__dirname, "../components/ActiveAgentsPanel.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");
|
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 */
|
/** 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 {
|
function hasClass(cls: string): boolean {
|
||||||
const escaped = cls.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
const escaped = cls.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
// Match standalone class or class within a grouped selector list
|
// Match standalone class, grouped selector, or compound selector (e.g. `.foo svg`)
|
||||||
return new RegExp(`${escaped}\\s*(,|\\{)`).test(stylesContent);
|
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", () => {
|
describe("Agent CSS classes", () => {
|
||||||
@@ -53,6 +66,10 @@ describe("Agent CSS classes", () => {
|
|||||||
it("should define AgentMetricsBar CSS classes", () => {
|
it("should define AgentMetricsBar CSS classes", () => {
|
||||||
expect(hasClass(".agent-metrics-bar")).toBe(true);
|
expect(hasClass(".agent-metrics-bar")).toBe(true);
|
||||||
expect(hasClass(".agent-metric-card")).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-info")).toBe(true);
|
||||||
expect(hasClass(".agent-metric-value")).toBe(true);
|
expect(hasClass(".agent-metric-value")).toBe(true);
|
||||||
expect(hasClass(".agent-metric-label")).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-controls")).toBe(true);
|
||||||
expect(hasClass(".agents-view-content")).toBe(true);
|
expect(hasClass(".agents-view-content")).toBe(true);
|
||||||
expect(hasClass(".agent-controls")).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")).toBe(true);
|
||||||
expect(hasClass(".agent-state-filter-select")).toBe(true);
|
expect(hasClass(".agent-state-filter-select")).toBe(true);
|
||||||
expect(hasClass(".agent-board")).toBe(true);
|
expect(hasClass(".agent-board")).toBe(true);
|
||||||
expect(hasClass(".agent-board-card")).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-header")).toBe(true);
|
||||||
expect(hasClass(".agent-board-icon")).toBe(true);
|
expect(hasClass(".agent-board-icon")).toBe(true);
|
||||||
expect(hasClass(".agent-board-badge")).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-health")).toBe(true);
|
||||||
expect(hasClass(".agent-board-name")).toBe(true);
|
expect(hasClass(".agent-board-name")).toBe(true);
|
||||||
expect(hasClass(".agent-board-id")).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-board-actions")).toBe(true);
|
||||||
expect(hasClass(".agent-list")).toBe(true);
|
expect(hasClass(".agent-list")).toBe(true);
|
||||||
expect(hasClass(".agent-card")).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-header")).toBe(true);
|
||||||
expect(hasClass(".agent-card-body")).toBe(true);
|
expect(hasClass(".agent-card-body")).toBe(true);
|
||||||
expect(hasClass(".agent-card-actions")).toBe(true);
|
expect(hasClass(".agent-card-actions")).toBe(true);
|
||||||
@@ -99,6 +135,48 @@ describe("Agent CSS classes", () => {
|
|||||||
expect(hasClass(".spin")).toBe(true);
|
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
|
// Verify AgentDetailView classes
|
||||||
it("should define AgentDetailView CSS classes", () => {
|
it("should define AgentDetailView CSS classes", () => {
|
||||||
expect(hasClass(".agent-detail-overlay")).toBe(true);
|
expect(hasClass(".agent-detail-overlay")).toBe(true);
|
||||||
@@ -211,11 +289,23 @@ describe("Agent CSS classes", () => {
|
|||||||
expect(hasClass(".agent-dialog-loading")).toBe(true);
|
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
|
// Verify no inline <style> blocks remain in agent components
|
||||||
it("should not have inline <style> blocks in AgentsView", () => {
|
it("should not have inline <style> blocks in AgentsView", () => {
|
||||||
expect(agentsViewContent).not.toContain("<style>");
|
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", () => {
|
it("should not have inline <style> blocks in AgentDetailView", () => {
|
||||||
expect(agentDetailViewContent).not.toContain("<style>");
|
expect(agentDetailViewContent).not.toContain("<style>");
|
||||||
});
|
});
|
||||||
|
|||||||
28
packages/dashboard/app/components/AgentEmptyState.tsx
Normal file
28
packages/dashboard/app/components/AgentEmptyState.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,27 +5,33 @@ interface AgentMetricsBarProps {
|
|||||||
stats: AgentStats | null;
|
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) {
|
export function AgentMetricsBar({ stats }: AgentMetricsBarProps) {
|
||||||
if (!stats) return null;
|
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 (
|
return (
|
||||||
<div className="agent-metrics-bar">
|
<div className="agent-metrics-bar">
|
||||||
{cards.map(card => (
|
{METRIC_CARDS.map((card) => {
|
||||||
<div key={card.label} className="agent-metric-card">
|
const value = card.valueKey === "successRate"
|
||||||
<card.icon size={18} style={{ color: card.color }} />
|
? `${Math.round(stats.successRate * 100)}%`
|
||||||
<div className="agent-metric-info">
|
: stats[card.valueKey];
|
||||||
<span className="agent-metric-value">{card.value}</span>
|
|
||||||
<span className="agent-metric-label">{card.label}</span>
|
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>
|
);
|
||||||
))}
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { fetchAgents, updateAgent, updateAgentState, deleteAgent, startAgentRun,
|
|||||||
import { AgentDetailView } from "./AgentDetailView";
|
import { AgentDetailView } from "./AgentDetailView";
|
||||||
import { ActiveAgentsPanel } from "./ActiveAgentsPanel";
|
import { ActiveAgentsPanel } from "./ActiveAgentsPanel";
|
||||||
import { AgentMetricsBar } from "./AgentMetricsBar";
|
import { AgentMetricsBar } from "./AgentMetricsBar";
|
||||||
|
import { AgentEmptyState } from "./AgentEmptyState";
|
||||||
import { useAgents } from "../hooks/useAgents";
|
import { useAgents } from "../hooks/useAgents";
|
||||||
import { subscribeSse } from "../sse-bus";
|
import { subscribeSse } from "../sse-bus";
|
||||||
import { useAgentHierarchy } from "../hooks/useAgentHierarchy";
|
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 }> = {
|
function getStateBadgeClass(state: AgentState): string {
|
||||||
idle: { bg: "var(--state-idle-bg)", text: "var(--state-idle-text)", border: "var(--state-idle-border)" },
|
switch (state) {
|
||||||
active: { bg: "var(--state-active-bg)", text: "var(--state-active-text)", border: "var(--state-active-border)" },
|
case "running":
|
||||||
running: { bg: "var(--state-active-bg)", text: "var(--state-active-text)", border: "var(--state-active-border)" },
|
return "agent-badge--running";
|
||||||
paused: { bg: "var(--state-paused-bg)", text: "var(--state-paused-text)", border: "var(--state-paused-border)" },
|
case "active":
|
||||||
error: { bg: "var(--state-error-bg)", text: "var(--state-error-text)", border: "var(--state-error-border)" },
|
return "agent-badge--active";
|
||||||
terminated: { bg: "var(--state-error-bg)", text: "var(--state-error-text)", border: "var(--state-error-border)" },
|
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 */
|
/** Recursive tree node component for agent hierarchy */
|
||||||
function AgentTreeNode({
|
function AgentTreeNode({
|
||||||
@@ -70,7 +98,7 @@ function AgentTreeNode({
|
|||||||
const childCount = getChildCount(agent.id);
|
const childCount = getChildCount(agent.id);
|
||||||
const expanded = isExpanded(agent.id);
|
const expanded = isExpanded(agent.id);
|
||||||
const health = getHealthStatus(agent);
|
const health = getHealthStatus(agent);
|
||||||
const stateStyle = STATE_COLORS[agent.state];
|
const stateBadgeClass = getStateBadgeClass(agent.state);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -99,12 +127,7 @@ function AgentTreeNode({
|
|||||||
<span className="agent-tree__icon">{getRoleIcon(agent.role)}</span>
|
<span className="agent-tree__icon">{getRoleIcon(agent.role)}</span>
|
||||||
<span className="agent-tree__name">{agent.name}</span>
|
<span className="agent-tree__name">{agent.name}</span>
|
||||||
<span
|
<span
|
||||||
className="agent-tree__badge"
|
className={`agent-tree__badge ${stateBadgeClass}`}
|
||||||
style={{
|
|
||||||
background: stateStyle.bg,
|
|
||||||
color: stateStyle.text,
|
|
||||||
border: `1px solid ${stateStyle.border}`,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{agent.state}
|
{agent.state}
|
||||||
</span>
|
</span>
|
||||||
@@ -162,7 +185,7 @@ function OrgChartNode({
|
|||||||
}) {
|
}) {
|
||||||
const { agent, children } = node;
|
const { agent, children } = node;
|
||||||
const health = getHealthStatus(agent);
|
const health = getHealthStatus(agent);
|
||||||
const stateStyle = STATE_COLORS[agent.state];
|
const stateBadgeClass = getStateBadgeClass(agent.state);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`org-chart-node${children.length > 0 ? " org-chart-node--has-children" : ""}`}>
|
<div className={`org-chart-node${children.length > 0 ? " org-chart-node--has-children" : ""}`}>
|
||||||
@@ -179,12 +202,7 @@ function OrgChartNode({
|
|||||||
</div>
|
</div>
|
||||||
<div className="org-chart-node__meta">
|
<div className="org-chart-node__meta">
|
||||||
<span
|
<span
|
||||||
className="org-chart-node__badge"
|
className={`org-chart-node__badge ${stateBadgeClass}`}
|
||||||
style={{
|
|
||||||
background: stateStyle.bg,
|
|
||||||
color: stateStyle.text,
|
|
||||||
border: `1px solid ${stateStyle.border}`,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{agent.state}
|
{agent.state}
|
||||||
</span>
|
</span>
|
||||||
@@ -539,7 +557,8 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
|
|||||||
<div className="agents-view-content">
|
<div className="agents-view-content">
|
||||||
{/* Filter and Create Bar */}
|
{/* Filter and Create Bar */}
|
||||||
<div className="agent-controls">
|
<div className="agent-controls">
|
||||||
<div className="agent-state-filter">
|
<div className="agent-controls-filters">
|
||||||
|
<div className="agent-state-filter">
|
||||||
<Filter size={14} />
|
<Filter size={14} />
|
||||||
<select
|
<select
|
||||||
className="agent-state-filter-select"
|
className="agent-state-filter-select"
|
||||||
@@ -557,7 +576,7 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label className="checkbox-label agent-system-filter">
|
<label className="checkbox-label agent-system-filter">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={showSystemAgents}
|
checked={showSystemAgents}
|
||||||
@@ -565,7 +584,8 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
|
|||||||
aria-label="Show system agents"
|
aria-label="Show system agents"
|
||||||
/>
|
/>
|
||||||
Show system agents
|
Show system agents
|
||||||
</label>
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="agent-controls-actions">
|
<div className="agent-controls-actions">
|
||||||
<button
|
<button
|
||||||
@@ -609,11 +629,7 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
|
|||||||
{agentView === "tree" ? (
|
{agentView === "tree" ? (
|
||||||
<div className="agent-tree__view">
|
<div className="agent-tree__view">
|
||||||
{displayAgents.length === 0 ? (
|
{displayAgents.length === 0 ? (
|
||||||
<div className="agent-empty">
|
<AgentEmptyState onCtaClick={() => setIsCreating(true)} />
|
||||||
<Bot size={48} opacity={0.3} />
|
|
||||||
<p>No agents found</p>
|
|
||||||
<p className="text-secondary">Create an agent to get started</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
hierarchy.rootNodes.map((node) => (
|
hierarchy.rootNodes.map((node) => (
|
||||||
<AgentTreeNode
|
<AgentTreeNode
|
||||||
@@ -638,11 +654,7 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
|
|||||||
<span>Loading org chart...</span>
|
<span>Loading org chart...</span>
|
||||||
</div>
|
</div>
|
||||||
) : displayOrgTree.length === 0 ? (
|
) : displayOrgTree.length === 0 ? (
|
||||||
<div className="agent-empty">
|
<AgentEmptyState onCtaClick={() => setIsCreating(true)} />
|
||||||
<Bot size={48} opacity={0.3} />
|
|
||||||
<p>No agents found</p>
|
|
||||||
<p className="text-secondary">Create an agent to get started</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
displayOrgTree.map((node) => (
|
displayOrgTree.map((node) => (
|
||||||
<OrgChartNode
|
<OrgChartNode
|
||||||
@@ -659,18 +671,15 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
|
|||||||
) : (
|
) : (
|
||||||
<div className={agentView === "board" ? "agent-board" : "agent-list"}>
|
<div className={agentView === "board" ? "agent-board" : "agent-list"}>
|
||||||
{displayAgents.length === 0 ? (
|
{displayAgents.length === 0 ? (
|
||||||
<div className="agent-empty">
|
<AgentEmptyState onCtaClick={() => setIsCreating(true)} />
|
||||||
<Bot size={48} opacity={0.3} />
|
|
||||||
<p>No agents found</p>
|
|
||||||
<p className="text-secondary">Create an agent to get started</p>
|
|
||||||
</div>
|
|
||||||
) : agentView === "board" ? (
|
) : agentView === "board" ? (
|
||||||
// Board view: compact grid layout
|
// Board view: compact grid layout
|
||||||
displayAgents.map(agent => {
|
displayAgents.map(agent => {
|
||||||
const health = getHealthStatus(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 (
|
return (
|
||||||
<div key={agent.id} className="agent-board-card" style={{ borderColor: stateStyle.border }}>
|
<div key={agent.id} className={`agent-board-card ${stateCardClass}`}>
|
||||||
<div
|
<div
|
||||||
className="agent-board-clickable"
|
className="agent-board-clickable"
|
||||||
onClick={() => setSelectedAgentId(agent.id)}
|
onClick={() => setSelectedAgentId(agent.id)}
|
||||||
@@ -681,12 +690,7 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
|
|||||||
<div className="agent-board-header">
|
<div className="agent-board-header">
|
||||||
<span className="agent-board-icon">{getRoleIcon(agent.role)}</span>
|
<span className="agent-board-icon">{getRoleIcon(agent.role)}</span>
|
||||||
<span
|
<span
|
||||||
className="agent-board-badge"
|
className={`agent-board-badge ${stateBadgeClass}`}
|
||||||
style={{
|
|
||||||
background: stateStyle.bg,
|
|
||||||
color: stateStyle.text,
|
|
||||||
border: `1px solid ${stateStyle.border}`,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{agent.state}
|
{agent.state}
|
||||||
</span>
|
</span>
|
||||||
@@ -847,12 +851,13 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
|
|||||||
// List view: detailed card layout
|
// List view: detailed card layout
|
||||||
displayAgents.map(agent => {
|
displayAgents.map(agent => {
|
||||||
const health = getHealthStatus(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 configuredIntervalMs = resolveHeartbeatIntervalMs(agent.runtimeConfig?.heartbeatIntervalMs);
|
||||||
const heartbeatOptions = getHeartbeatIntervalOptions(configuredIntervalMs);
|
const heartbeatOptions = getHeartbeatIntervalOptions(configuredIntervalMs);
|
||||||
const isUpdatingHeartbeat = updatingHeartbeatAgentId === agent.id;
|
const isUpdatingHeartbeat = updatingHeartbeatAgentId === agent.id;
|
||||||
return (
|
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-card-header">
|
||||||
<div
|
<div
|
||||||
className="agent-info agent-info--clickable"
|
className="agent-info agent-info--clickable"
|
||||||
@@ -905,12 +910,7 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="agent-badges">
|
<div className="agent-badges">
|
||||||
<span
|
<span
|
||||||
className="badge"
|
className={`badge ${stateBadgeClass}`}
|
||||||
style={{
|
|
||||||
background: stateStyle.bg,
|
|
||||||
color: stateStyle.text,
|
|
||||||
border: `1px solid ${stateStyle.border}`,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{agent.state}
|
{agent.state}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -108,4 +108,16 @@ describe("AgentMetricsBar", () => {
|
|||||||
expect(screen.getByTestId("check-circle-icon")).toBeInTheDocument();
|
expect(screen.getByTestId("check-circle-icon")).toBeInTheDocument();
|
||||||
expect(screen.getByTestId("zap-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=");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -134,6 +134,19 @@ describe("AgentsView", () => {
|
|||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText("No agents found")).toBeTruthy();
|
expect(screen.getByText("No agents found")).toBeTruthy();
|
||||||
expect(screen.getByText("Create an agent to get started")).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(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText("No agents found")).toBeTruthy();
|
expect(screen.getByText("No agents found")).toBeTruthy();
|
||||||
expect(screen.getByText("Create an agent to get started")).toBeTruthy();
|
expect(screen.getByText("Create an agent to get started")).toBeTruthy();
|
||||||
|
expect(screen.getByRole("button", { name: "Create Agent" })).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ vi.mock("../../api", () => ({
|
|||||||
deleteAgent: vi.fn(),
|
deleteAgent: vi.fn(),
|
||||||
startAgentRun: vi.fn(),
|
startAgentRun: vi.fn(),
|
||||||
fetchModels: vi.fn(() => Promise.resolve({ models: [] })),
|
fetchModels: vi.fn(() => Promise.resolve({ models: [] })),
|
||||||
|
fetchOrgTree: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -53,6 +54,7 @@ import {
|
|||||||
updateAgentState,
|
updateAgentState,
|
||||||
deleteAgent,
|
deleteAgent,
|
||||||
startAgentRun,
|
startAgentRun,
|
||||||
|
fetchOrgTree,
|
||||||
} from "../../api";
|
} from "../../api";
|
||||||
|
|
||||||
const mockAgents: Agent[] = [
|
const mockAgents: Agent[] = [
|
||||||
@@ -105,6 +107,7 @@ describe("AgentsView mobile adaptations", () => {
|
|||||||
endedAt: null,
|
endedAt: null,
|
||||||
status: "active",
|
status: "active",
|
||||||
});
|
});
|
||||||
|
vi.mocked(fetchOrgTree).mockResolvedValue([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders board view grid and board cards", async () => {
|
it("renders board view grid and board cards", async () => {
|
||||||
@@ -136,6 +139,7 @@ describe("AgentsView mobile adaptations", () => {
|
|||||||
await waitFor(() => expect(screen.getByText("Agents")).toBeTruthy());
|
await waitFor(() => expect(screen.getByText("Agents")).toBeTruthy());
|
||||||
|
|
||||||
expect(container.querySelector(".agent-controls")).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-state-filter")).toBeTruthy();
|
||||||
expect(container.querySelector(".agent-controls-actions")).toBeTruthy();
|
expect(container.querySelector(".agent-controls-actions")).toBeTruthy();
|
||||||
});
|
});
|
||||||
@@ -195,9 +199,24 @@ describe("agents-view mobile CSS", () => {
|
|||||||
expect(block).toContain("flex-wrap: wrap");
|
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", () => {
|
it("defines .agents-view-title with flex-wrap on mobile", () => {
|
||||||
expect(mobileMediaBlock).toContain(".agents-view-title");
|
expect(mobileMediaBlock).toContain(".agents-view-title");
|
||||||
const block = extractRuleBlock(mobileMediaBlock, ".agents-view-title");
|
const block = extractRuleBlock(mobileMediaBlock, ".agents-view-title");
|
||||||
expect(block).toContain("flex-wrap: wrap");
|
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)");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -872,7 +872,7 @@ body {
|
|||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
cursor: pointer;
|
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 {
|
.view-toggle-btn:hover {
|
||||||
@@ -887,6 +887,7 @@ body {
|
|||||||
.view-toggle-btn.active {
|
.view-toggle-btn.active {
|
||||||
background: var(--todo);
|
background: var(--todo);
|
||||||
color: var(--bg);
|
color: var(--bg);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.view-toggle-btn.active:hover {
|
.view-toggle-btn.active:hover {
|
||||||
@@ -25731,9 +25732,21 @@ html .column.drag-over * {
|
|||||||
background: var(--card);
|
background: var(--card);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: var(--radius-md);
|
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;
|
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 {
|
.agent-metric-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -25799,18 +25812,30 @@ html .column.drag-over * {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--space-md);
|
gap: var(--space-md);
|
||||||
margin-bottom: var(--space-lg);
|
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 {
|
.agent-state-filter {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: var(--space-sm);
|
||||||
padding: 6px 10px;
|
padding: var(--space-xs) calc(var(--space-sm) + var(--space-xs));
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
color: var(--text-muted);
|
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 {
|
.agent-state-filter:hover {
|
||||||
@@ -25841,8 +25866,11 @@ html .column.drag-over * {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: var(--space-sm);
|
||||||
user-select: none;
|
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 {
|
.agent-system-filter:hover {
|
||||||
@@ -25870,18 +25898,18 @@ html .column.drag-over * {
|
|||||||
.agent-tree__view {
|
.agent-tree__view {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 4px;
|
gap: var(--space-xs);
|
||||||
padding: 8px 0;
|
padding: var(--space-sm) 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-tree__node {
|
.agent-tree__node {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: var(--space-sm);
|
||||||
padding: 8px 12px;
|
padding: var(--space-sm) var(--space-md);
|
||||||
border-radius: 4px;
|
border-radius: var(--radius-sm);
|
||||||
transition: background-color 0.15s ease;
|
transition: background-color var(--transition-fast);
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-tree__node:hover {
|
.agent-tree__node:hover {
|
||||||
@@ -25889,21 +25917,21 @@ html .column.drag-over * {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.agent-tree__indent--0 { padding-left: 0; }
|
.agent-tree__indent--0 { padding-left: 0; }
|
||||||
.agent-tree__indent--1 { padding-left: 24px; }
|
.agent-tree__indent--1 { padding-left: var(--space-xl); }
|
||||||
.agent-tree__indent--2 { padding-left: 48px; }
|
.agent-tree__indent--2 { padding-left: calc(var(--space-xl) * 2); }
|
||||||
.agent-tree__indent--3 { padding-left: 72px; }
|
.agent-tree__indent--3 { padding-left: calc(var(--space-xl) * 3); }
|
||||||
.agent-tree__indent--4 { padding-left: 96px; }
|
.agent-tree__indent--4 { padding-left: calc(var(--space-xl) * 4); }
|
||||||
|
|
||||||
.agent-tree__toggle {
|
.agent-tree__toggle {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 24px;
|
width: calc(var(--space-md) * 2);
|
||||||
height: 24px;
|
height: calc(var(--space-md) * 2);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: 4px;
|
border-radius: var(--radius-sm);
|
||||||
color: var(--text-secondary);
|
color: var(--text-muted);
|
||||||
transition: background-color 0.15s ease, color 0.15s ease;
|
transition: background-color var(--transition-fast), color var(--transition-fast);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -25912,7 +25940,7 @@ html .column.drag-over * {
|
|||||||
|
|
||||||
.agent-tree__toggle:hover {
|
.agent-tree__toggle:hover {
|
||||||
background: var(--card-hover);
|
background: var(--card-hover);
|
||||||
color: var(--text-primary);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-tree__toggle--leaf {
|
.agent-tree__toggle--leaf {
|
||||||
@@ -25928,7 +25956,7 @@ html .column.drag-over * {
|
|||||||
.agent-tree__content {
|
.agent-tree__content {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: var(--space-sm);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -25969,14 +25997,28 @@ html .column.drag-over * {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.agent-is-child {
|
.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);
|
border-left: 2px solid var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-tree__children {
|
.agent-tree__children {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
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 {
|
.agent-board {
|
||||||
@@ -25989,7 +26031,7 @@ html .column.drag-over * {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--space-sm);
|
gap: var(--space-sm);
|
||||||
padding: var(--space-md);
|
padding: var(--space-lg);
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-top-width: 3px;
|
border-top-width: 3px;
|
||||||
@@ -26051,8 +26093,8 @@ html .column.drag-over * {
|
|||||||
|
|
||||||
.agent-board-actions {
|
.agent-board-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 6px;
|
gap: var(--space-xs);
|
||||||
margin-top: 4px;
|
margin-top: var(--space-xs);
|
||||||
padding-top: var(--space-sm);
|
padding-top: var(--space-sm);
|
||||||
border-top: 1px solid var(--border);
|
border-top: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
@@ -26215,10 +26257,66 @@ html .column.drag-over * {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 48px;
|
gap: var(--space-sm);
|
||||||
|
padding: calc(var(--space-2xl) + var(--space-lg));
|
||||||
color: var(--text-muted);
|
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 {
|
.spin {
|
||||||
animation: spin 1s linear infinite;
|
animation: spin 1s linear infinite;
|
||||||
}
|
}
|
||||||
@@ -28033,7 +28131,7 @@ html .column.drag-over * {
|
|||||||
|
|
||||||
.agent-board-actions .btn,
|
.agent-board-actions .btn,
|
||||||
.agent-card-actions .btn {
|
.agent-card-actions .btn {
|
||||||
min-height: 36px;
|
min-height: calc(var(--space-2xl) + var(--space-xs));
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-controls {
|
.agent-controls {
|
||||||
@@ -28042,6 +28140,13 @@ html .column.drag-over * {
|
|||||||
gap: var(--space-sm);
|
gap: var(--space-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.agent-controls-filters {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
width: 100%;
|
||||||
|
padding: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
.agent-controls-actions {
|
.agent-controls-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -28050,12 +28155,13 @@ html .column.drag-over * {
|
|||||||
|
|
||||||
.agent-controls-actions .btn {
|
.agent-controls-actions .btn {
|
||||||
width: 100%;
|
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%;
|
width: 100%;
|
||||||
min-height: 36px;
|
min-height: calc(var(--space-2xl) + var(--space-xs));
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-state-filter-select {
|
.agent-state-filter-select {
|
||||||
@@ -28211,14 +28317,14 @@ html .column.drag-over * {
|
|||||||
min-width: 36px;
|
min-width: 36px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-tree__indent--1 { padding-left: 16px; }
|
.agent-tree__indent--1 { padding-left: var(--space-lg); }
|
||||||
.agent-tree__indent--2 { padding-left: 32px; }
|
.agent-tree__indent--2 { padding-left: calc(var(--space-lg) * 2); }
|
||||||
.agent-tree__indent--3 { padding-left: 48px; }
|
.agent-tree__indent--3 { padding-left: calc(var(--space-lg) * 3); }
|
||||||
.agent-tree__indent--4 { padding-left: 64px; }
|
.agent-tree__indent--4 { padding-left: calc(var(--space-lg) * 4); }
|
||||||
|
|
||||||
.agent-tree__toggle {
|
.agent-tree__toggle {
|
||||||
min-width: 36px;
|
min-width: calc(var(--space-lg) * 2 + var(--space-xs));
|
||||||
min-height: 36px;
|
min-height: calc(var(--space-lg) * 2 + var(--space-xs));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Performance Tab mobile */
|
/* Performance Tab mobile */
|
||||||
@@ -33229,19 +33335,19 @@ html .column.drag-over * {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
gap: 1.5rem;
|
gap: var(--space-xl);
|
||||||
padding: 1rem;
|
padding: var(--space-lg);
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
overflow-y: visible;
|
overflow-y: visible;
|
||||||
min-height: 220px;
|
min-height: calc(var(--space-xl) * 9 + var(--space-xs));
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-org-chart__loading,
|
.agent-org-chart__loading,
|
||||||
.chain-of-command-loading {
|
.chain-of-command-loading {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: var(--space-sm);
|
||||||
color: var(--text-secondary);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.org-chart-node {
|
.org-chart-node {
|
||||||
@@ -33249,19 +33355,19 @@ html .column.drag-over * {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
min-width: 220px;
|
min-width: calc(var(--space-xl) * 9 + var(--space-xs));
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.org-chart-node-card {
|
.org-chart-node-card {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border);
|
||||||
border-radius: 10px;
|
border-radius: var(--radius-lg);
|
||||||
padding: 0.75rem;
|
padding: var(--space-md);
|
||||||
color: var(--text-primary);
|
color: var(--text);
|
||||||
cursor: pointer;
|
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;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33269,19 +33375,19 @@ html .column.drag-over * {
|
|||||||
.org-chart-node-card:focus-visible {
|
.org-chart-node-card:focus-visible {
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
background: var(--card-hover);
|
background: var(--card-hover);
|
||||||
transform: translateY(-1px);
|
transform: translateY(calc(var(--space-xs) * -0.25));
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.org-chart-node__header {
|
.org-chart-node__header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: var(--space-sm);
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: var(--space-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.org-chart-node__icon {
|
.org-chart-node__icon {
|
||||||
font-size: 1rem;
|
font-size: var(--space-lg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.org-chart-node__name {
|
.org-chart-node__name {
|
||||||
@@ -33293,23 +33399,23 @@ html .column.drag-over * {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 0.5rem;
|
gap: var(--space-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.org-chart-node__badge {
|
.org-chart-node__badge {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0.125rem 0.5rem;
|
padding: calc(var(--space-xs) * 0.5) var(--space-sm);
|
||||||
border-radius: 999px;
|
border-radius: var(--radius-pill);
|
||||||
font-size: 0.72rem;
|
font-size: calc(var(--space-sm) + var(--space-xs) * 0.75);
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
}
|
}
|
||||||
|
|
||||||
.org-chart-node__health {
|
.org-chart-node__health {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.3rem;
|
gap: var(--space-xs);
|
||||||
font-size: 0.75rem;
|
font-size: var(--space-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.org-chart-node--has-children > .org-chart-node-card::after {
|
.org-chart-node--has-children > .org-chart-node-card::after {
|
||||||
@@ -33317,10 +33423,10 @@ html .column.drag-over * {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
bottom: -16px;
|
bottom: calc(var(--space-lg) * -1);
|
||||||
width: 1px;
|
width: 1px;
|
||||||
height: 16px;
|
height: var(--space-lg);
|
||||||
background: var(--border-color);
|
background: var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.org-chart-children {
|
.org-chart-children {
|
||||||
@@ -33328,60 +33434,60 @@ html .column.drag-over * {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 1rem;
|
gap: var(--space-lg);
|
||||||
padding-top: 1.5rem;
|
padding-top: var(--space-xl);
|
||||||
margin-top: 0.25rem;
|
margin-top: var(--space-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.org-chart-children::before {
|
.org-chart-children::before {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 24px;
|
left: var(--space-xl);
|
||||||
right: 24px;
|
right: var(--space-xl);
|
||||||
height: 1px;
|
height: 1px;
|
||||||
background: var(--border-color);
|
background: var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.org-chart-children > .org-chart-node::before {
|
.org-chart-children > .org-chart-node::before {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -24px;
|
top: calc(var(--space-xl) * -1);
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
width: 1px;
|
width: 1px;
|
||||||
height: 24px;
|
height: var(--space-xl);
|
||||||
background: var(--border-color);
|
background: var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chain-of-command-path {
|
.chain-of-command-path {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.35rem 0.5rem;
|
gap: var(--space-xs) var(--space-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chain-of-command-item {
|
.chain-of-command-item {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.4rem;
|
gap: var(--space-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chain-of-command-node {
|
.chain-of-command-node {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0.25rem 0.6rem;
|
padding: var(--space-xs) calc(var(--space-sm) + var(--space-xs) * 0.5);
|
||||||
border-radius: 999px;
|
border-radius: var(--radius-pill);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border);
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
color: var(--text-secondary);
|
color: var(--text-muted);
|
||||||
font-size: 0.78rem;
|
font-size: calc(var(--space-sm) + var(--space-xs));
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chain-of-command-node:hover:not(:disabled) {
|
.chain-of-command-node:hover:not(:disabled) {
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
color: var(--text-primary);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chain-of-command-node:disabled {
|
.chain-of-command-node:disabled {
|
||||||
@@ -33391,7 +33497,7 @@ html .column.drag-over * {
|
|||||||
|
|
||||||
.chain-of-command-node--current {
|
.chain-of-command-node--current {
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
color: var(--text-primary);
|
color: var(--text);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33401,11 +33507,26 @@ html .column.drag-over * {
|
|||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.agent-org-chart {
|
.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 {
|
.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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user