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:
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user