feat(FN-3414): enable heartbeat tool mode and compact mobile header styling
This merge delivers three distinct features: a compact mobile header row for the dashboard with corresponding regression tests, a plugin slot registry with Docker node provisioning for the mesh network, and heartbeat agent sessions now support tool mode for expanded capability access. Fusion-Task-Id: FN-3414
This commit is contained in:
@@ -203,6 +203,19 @@ describe("Agent CSS classes", () => {
|
||||
});
|
||||
|
||||
// Verify AgentDetailView classes
|
||||
it("encodes compact mobile agent detail header layout contracts", () => {
|
||||
const css = fs.readFileSync(path.join(__dirname, "../components/AgentDetailView.css"), "utf-8");
|
||||
const mobileStart = css.indexOf("@media (max-width: 768px)");
|
||||
const mobileCss = mobileStart >= 0 ? css.slice(mobileStart) : "";
|
||||
|
||||
expect(mobileCss).toContain(".agent-detail-header {");
|
||||
expect(mobileCss).toContain("grid-template-columns: minmax(0, 1fr) auto;");
|
||||
expect(mobileCss).not.toContain("grid-template-rows: auto auto;");
|
||||
expect(mobileCss).toContain(".agent-detail-header-actions {");
|
||||
expect(mobileCss).toContain("grid-column: 2;");
|
||||
expect(mobileCss).toContain(".agent-detail-mobile-icon-control .agent-detail-control-label {");
|
||||
});
|
||||
|
||||
it("should define AgentDetailView CSS classes", () => {
|
||||
expect(hasClass(".agent-detail-overlay")).toBe(true);
|
||||
expect(hasClass(".agent-detail-modal")).toBe(true);
|
||||
|
||||
@@ -1403,14 +1403,16 @@
|
||||
|
||||
.agent-detail-header {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
grid-template-rows: auto auto;
|
||||
gap: var(--space-xs);
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
column-gap: var(--space-xs);
|
||||
row-gap: 0;
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
padding-top: max(var(--space-sm), env(safe-area-inset-top, 0));
|
||||
}
|
||||
|
||||
.agent-detail-identity {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
@@ -1440,16 +1442,16 @@
|
||||
}
|
||||
|
||||
.agent-detail-header-actions {
|
||||
grid-row: 2;
|
||||
flex: 1 1 auto;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-xs);
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
flex: 0 0 auto;
|
||||
justify-content: flex-end;
|
||||
gap: calc(var(--space-xs) * 0.5);
|
||||
}
|
||||
|
||||
.agent-detail-controls {
|
||||
flex: 1 1 auto;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-xs);
|
||||
flex: 0 0 auto;
|
||||
gap: calc(var(--space-xs) * 0.5);
|
||||
}
|
||||
|
||||
.agent-detail-controls .btn--compact {
|
||||
@@ -1458,6 +1460,24 @@
|
||||
min-height: calc(var(--space-lg) + var(--space-md) + var(--space-xs));
|
||||
}
|
||||
|
||||
.agent-detail-controls .agent-detail-mobile-icon-control {
|
||||
min-width: calc(var(--space-lg) + var(--space-md) + var(--space-xs));
|
||||
padding-left: calc(var(--space-sm) - var(--space-xs) * 0.5);
|
||||
padding-right: calc(var(--space-sm) - var(--space-xs) * 0.5);
|
||||
}
|
||||
|
||||
.agent-detail-controls .agent-detail-mobile-icon-control .agent-detail-control-label {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.agent-detail-controls .btn--compact svg {
|
||||
width: calc(var(--space-md) + var(--space-xs) * 0.5);
|
||||
height: calc(var(--space-md) + var(--space-xs) * 0.5);
|
||||
@@ -1465,7 +1485,7 @@
|
||||
|
||||
.agent-detail-utility-actions {
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.agent-detail-utility-actions .btn-icon {
|
||||
|
||||
@@ -545,22 +545,22 @@ export function AgentDetailView({ agentId, projectId, onClose, addToast, onChild
|
||||
</>
|
||||
)}
|
||||
{agent.state === "active" && (
|
||||
<button className="btn btn--compact" onClick={() => void handleStateChange("paused")} disabled={isTransitioning}>
|
||||
<button className="btn btn--compact agent-detail-mobile-icon-control" onClick={() => void handleStateChange("paused")} disabled={isTransitioning} aria-label="Pause">
|
||||
<Pause size={14} />
|
||||
Pause
|
||||
<span className="agent-detail-control-label">Pause</span>
|
||||
</button>
|
||||
)}
|
||||
{agent.state === "paused" && (
|
||||
<button className="btn btn-task-create btn--compact" onClick={() => void handleStateChange("active")} disabled={isTransitioning}>
|
||||
<button className="btn btn-task-create btn--compact agent-detail-mobile-icon-control" onClick={() => void handleStateChange("active")} disabled={isTransitioning} aria-label="Resume">
|
||||
<Play size={14} />
|
||||
Resume
|
||||
<span className="agent-detail-control-label">Resume</span>
|
||||
</button>
|
||||
)}
|
||||
{agent.state === "running" && (
|
||||
<>
|
||||
<button className="btn btn--compact" onClick={() => void handleStateChange("paused")} disabled={isTransitioning}>
|
||||
<button className="btn btn--compact agent-detail-mobile-icon-control" onClick={() => void handleStateChange("paused")} disabled={isTransitioning} aria-label="Pause">
|
||||
<Pause size={14} />
|
||||
Pause
|
||||
<span className="agent-detail-control-label">Pause</span>
|
||||
</button>
|
||||
<button className="btn btn--danger btn--compact" onClick={() => void handleStateChange("terminated")} disabled={isTransitioning}>
|
||||
<Square size={14} />
|
||||
@@ -596,7 +596,7 @@ export function AgentDetailView({ agentId, projectId, onClose, addToast, onChild
|
||||
|
||||
{/* Utility actions: refresh + close */}
|
||||
<div className="agent-detail-utility-actions">
|
||||
<button className="btn-icon" onClick={() => void loadAgent()} title="Refresh">
|
||||
<button className="btn-icon" onClick={() => void loadAgent()} title="Refresh" aria-label="Refresh">
|
||||
<RefreshCw size={16} />
|
||||
</button>
|
||||
{!inline && (
|
||||
|
||||
@@ -302,8 +302,12 @@ describe("AgentDetailView", () => {
|
||||
await userEvent.click(screen.getByLabelText("Back to agents"));
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
|
||||
const identityContainer = document.querySelector(".agent-detail-identity");
|
||||
const header = document.querySelector(".agent-detail-header");
|
||||
const identityContainer = header?.querySelector(".agent-detail-identity");
|
||||
const actionsContainer = header?.querySelector(".agent-detail-header-actions");
|
||||
expect(identityContainer?.querySelector(".agent-detail-inline-back")).toBeTruthy();
|
||||
expect(actionsContainer?.querySelector('[aria-label="Refresh"]')).toBeTruthy();
|
||||
expect(actionsContainer?.querySelector(".agent-detail-mobile-icon-control")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("keeps modal mode as dialog with close button", async () => {
|
||||
@@ -902,7 +906,7 @@ describe("AgentDetailView", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps desktop header actions on one row and mobile uses explicit two-row header contracts", () => {
|
||||
it("keeps mobile inline header controls on the same row as identity", () => {
|
||||
const stylesContent = loadAllAppCss();
|
||||
|
||||
expect(stylesContent).toContain(".agent-detail-header-actions {");
|
||||
@@ -911,11 +915,12 @@ describe("AgentDetailView", () => {
|
||||
|
||||
expect(stylesContent).toContain("@media (max-width: 768px)");
|
||||
expect(stylesContent).toContain(".agent-detail-header {");
|
||||
expect(stylesContent).toContain("grid-template-rows: auto auto;");
|
||||
expect(stylesContent).toContain("grid-template-columns: minmax(0, 1fr) auto;");
|
||||
expect(stylesContent).toContain(".agent-detail-identity {");
|
||||
expect(stylesContent).toContain("grid-row: 1;");
|
||||
expect(stylesContent).toContain("grid-column: 1;");
|
||||
expect(stylesContent).toContain(".agent-detail-header-actions {");
|
||||
expect(stylesContent).toContain("grid-row: 2;");
|
||||
expect(stylesContent).toContain("grid-column: 2;");
|
||||
expect(stylesContent).toContain(".agent-detail-mobile-icon-control .agent-detail-control-label {");
|
||||
});
|
||||
|
||||
it("shows statistics section on dashboard", async () => {
|
||||
|
||||
Reference in New Issue
Block a user