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:
Fusion
2026-05-04 21:06:30 -07:00
committed by gsxdsm
parent d3137ba594
commit bc6e1276e3
4 changed files with 61 additions and 23 deletions

View File

@@ -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);

View File

@@ -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 {

View File

@@ -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 && (

View File

@@ -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 () => {