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:
7
.changeset/fn-7340-footer-concurrency-dot-alignment.md
Normal file
7
.changeset/fn-7340-footer-concurrency-dot-alignment.md
Normal 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.
|
||||
@@ -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-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. -->
|
||||
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. -->
|
||||
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.
|
||||
|
||||
@@ -138,8 +138,14 @@
|
||||
}
|
||||
|
||||
.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);
|
||||
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. */
|
||||
@@ -164,6 +170,26 @@
|
||||
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) {
|
||||
.engine-control-menu__popover {
|
||||
/*
|
||||
|
||||
@@ -1,8 +1,20 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { render, screen, fireEvent, waitFor, act, cleanup } from "@testing-library/react";
|
||||
import { EngineControlMenu } from "../EngineControlMenu";
|
||||
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 = {
|
||||
maxConcurrent: 2,
|
||||
maxTriageConcurrent: 1,
|
||||
@@ -65,6 +77,19 @@ function expectUseMarkerPct(testId: string, pct: string) {
|
||||
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", () => {
|
||||
beforeEach(() => {
|
||||
vi.useRealTimers();
|
||||
@@ -89,6 +114,36 @@ describe("EngineControlMenu", () => {
|
||||
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 () => {
|
||||
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-global-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 () => {
|
||||
|
||||
Reference in New Issue
Block a user