feat(FN-3238): unify header action cluster in AgentDetailView with planning

Merged changes unify the header action cluster in AgentDetailView with refined styling and component refactoring, while also adding planning disclosure UX for steps 2-3 in the PlanningModeModal — both backed by updated test coverage.

Fusion-Task-Id: FN-3238
This commit is contained in:
Fusion
2026-05-03 08:55:45 -07:00
committed by gsxdsm
parent d26d0e69c9
commit 70a0979394
3 changed files with 104 additions and 86 deletions

View File

@@ -125,6 +125,16 @@
margin-top: calc(var(--space-xs) * 0.5);
}
/* Unified right-side header action cluster */
.agent-detail-header-actions {
display: flex;
align-items: center;
justify-content: flex-end;
gap: var(--space-sm);
flex: 1 1 auto;
min-width: 0;
}
/* Lifecycle controls: compact action buttons */
.agent-detail-controls {
display: flex;
@@ -1367,13 +1377,20 @@
flex-wrap: wrap;
}
.agent-detail-controls {
.agent-detail-header-actions {
flex: 1 1 100%;
flex-wrap: wrap;
gap: calc(var(--space-xs) + var(--space-sm) * 0.25);
justify-content: space-between;
gap: var(--space-sm);
order: 3;
}
.agent-detail-controls {
flex: 1 1 auto;
flex-wrap: wrap;
gap: calc(var(--space-xs) + var(--space-sm) * 0.25);
}
.agent-detail-controls .btn--compact {
padding: calc(var(--space-sm) - var(--space-xs) * 0.5) calc(var(--space-md) - var(--space-xs) * 0.5);
font-size: var(--space-md);
@@ -1387,6 +1404,7 @@
.agent-detail-utility-actions {
flex-shrink: 0;
margin-left: auto;
}
.agent-detail-utility-actions .btn-icon {

View File

@@ -519,81 +519,83 @@ export function AgentDetailView({ agentId, projectId, onClose, addToast, onChild
</div>
</div>
{/* Lifecycle controls: compact action buttons */}
<div className="agent-detail-controls">
{/* State-dependent action buttons */}
{agent.state === "idle" && (
<>
<button className="btn btn-task-create btn--compact" onClick={() => void handleStateChange("active")} disabled={isTransitioning}>
<Play size={14} />
Start
</button>
<button className="btn btn--danger btn--compact" onClick={handleDelete}>
<Trash2 size={14} />
Delete
</button>
</>
)}
{agent.state === "active" && (
<button className="btn btn--compact" onClick={() => void handleStateChange("paused")} disabled={isTransitioning}>
<Pause size={14} />
Pause
</button>
)}
{agent.state === "paused" && (
<button className="btn btn-task-create btn--compact" onClick={() => void handleStateChange("active")} disabled={isTransitioning}>
<Play size={14} />
Resume
</button>
)}
{agent.state === "running" && (
<>
<div className="agent-detail-header-actions">
{/* Lifecycle controls: compact action buttons */}
<div className="agent-detail-controls">
{/* State-dependent action buttons */}
{agent.state === "idle" && (
<>
<button className="btn btn-task-create btn--compact" onClick={() => void handleStateChange("active")} disabled={isTransitioning}>
<Play size={14} />
Start
</button>
<button className="btn btn--danger btn--compact" onClick={handleDelete}>
<Trash2 size={14} />
Delete
</button>
</>
)}
{agent.state === "active" && (
<button className="btn btn--compact" onClick={() => void handleStateChange("paused")} disabled={isTransitioning}>
<Pause size={14} />
Pause
</button>
<button className="btn btn--danger btn--compact" onClick={() => void handleStateChange("terminated")} disabled={isTransitioning}>
<Square size={14} />
Stop
</button>
</>
)}
{agent.state === "error" && (
<>
)}
{agent.state === "paused" && (
<button className="btn btn-task-create btn--compact" onClick={() => void handleStateChange("active")} disabled={isTransitioning}>
<Play size={14} />
Retry
Resume
</button>
<button className="btn btn--danger btn--compact" onClick={() => void handleStateChange("terminated")} disabled={isTransitioning}>
<Square size={14} />
Stop
</button>
</>
)}
{agent.state === "terminated" && (
<>
<button className="btn btn-task-create btn--compact" onClick={() => void handleStateChange("active")} disabled={isTransitioning}>
<Play size={14} />
Start
</button>
<button className="btn btn--danger btn--compact" onClick={handleDelete}>
<Trash2 size={14} />
Delete
</button>
</>
)}
</div>
)}
{agent.state === "running" && (
<>
<button className="btn btn--compact" onClick={() => void handleStateChange("paused")} disabled={isTransitioning}>
<Pause size={14} />
Pause
</button>
<button className="btn btn--danger btn--compact" onClick={() => void handleStateChange("terminated")} disabled={isTransitioning}>
<Square size={14} />
Stop
</button>
</>
)}
{agent.state === "error" && (
<>
<button className="btn btn-task-create btn--compact" onClick={() => void handleStateChange("active")} disabled={isTransitioning}>
<Play size={14} />
Retry
</button>
<button className="btn btn--danger btn--compact" onClick={() => void handleStateChange("terminated")} disabled={isTransitioning}>
<Square size={14} />
Stop
</button>
</>
)}
{agent.state === "terminated" && (
<>
<button className="btn btn-task-create btn--compact" onClick={() => void handleStateChange("active")} disabled={isTransitioning}>
<Play size={14} />
Start
</button>
<button className="btn btn--danger btn--compact" onClick={handleDelete}>
<Trash2 size={14} />
Delete
</button>
</>
)}
</div>
{/* Utility actions: refresh + close */}
<div className="agent-detail-utility-actions">
<button className="btn-icon" onClick={() => void loadAgent()} title="Refresh">
<RefreshCw size={16} />
</button>
{!inline && (
<button className="btn-icon" onClick={onClose} aria-label="Close" title="Close">
<X size={20} />
{/* Utility actions: refresh + close */}
<div className="agent-detail-utility-actions">
<button className="btn-icon" onClick={() => void loadAgent()} title="Refresh">
<RefreshCw size={16} />
</button>
)}
{!inline && (
<button className="btn-icon" onClick={onClose} aria-label="Close" title="Close">
<X size={20} />
</button>
)}
</div>
</div>
</div>

View File

@@ -808,7 +808,7 @@ describe("AgentDetailView", () => {
});
});
it("renders lifecycle controls in compact layout with agent-detail-controls container", async () => {
it("groups lifecycle and utility controls under a shared header action cluster", async () => {
render(
<AgentDetailView
agentId="agent-001"
@@ -818,32 +818,30 @@ describe("AgentDetailView", () => {
);
await waitFor(() => {
// Verify lifecycle controls are in the compact controls container
const controlsContainer = document.querySelector(".agent-detail-controls");
const headerActions = document.querySelector(".agent-detail-header-actions");
expect(headerActions).toBeTruthy();
const controlsContainer = headerActions?.querySelector(".agent-detail-controls");
expect(controlsContainer).toBeTruthy();
expect(controlsContainer?.querySelector(".btn--compact")).toBeTruthy();
});
});
it("renders utility actions in separate container", async () => {
render(
<AgentDetailView
agentId="agent-001"
onClose={vi.fn()}
addToast={vi.fn()}
/>
);
await waitFor(() => {
// Verify utility actions are in the separate utility container
const utilityContainer = document.querySelector(".agent-detail-utility-actions");
const utilityContainer = headerActions?.querySelector(".agent-detail-utility-actions");
expect(utilityContainer).toBeTruthy();
// Refresh and Close buttons should be in utility container
expect(utilityContainer?.querySelector('[title="Refresh"]')).toBeTruthy();
expect(utilityContainer?.querySelector('[title="Close"]')).toBeTruthy();
});
});
it("keeps desktop header actions on one row and mobile wraps them safely", () => {
const stylesContent = loadAllAppCss();
expect(stylesContent).toContain(".agent-detail-header-actions {");
expect(stylesContent).toContain("justify-content: flex-end;");
const mobileHeaderActionsBlock = /@media \(max-width: 768px\)\s*\{[\s\S]*?\.agent-detail-header-actions\s*\{[\s\S]*?flex-wrap: wrap;[\s\S]*?\}/;
expect(stylesContent).toMatch(mobileHeaderActionsBlock);
});
it("shows statistics section on dashboard", async () => {
render(
<AgentDetailView