feat(FN-4065): fix sidebar action overflow in AgentsView

Fixes sidebar action containment in the dashboard AgentsView component and adds test coverage for the sidebar action overflow contract.

Fusion-Task-Id: FN-4065
This commit is contained in:
Fusion
2026-05-12 01:07:35 -07:00
committed by gsxdsm
parent 079772a106
commit a65b8019d6
3 changed files with 126 additions and 82 deletions

View File

@@ -698,16 +698,30 @@
}
.agent-card-actions {
display: flex;
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: var(--space-sm);
align-items: center;
flex-wrap: nowrap;
align-items: start;
width: 100%;
min-width: 0;
container-type: inline-size;
container-name: agent-card-actions;
}
.agent-card-actions-group {
display: flex;
flex-wrap: wrap;
gap: var(--space-sm);
align-items: center;
min-width: 0;
}
.agent-card-actions-group--secondary {
justify-content: flex-end;
}
.agent-card-actions .btn {
flex-shrink: 0;
flex: 0 0 auto;
white-space: nowrap;
}
@@ -715,21 +729,29 @@
gap: var(--space-xs);
}
.agents-split-sidebar .agent-card-actions-group {
gap: var(--space-xs);
}
.agents-split-sidebar .agent-card-actions .btn {
gap: var(--space-xs);
padding: var(--space-xs) var(--space-sm);
}
@container agent-card-actions (max-width: calc(var(--space-2xl) * 9)) {
.agents-split-sidebar .agent-card-actions {
grid-template-columns: minmax(0, 1fr);
}
.agents-split-sidebar .agent-card-actions-group--secondary {
justify-content: flex-start;
}
.agents-split-sidebar .agent-card-actions .agent-card-action-label {
display: none;
}
}
.agent-card-details-btn {
margin-left: auto;
}
.agent-card--clickable {
cursor: pointer;
}

View File

@@ -1368,94 +1368,98 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
</div>
<div className="agent-card-actions">
{agent.state === "idle" && (
<button
className="btn btn-sm"
onClick={() => void handleStateChange(agent.id, "active")}
disabled={transitioningAgentIds.has(agent.id)}
title="Activate"
>
<Play size={14} /> <span className="agent-card-action-label">Start</span>
</button>
)}
{agent.state === "active" && (
<>
<div className="agent-card-actions-group agent-card-actions-group--primary">
{agent.state === "idle" && (
<button
className="btn btn-sm"
onClick={() => void handleRunHeartbeat(agent.id, agent.name)}
onClick={() => void handleStateChange(agent.id, "active")}
disabled={transitioningAgentIds.has(agent.id)}
title="Run Now"
aria-label={`Run now for ${agent.name}`}
title="Activate"
>
<Activity size={14} /> <span className="agent-card-action-label">Run Now</span>
<Play size={14} /> <span className="agent-card-action-label">Start</span>
</button>
)}
{agent.state === "active" && (
<>
<button
className="btn btn-sm"
onClick={() => void handleRunHeartbeat(agent.id, agent.name)}
disabled={transitioningAgentIds.has(agent.id)}
title="Run Now"
aria-label={`Run now for ${agent.name}`}
>
<Activity size={14} /> <span className="agent-card-action-label">Run Now</span>
</button>
<button
className="btn btn-sm"
onClick={() => void handleStateChange(agent.id, "paused")}
disabled={transitioningAgentIds.has(agent.id)}
title="Pause"
>
<Pause size={14} /> <span className="agent-card-action-label">Pause</span>
</button>
</>
)}
{agent.state === "paused" && (
<button
className="btn btn-sm"
onClick={() => void handleStateChange(agent.id, "paused")}
onClick={() => void handleStateChange(agent.id, "active")}
disabled={transitioningAgentIds.has(agent.id)}
title="Pause"
title="Resume"
>
<Pause size={14} /> <span className="agent-card-action-label">Pause</span>
<Play size={14} /> <span className="agent-card-action-label">Resume</span>
</button>
</>
)}
{agent.state === "paused" && (
<button
className="btn btn-sm"
onClick={() => void handleStateChange(agent.id, "active")}
disabled={transitioningAgentIds.has(agent.id)}
title="Resume"
>
<Play size={14} /> <span className="agent-card-action-label">Resume</span>
</button>
)}
{agent.state === "running" && (
<>
)}
{agent.state === "running" && (
<>
<button
className="btn btn-sm"
onClick={() => openAgentDetail(agent.id, { initialTab: "runs", initialRunId: null, preferActiveRun: true })}
title="View live run details"
aria-label={`View live run details for ${agent.name}`}
>
<Activity size={14} /> <span className="agent-card-action-label">Running</span>
</button>
<button
className="btn btn-sm"
onClick={() => void handleStateChange(agent.id, "paused")}
disabled={transitioningAgentIds.has(agent.id)}
title="Pause"
>
<Pause size={14} /> <span className="agent-card-action-label">Pause</span>
</button>
</>
)}
{agent.state === "error" && (
<button
className="btn btn-sm"
onClick={() => openAgentDetail(agent.id, { initialTab: "runs", initialRunId: null, preferActiveRun: true })}
title="View live run details"
aria-label={`View live run details for ${agent.name}`}
>
<Activity size={14} /> <span className="agent-card-action-label">Running</span>
</button>
<button
className="btn btn-sm"
onClick={() => void handleStateChange(agent.id, "paused")}
onClick={() => void handleStateChange(agent.id, "active")}
disabled={transitioningAgentIds.has(agent.id)}
title="Pause"
title="Retry"
>
<Pause size={14} /> <span className="agent-card-action-label">Pause</span>
<Play size={14} /> <span className="agent-card-action-label">Retry</span>
</button>
</>
)}
{agent.state === "error" && (
)}
</div>
<div className="agent-card-actions-group agent-card-actions-group--secondary">
<button
className="btn btn-sm"
onClick={() => void handleStateChange(agent.id, "active")}
disabled={transitioningAgentIds.has(agent.id)}
title="Retry"
className="btn btn-sm agent-card-details-btn"
onClick={() => openAgentDetail(agent.id)}
title={`View details for ${agent.name}`}
aria-label={`View details for ${agent.name}`}
>
<Play size={14} /> <span className="agent-card-action-label">Retry</span>
<Info size={14} /> <span className="agent-card-action-label">Details</span>
</button>
)}
<button
className="btn btn-sm agent-card-details-btn"
onClick={() => openAgentDetail(agent.id)}
title={`View details for ${agent.name}`}
aria-label={`View details for ${agent.name}`}
>
<Info size={14} /> <span className="agent-card-action-label">Details</span>
</button>
{(agent.state === "idle" || agent.state === "paused") && (
<button
className="btn btn-sm btn-danger"
onClick={() => void handleDelete(agent.id, agent.name)}
title="Delete"
>
<Trash2 size={14} /> <span className="agent-card-action-label">Delete</span>
</button>
)}
{(agent.state === "idle" || agent.state === "paused") && (
<button
className="btn btn-sm btn-danger"
onClick={() => void handleDelete(agent.id, agent.name)}
title="Delete"
>
<Trash2 size={14} /> <span className="agent-card-action-label">Delete</span>
</button>
)}
</div>
</div>
</div>
);

