FN-7235: align footer concurrency use markers

Align footer concurrency indicators with absolute running-agent utilization.

- Change footer use-marker ratio math to use current running count over configured cap.
- Cover zero, one-active, mid-track, over-cap, loading, and error marker states in EngineControlMenu tests.
- Document the footer marker behavior and add a patch changeset for the published CLI package.

Files changed:
 .changeset/fn-7235-footer-concurrency-marker.md    |  7 +++
 docs/dashboard-guide.md                            |  3 +-
 .../dashboard/app/components/EngineControlMenu.tsx | 14 +++--
 .../__tests__/EngineControlMenu.test.tsx           | 70 +++++++++++++++++++---
 4 files changed, 79 insertions(+), 15 deletions(-)

Fusion-Task-Id: FN-7235

Fusion-Task-Lineage: 0dda1277-2d89-4195-882c-6488dfc67288

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-06-29 13:44:09 -07:00
parent a84afc178f
commit 7b43f73327
4 changed files with 79 additions and 15 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Footer concurrency markers now line up with the running-agent counts.
category: fix
dev: Align EngineControlMenu current-use marker math with CommandCenterControls by mapping utilization as current / cap instead of slider min/max coordinates.

View File

@@ -1030,7 +1030,8 @@ Use this panel when upgrading a project with pre-FN-6245/FN-6277 in-review rows
### Executor footer engine controls ### Executor footer engine controls
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. 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, clamped to the track when usage exceeds the configured cap. Slider changes save through the existing `/api/settings` path with the same debounced behavior used by Command Center controls; no separate backend route is required. <!-- 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. -->
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. 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. Slider changes save through the existing `/api/settings` path with the same debounced behavior used by Command Center controls; no separate backend route is required.
<!-- 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

