FN-7340: align footer concurrency marker geometry

Align footer Engine Controls current-use dots with Command Center slider geometry.

- Mirror Command Center range sizing, touch behavior, and mobile thumb geometry in the footer controls.
- Cover the shared marker geometry contract and pending-cap marker recalculation in EngineControlMenu tests.
- Document the footer/Command Center alignment requirement and add a patch changeset for the published CLI package.

Files changed:
 .changeset/fn-7340-footer-concurrency-dot-alignment.md    |  7 ++
 docs/dashboard-guide.md                            |  3 +-
 packages/dashboard/app/components/EngineControlMenu.css | 28 ++++++-
 packages/dashboard/app/components/__tests__/EngineControlMenu.test.tsx           | 85 ++++++++++++++++++++++
 4 files changed, 121 insertions(+), 2 deletions(-)

Fusion-Task-Id: FN-7340

Fusion-Task-Lineage: 5cb4d829-2b70-429c-b9da-0c559fdbf131

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-06-30 22:45:06 -07:00
parent 45b1ee6f9c
commit d786e7b9c9
4 changed files with 121 additions and 2 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Align footer concurrency current-use dots with Command Center sliders.
category: fix
dev: Mirrors Command Center range geometry in the footer Engine Controls popup while preserving current/cap utilization math.

View File