View File

@@ -651,18 +651,36 @@ describe("AgentsView", () => {
expect(screen.getAllByText("Details").length).toBeGreaterThanOrEqual(4);
});
it("keeps a visible icon affordance on View Details buttons when labels are compacted", async () => {
render(<AgentsView addToast={mockAddToast} />);
it("keeps a visible icon affordance on split-sidebar action buttons when labels are compacted", async () => {
const { container } = render(<AgentsView addToast={mockAddToast} />);
const detailsButton = await screen.findByRole("button", { name: "View details for Test Agent 1" });
const sidebarCard = await waitFor(() => {
const card = container.querySelector(".agents-split-sidebar .agent-card");
expect(card).toBeTruthy();
return card as HTMLElement;
});
const actions = sidebarCard.querySelector(".agent-card-actions");
const primaryGroup = sidebarCard.querySelector(".agent-card-actions-group--primary");
const secondaryGroup = sidebarCard.querySelector(".agent-card-actions-group--secondary");
const detailsButton = within(sidebarCard).getByRole("button", { name: "View details for Test Agent 1" });
expect(actions).toBeTruthy();
expect(primaryGroup).toBeTruthy();
expect(secondaryGroup).toBeTruthy();
expect(primaryGroup?.querySelector("button")).toBeTruthy();
expect(secondaryGroup?.contains(detailsButton)).toBe(true);
expect(detailsButton.querySelector("svg")).toBeTruthy();
expect(sidebarCard.querySelectorAll(".agent-card-action-label").length).toBeGreaterThan(0);
});
it("hides split-sidebar action labels only within an agent-card-actions container query", () => {
it("uses a grid-and-wrap containment contract for split-sidebar action rows", () => {
const css = loadAllAppCss();
expect(css).toMatch(/\.agent-card-actions\s*\{[^}]*display:\s*grid;[^}]*grid-template-columns:\s*minmax\(0, 1fr\) auto;[^}]*width:\s*100%;[^}]*min-width:\s*0;[^}]*\}/);
expect(css).toMatch(/\.agent-card-actions-group\s*\{[^}]*display:\s*flex;[^}]*flex-wrap:\s*wrap;[^}]*min-width:\s*0;[^}]*\}/);
expect(css).not.toContain(".agents-split-sidebar .agent-card-actions .agent-card-action-label {\n display: none;\n}");
expect(css).toContain("@container agent-card-actions (max-width: calc(var(--space-2xl) * 9))");
expect(css).toContain(".agents-split-sidebar .agent-card-actions {\n grid-template-columns: minmax(0, 1fr);\n }");
expect(css).toContain(".agents-split-sidebar .agent-card-actions .agent-card-action-label {\n display: none;\n }");
});