@@ -54,9 +54,13 @@ function getErrorMessage(error: unknown, fallback: string) {
return error instanceof Error ? error.message : fallback; return error instanceof Error ? error.message : fallback;
} }
function getUseMarkerRatio(current: number, min: number, max: number) { /*
if (max <= min) return 0; FNXC:GlobalConcurrencyControls 2026-06-29-10:30:
return clamp((current - min) / (max - min), 0, 1); FN-7235 keeps the footer current-use marker consistent with FN-7160 Command Center behavior: it shows absolute utilization on a 0..cap scale. Do not subtract the range input floor of 1, because one running agent must render above zero even though the editable slider cannot be set to 0.
*/
function getUseMarkerRatio(current: number, max: number) {
if (max <= 0) return 0;
return clamp(current / max, 0, 1);
} }
function getUseMarkerStyle(ratio: number): CSSProperties { function getUseMarkerStyle(ratio: number): CSSProperties {
@@ -244,8 +248,8 @@ export const EngineControlMenu = forwardRef<EngineControlMenuHandle, EngineContr
const globalCountsLoaded = gc.status === "loaded"; const globalCountsLoaded = gc.status === "loaded";
const projectActive = gc.projectActiveCount(projectId); const projectActive = gc.projectActiveCount(projectId);
const maxConcurrentSliderMax = getConcurrencySliderMax("maxConcurrent", concurrencyValues.maxConcurrent); const maxConcurrentSliderMax = getConcurrencySliderMax("maxConcurrent", concurrencyValues.maxConcurrent);
const globalUseMarkerRatio = getUseMarkerRatio(gc.currentlyActive, gc.min, gc.sliderMax); const globalUseMarkerRatio = getUseMarkerRatio(gc.currentlyActive, gc.sliderMax);
const projectUseMarkerRatio = getUseMarkerRatio(projectActive, CONCURRENCY_SLIDER_LIMITS.maxConcurrent.min, maxConcurrentSliderMax); const projectUseMarkerRatio = getUseMarkerRatio(projectActive, maxConcurrentSliderMax);
return ( return (
<div className="engine-control-menu" ref={menuRef}> <div className="engine-control-menu" ref={menuRef}>

View File

@@ -54,6 +54,12 @@ function mockGlobalConcurrency(overrides: Partial<{
}); });
} }
// FNXC:EngineControls 2026-06-29-12:00: FN-7235 reproduces the footer mismatch by asserting running-count markers use the loaded cap (`current / cap`) rather than expanded slider-track coordinates; both global and project renderers must move 1 running agent above zero.
// FNXC:EngineControls 2026-06-29-13:25: Keep the footer marker guard aligned with the Command Center representative states: zero, one active, mid-track utilization, over-cap clamping, loading, and error.
function expectUseMarkerPct(testId: string, pct: string) {
expect(screen.getByTestId(testId).style.getPropertyValue("--use-pct")).toBe(pct);
}
describe("EngineControlMenu", () => { describe("EngineControlMenu", () => {
beforeEach(() => { beforeEach(() => {
vi.useRealTimers(); vi.useRealTimers();
@@ -218,7 +224,7 @@ describe("EngineControlMenu", () => {
expect(screen.getByLabelText(/max worktrees/i)).toHaveAttribute("max", "50"); expect(screen.getByLabelText(/max worktrees/i)).toHaveAttribute("max", "50");
}); });
it("renders running counts and current-use markers with clamped slider positions", async () => { it("renders running counts and current-use markers with clamped absolute utilization", async () => {
legacyMocks.fetchSettings.mockResolvedValue({ legacyMocks.fetchSettings.mockResolvedValue({
...defaultSettings, ...defaultSettings,
maxConcurrent: 50, maxConcurrent: 50,
@@ -237,20 +243,66 @@ describe("EngineControlMenu", () => {
expect(screen.getByTestId("engine-control-project-use-marker")).toHaveStyle({ "--use-pct": "100%" }); expect(screen.getByTestId("engine-control-project-use-marker")).toHaveStyle({ "--use-pct": "100%" });
}); });
it("positions current-use markers at zero and mid-track for representative running counts", async () => { it("positions current-use markers by absolute utilization instead of slider-coordinate math", async () => {
legacyMocks.fetchSettings.mockResolvedValue({
...defaultSettings,
maxConcurrent: 50,
});
mockGlobalConcurrency({ mockGlobalConcurrency({
globalMaxConcurrent: 33, globalMaxConcurrent: 50,
currentlyActive: 17, currentlyActive: 17,
projectsActive: { proj_123: 0 }, projectsActive: { proj_123: 17 },
}); });
await openMenu(); await openMenu();
expect(await screen.findByTestId("engine-control-global-use-marker")).toHaveStyle({ "--use-pct": "50%" }); await screen.findByTestId("engine-control-global-use-marker");
expect(screen.getByTestId("engine-control-project-use-marker")).toHaveStyle({ "--use-pct": "0%" }); expectUseMarkerPct("engine-control-global-use-marker", "34%");
expectUseMarkerPct("engine-control-project-use-marker", "34%");
expect(screen.getByTestId("engine-control-global-use-marker").style.getPropertyValue("--use-pct")).not.toBe(`${((17 - 1) / (50 - 1)) * 100}%`);
expect(screen.getByTestId("engine-control-project-use-marker").style.getPropertyValue("--use-pct")).not.toBe(`${((17 - 1) / (50 - 1)) * 100}%`);
expect(screen.queryAllByTestId(/engine-control-.*-use-marker/)).toHaveLength(2);
}); });
it("defaults the current-project running count to zero for empty projectsActive and missing projectId", async () => { it("positions mid-track footer markers using absolute utilization", async () => {
legacyMocks.fetchSettings.mockResolvedValue({
...defaultSettings,
maxConcurrent: 10,
});
mockGlobalConcurrency({
globalMaxConcurrent: 10,
currentlyActive: 6,
projectsActive: { proj_123: 6 },
});
await openMenu();
await screen.findByTestId("engine-control-global-use-marker");
expectUseMarkerPct("engine-control-global-use-marker", "18.75%");
expectUseMarkerPct("engine-control-project-use-marker", "12%");
});
it("keeps one active agent visibly above zero on both footer markers", async () => {
legacyMocks.fetchSettings.mockResolvedValue({
...defaultSettings,
maxConcurrent: 10,
});
mockGlobalConcurrency({
globalMaxConcurrent: 10,
currentlyActive: 1,
projectsActive: { proj_123: 1 },
});
await openMenu();
await screen.findByTestId("engine-control-global-use-marker");
expectUseMarkerPct("engine-control-global-use-marker", "3.125%");
expectUseMarkerPct("engine-control-project-use-marker", "2%");
expect(screen.getByTestId("engine-control-global-use-marker").style.getPropertyValue("--use-pct")).not.toBe("0%");
expect(screen.getByTestId("engine-control-project-use-marker").style.getPropertyValue("--use-pct")).not.toBe("0%");
});
it("positions zero running at the start of both footer markers", async () => {
mockGlobalConcurrency({ mockGlobalConcurrency({
globalMaxConcurrent: 6, globalMaxConcurrent: 6,
currentlyActive: 0, currentlyActive: 0,
@@ -265,7 +317,7 @@ describe("EngineControlMenu", () => {
expect(screen.getByTestId("engine-control-project-use-marker")).toHaveStyle({ "--use-pct": "0%" }); expect(screen.getByTestId("engine-control-project-use-marker")).toHaveStyle({ "--use-pct": "0%" });
}); });
it("does not render running counts or markers while global concurrency is loading", async () => { it("suppresses footer running counts and markers while utilization is loading", async () => {
let resolveGlobalConcurrency!: (value: { let resolveGlobalConcurrency!: (value: {
globalMaxConcurrent: number; globalMaxConcurrent: number;
currentlyActive: number; currentlyActive: number;
@@ -293,7 +345,7 @@ describe("EngineControlMenu", () => {
}); });
}); });
it("does not render running counts or markers when global concurrency fails to load", async () => { it("suppresses footer running counts and markers when utilization fails", async () => {
legacyMocks.fetchGlobalConcurrency.mockRejectedValue(new Error("global concurrency unavailable")); legacyMocks.fetchGlobalConcurrency.mockRejectedValue(new Error("global concurrency unavailable"));
await openMenu(); await openMenu();