@@ -1087,7 +1087,8 @@ Use this panel when upgrading a project with pre-FN-6245/FN-6277 in-review rows
<!-- FNXC:ExecutorStatusBar 2026-06-29-00:00: FN-7235 documents that footer concurrency current-use dots use the same absolute utilization math as Command Center controls, so running-agent counts visually align with the slider track instead of the editable slider minimum. --> <!-- FNXC:ExecutorStatusBar 2026-06-29-00:00: FN-7235 documents that footer concurrency current-use dots use the same absolute utilization math as Command Center controls, so running-agent counts visually align with the slider track instead of the editable slider minimum. -->
<!-- FNXC:ExecutorStatusBar 2026-06-29-19:09: FN-7248 makes footer concurrency edits confirmation-gated like Command Center. Closing the popover, outside-clicking, pressing Escape, dismissing the backdrop, or unmounting must revert unconfirmed slider edits instead of saving them. --> <!-- FNXC:ExecutorStatusBar 2026-06-29-19:09: FN-7248 makes footer concurrency edits confirmation-gated like Command Center. Closing the popover, outside-clicking, pressing Escape, dismissing the backdrop, or unmounting must revert unconfirmed slider edits instead of saving them. -->
<!-- FNXC:ExecutorStatusBar 2026-06-30-16:42: FN-7273 keeps the footer Engine Controls popover usable on mobile, narrow tablets, and tablet landscape by documenting that constrained screens use a full-width bottom panel above both fixed bottom bars instead of the compact desktop anchor. --> <!-- FNXC:ExecutorStatusBar 2026-06-30-16:42: FN-7273 keeps the footer Engine Controls popover usable on mobile, narrow tablets, and tablet landscape by documenting that constrained screens use a full-width bottom panel above both fixed bottom bars instead of the compact desktop anchor. -->
The global AI engine stop/start control and triage pause/resume control live in the executor footer status bar rather than the header. Select the small engine-controls button beside the executor state badge, or select the state text such as **Running**, to open the footer popover. The popover includes **Stop AI engine** / **Start AI engine**, **Pause triage** / **Resume scheduling**, and live scheduler sliders for max concurrent tasks, max triage concurrency, and max worktrees. On mobile, narrow tablets, and tablet landscape, the same controls open as a full-width bottom panel above the executor footer and mobile navigation so the close button and sliders remain reachable. Use the visible **Close engine controls** X button, Escape, or outside-click to dismiss it. The global and current-project concurrency sliders also show how many agents are running, including actively-triaging planners (`triage` + `planning`, not paused), and a dot on the slider track for current use. The dot uses absolute utilization (`running / cap`) rather than range-slider coordinates, so one running agent renders above the start of the track, zero stays at the start, and over-cap usage clamps to the end. Changed concurrency slider values ask for confirmation after the value settles. Confirming saves the global cap through `/api/global-concurrency` and project caps through `/api/settings`; cancel, backdrop dismissal, Escape, close, outside-click, or unmount reverts unconfirmed slider edits without saving. Multiple changed project sliders within one debounce window are summarized in one confirmation dialog, matching Command Center behavior. <!-- FNXC:GlobalConcurrencyControls 2026-06-30-21:45: FN-7340 keeps footer current-use marker geometry aligned with Command Center range-input geometry while preserving the FN-7235 absolute-utilization math. -->
The global AI engine stop/start control and triage pause/resume control live in the executor footer status bar rather than the header. Select the small engine-controls button beside the executor state badge, or select the state text such as **Running**, to open the footer popover. The popover includes **Stop AI engine** / **Start AI engine**, **Pause triage** / **Resume scheduling**, and live scheduler sliders for max concurrent tasks, max triage concurrency, and max worktrees. On mobile, narrow tablets, and tablet landscape, the same controls open as a full-width bottom panel above the executor footer and mobile navigation so the close button and sliders remain reachable. Use the visible **Close engine controls** X button, Escape, or outside-click to dismiss it. The global and current-project concurrency sliders also show how many agents are running, including actively-triaging planners (`triage` + `planning`, not paused), and a dot on the slider track for current use. The dot uses absolute utilization (`running / cap`) rather than range-slider coordinates and shares the same track/thumb geometry as Command Center sliders, so one running agent renders above the start of the track, zero stays at the start, and over-cap usage clamps to the end. Changed concurrency slider values ask for confirmation after the value settles. Confirming saves the global cap through `/api/global-concurrency` and project caps through `/api/settings`; cancel, backdrop dismissal, Escape, close, outside-click, or unmount reverts unconfirmed slider edits without saving. Multiple changed project sliders within one debounce window are summarized in one confirmation dialog, matching Command Center behavior.
<!-- FNXC:ExecutorStatusBar 2026-06-27-00:00: FN-7163 makes footer stats loading initial-only so routine heartbeat refreshes keep the populated footer and open concurrency popover mounted instead of blinking to the loading branch. --> <!-- FNXC:ExecutorStatusBar 2026-06-27-00:00: FN-7163 makes footer stats loading initial-only so routine heartbeat refreshes keep the populated footer and open concurrency popover mounted instead of blinking to the loading branch. -->
Brief, single-poll executor stats fetch blips keep showing the last good footer stats instead of flashing **Connecting…**. Routine executor stats heartbeats also keep the populated footer mounted after initial load, so an open engine/concurrency popover stays open while counts refresh. The footer only switches to **Connecting…** for sustained suspension-like stats failures, or to an explicit error state for non-transient failures. Brief, single-poll executor stats fetch blips keep showing the last good footer stats instead of flashing **Connecting…**. Routine executor stats heartbeats also keep the populated footer mounted after initial load, so an open engine/concurrency popover stays open while counts refresh. The footer only switches to **Connecting…** for sustained suspension-like stats failures, or to an explicit error state for non-transient failures.

View File

@@ -138,8 +138,14 @@
} }
.engine-control-menu__range { .engine-control-menu__range {
width: 100%; /*
FNXC:GlobalConcurrencyControls 2026-06-30-21:45:
Footer current-use dots must align with the Command Center concurrency sliders. Mirror the dashboard range-input geometry instead of nudging the marker so the shared current/cap marker math lands on the same effective track/thumb positions across footer and dashboard surfaces.
*/
inline-size: 100%;
min-block-size: var(--space-xl);
accent-color: var(--accent); accent-color: var(--accent);
touch-action: pan-y;
} }
/* FNXC:GlobalConcurrencyControls 2026-06-26-06:26: The current-use marker reuses the global .status-dot convention and is positioned with logical inset properties so utilization is visible on LTR/RTL slider tracks without intercepting drag input. */ /* FNXC:GlobalConcurrencyControls 2026-06-26-06:26: The current-use marker reuses the global .status-dot convention and is positioned with logical inset properties so utilization is visible on LTR/RTL slider tracks without intercepting drag input. */
@@ -164,6 +170,26 @@
bottom: calc(100% + var(--space-xs)); bottom: calc(100% + var(--space-xs));
} }
@media (max-width: 768px) {
.engine-control-menu__range-wrap {
--engine-control-range-thumb-size: var(--space-xl);
}
.engine-control-menu__range {
min-block-size: var(--space-2xl);
}
.engine-control-menu__range::-webkit-slider-thumb {
inline-size: var(--space-xl);
block-size: var(--space-xl);
}
.engine-control-menu__range::-moz-range-thumb {
inline-size: var(--space-xl);
block-size: var(--space-xl);
}
}
@media (max-width: 1024px) { @media (max-width: 1024px) {
.engine-control-menu__popover { .engine-control-menu__popover {
/* /*

View File

@@ -1,8 +1,20 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, fireEvent, waitFor, act, cleanup } from "@testing-library/react"; import { render, screen, fireEvent, waitFor, act, cleanup } from "@testing-library/react";
import { EngineControlMenu } from "../EngineControlMenu"; import { EngineControlMenu } from "../EngineControlMenu";
import { ConfirmDialogProvider } from "../../hooks/useConfirm"; import { ConfirmDialogProvider } from "../../hooks/useConfirm";
const engineControlMenuCss = readFileSync(
join(process.cwd(), "app/components/EngineControlMenu.css"),
"utf8",
);
const commandCenterControlsCss = readFileSync(
join(process.cwd(), "app/components/command-center/CommandCenterControls.css"),
"utf8",
);
const defaultSettings = { const defaultSettings = {
maxConcurrent: 2, maxConcurrent: 2,
maxTriageConcurrent: 1, maxTriageConcurrent: 1,
@@ -65,6 +77,19 @@ function expectUseMarkerPct(testId: string, pct: string) {
expect(screen.getByTestId(testId).style.getPropertyValue("--use-pct")).toBe(pct); expect(screen.getByTestId(testId).style.getPropertyValue("--use-pct")).toBe(pct);
} }
function expectFooterUseOffset(testId: string, ratio: number) {
expect(screen.getByTestId(testId).style.getPropertyValue("--use-offset")).toBe(
`calc((var(--engine-control-range-thumb-size) / 2) + ((100% - var(--engine-control-range-thumb-size)) * ${ratio}))`,
);
}
function cssRule(css: string, selector: string) {
const escapedSelector = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const match = css.match(new RegExp(`${escapedSelector}\\s*\\{([\\s\\S]*?)\\}`));
expect(match, `Expected CSS rule for ${selector}`).not.toBeNull();
return match?.[1] ?? "";
}
describe("EngineControlMenu", () => { describe("EngineControlMenu", () => {
beforeEach(() => { beforeEach(() => {
vi.useRealTimers(); vi.useRealTimers();
@@ -89,6 +114,36 @@ describe("EngineControlMenu", () => {
vi.useRealTimers(); vi.useRealTimers();
}); });
it("matches the Command Center slider geometry contract for footer current-use markers", () => {
const footerWrap = cssRule(engineControlMenuCss, ".engine-control-menu__range-wrap");
const footerRange = cssRule(engineControlMenuCss, ".engine-control-menu__range");
const footerMarker = cssRule(engineControlMenuCss, ".engine-control-menu__use-marker");
const commandWrap = cssRule(commandCenterControlsCss, ".cc-controls-range-wrap");
const commandTouchSliderRule = commandCenterControlsCss.match(/\.cc-controls-slider input\[type="range"\],\n\.cc-controls-touch-slider\s*\{([\s\S]*?)\}/)?.[1] ?? "";
const commandMarker = cssRule(commandCenterControlsCss, ".cc-controls-use-marker");
expect(footerWrap).toContain("--engine-control-range-thumb-size: var(--space-md);");
expect(commandWrap).toContain("--cc-controls-range-thumb-size: var(--space-md);");
expect(footerWrap).toContain("position: relative;");
expect(footerWrap).toContain("display: flex;");
expect(footerWrap).toContain("align-items: center;");
expect(footerRange).toContain("inline-size: 100%;");
expect(footerRange).toContain("min-block-size: var(--space-xl);");
expect(footerRange).toContain("accent-color: var(--accent);");
expect(footerRange).toContain("touch-action: pan-y;");
expect(commandTouchSliderRule).toContain("inline-size: 100%;");
expect(commandTouchSliderRule).toContain("min-block-size: var(--space-xl);");
expect(commandTouchSliderRule).toContain("accent-color: var(--accent);");
expect(commandTouchSliderRule).toContain("touch-action: pan-y;");
for (const declaration of ["position: absolute;", "inset-block-start: 50%;", "inset-inline-start: var(--use-offset, var(--use-pct));", "transform: translate(-50%, -50%);", "pointer-events: none;"]) {
expect(footerMarker).toContain(declaration);
expect(commandMarker).toContain(declaration);
}
expect(engineControlMenuCss).toContain("@media (max-width: 768px)");
expect(engineControlMenuCss).toContain("--engine-control-range-thumb-size: var(--space-xl);");
});
it("renders an explicit close button when opened", async () => { it("renders an explicit close button when opened", async () => {
await openMenu(); await openMenu();
@@ -570,6 +625,36 @@ describe("EngineControlMenu", () => {
expect(screen.getByTestId("engine-control-project-running")).toHaveTextContent("0 running (this project)"); expect(screen.getByTestId("engine-control-project-running")).toHaveTextContent("0 running (this project)");
expect(screen.getByTestId("engine-control-global-use-marker")).toHaveStyle({ "--use-pct": "0%" }); expect(screen.getByTestId("engine-control-global-use-marker")).toHaveStyle({ "--use-pct": "0%" });
expect(screen.getByTestId("engine-control-project-use-marker")).toHaveStyle({ "--use-pct": "0%" }); expect(screen.getByTestId("engine-control-project-use-marker")).toHaveStyle({ "--use-pct": "0%" });
expectFooterUseOffset("engine-control-global-use-marker", 0);
expectFooterUseOffset("engine-control-project-use-marker", 0);
});
it("recomputes footer project marker positions from the visible pending cap", async () => {
legacyMocks.fetchSettings.mockResolvedValue({
...defaultSettings,
maxConcurrent: 60,
});
mockGlobalConcurrency({
globalMaxConcurrent: 48,
currentlyActive: 16,
projectsActive: { proj_123: 30 },
});
await openMenu();
await screen.findByLabelText(/maximum concurrent agents across all projects/i);
const maxConcurrent = screen.getByLabelText(/max concurrent tasks/i);
vi.useFakeTimers();
expectUseMarkerPct("engine-control-global-use-marker", `${(16 / 48) * 100}%`);
expectUseMarkerPct("engine-control-project-use-marker", "50%");
fireEvent.change(maxConcurrent, { target: { value: "50" } });
expectUseMarkerPct("engine-control-global-use-marker", `${(16 / 48) * 100}%`);
expectUseMarkerPct("engine-control-project-use-marker", "60%");
expectFooterUseOffset("engine-control-global-use-marker", 16 / 48);
expectFooterUseOffset("engine-control-project-use-marker", 30 / 50);
}); });
it("suppresses footer running counts and markers while utilization is loading", async () => { it("suppresses footer running counts and markers while utilization is loading", async () => {