FN-8007: align concurrency current-use markers
Align dashboard and footer concurrency markers with their native range thumbs. - Map running counts in min-relative slider coordinates and clamp them to the configured cap - Standardize native slider thumb dimensions and marker geometry across browsers - Add dashboard coverage and document the marker behavior Files changed: .changeset/fn-8007-concurrency-dot-alignment.md | 7 + docs/dashboard-guide.md | 8 +- .../dashboard/app/components/EngineControlMenu.css | 20 ++- .../dashboard/app/components/EngineControlMenu.tsx | 19 ++- .../__tests__/EngineControlMenu.test.tsx | 96 +++++------- .../command-center/CommandCenterControls.css | 22 ++- .../command-center/CommandCenterControls.tsx | 19 ++- .../__tests__/CommandCenterControls.test.tsx | 164 +++++++++++++++++++++ 8 files changed, 277 insertions(+), 78 deletions(-) Fusion-Task-Id: FN-8007 Fusion-Task-Lineage: 9ad8ee0b-09da-413e-96bc-530c897cb32e Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-8007-concurrency-dot-alignment.md
Normal file
7
.changeset/fn-8007-concurrency-dot-alignment.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Concurrency slider current-use dots now line up with the running-count value on the dashboard and footer.
|
||||
category: fix
|
||||
dev: Match the current-use marker's native range coordinate mapping and thumb-size edge inset in CommandCenterControls and EngineControlMenu.
|
||||
@@ -1148,7 +1148,7 @@ Features:
|
||||
<!-- FNXC:CommandCenter 2026-06-25-19:47: FN-7019 restored the user-facing picker contract: preset and custom date-range selections must change every historical analytics tab, while Mission Control stays live and intentionally ignores historical range filters. -->
|
||||
<!-- FNXC:CommandCenter 2026-06-19-23:54: FN-6755 moved team-specific operations out of Overview: org hierarchy and heartbeat pause/resume live in Team, while Overview keeps global AI engine, concurrency, and theme controls. -->
|
||||
<!-- FNXC:GlobalConcurrencyControls 2026-06-26-00:00: The Command Center Concurrency card mirrors the footer concurrency popover by showing read-only running-agent counts and current-use markers for the shared global cap and current-project max-concurrent slider. -->
|
||||
<!-- FNXC:GlobalConcurrencyControls 2026-06-27-00:00: Those running-agent counts include every top-level slot holder: in-progress executors, active triage planners (`triage` + `planning`, not paused), and active in-review reviewer/merger/fix agents. Current-use markers map absolute utilization on a 0..cap scale, not slider-value coordinates, so one active agent renders above 0%. -->
|
||||
<!-- FNXC:GlobalConcurrencyControls 2026-07-15-17:30: FN-8007 aligns each current-use marker with its native range thumb: it maps the cap-clamped running count min-relatively across the expanded slider range. One running agent at slider minimum remains visible at the track start, and over-cap use pins to the cap thumb rather than the track end. -->
|
||||
<!-- FNXC:CommandCenter 2026-06-26-00:00: The four Overview Concurrency sliders change live scheduler capacity, so each settled edit opens a confirmation popup before persisting; cancel, backdrop, or Escape leaves the previous persisted value in place. -->
|
||||
<!-- FNXC:CommandCenter 2026-06-27-10:03: Tokens detail charts must show every model bucket returned by analytics for accurate spend attribution; Overview remains a compact top-model summary because its copy explicitly frames those cards as top consumers/share. -->
|
||||
<!-- FNXC:CommandCenterActivity 2026-06-30-00:00: Activity active-agent counts include both durable-agent usage events and ephemeral task-worker execution runs from agentRuns, because task execution can be visible without a matching usage_events row. -->
|
||||
@@ -1338,11 +1338,11 @@ Use this panel when upgrading a project with pre-FN-6245/FN-6277 in-review rows
|
||||
|
||||
### Executor footer engine controls
|
||||
|
||||
<!-- 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-07-15-17:30: FN-8007 documents that footer concurrency current-use dots use the same native range-thumb mapping as Command Center controls, including the slider-min floor and cap-clamped over-cap position. -->
|
||||
<!-- 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: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:GlobalConcurrencyControls 2026-07-15-17:30: FN-8007 keeps footer and dashboard current-use marker geometry aligned with the native range thumb, including its desktop and mobile thumb-size edge inset. -->
|
||||
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 the same min-relative range coordinates and thumb-size edge inset as the native slider: it aligns to the cap-clamped running count, so one running agent at the slider minimum stays visible at the start and over-cap usage pins to the cap thumb instead of the expanded track 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.
|
||||
|
||||
@@ -130,8 +130,12 @@
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:GlobalConcurrencyControls 2026-07-15-17:30:
|
||||
FN-8007 makes the current-use dot's half-thumb edge inset match the native desktop range thumb. This preserves the min-relative thumb alignment used by the marker: one running agent stays visible at the start and over-cap use stays on the cap thumb.
|
||||
*/
|
||||
.engine-control-menu__range-wrap {
|
||||
--engine-control-range-thumb-size: var(--space-md);
|
||||
--engine-control-range-thumb-size: calc(var(--space-lg) + var(--space-xs) / 2);
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -151,6 +155,20 @@
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:GlobalConcurrencyControls 2026-07-15-18:10:
|
||||
FN-8007 defines both desktop pseudo-thumbs locally with the marker's shared size token. This prevents browser-default thumb dimensions from changing the marker travel geometry; WebKit and Gecko must use the same dimensions as the half-thumb inset.
|
||||
*/
|
||||
.engine-control-menu__range::-webkit-slider-thumb {
|
||||
width: var(--engine-control-range-thumb-size);
|
||||
height: var(--engine-control-range-thumb-size);
|
||||
}
|
||||
|
||||
.engine-control-menu__range::-moz-range-thumb {
|
||||
width: var(--engine-control-range-thumb-size);
|
||||
height: var(--engine-control-range-thumb-size);
|
||||
}
|
||||
|
||||
/* 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. */
|
||||
.engine-control-menu__use-marker {
|
||||
--use-pct: 0%;
|
||||
|
||||
@@ -78,12 +78,12 @@ function getConcurrencyChangeSummary(t: ReturnType<typeof useTranslation>["t"],
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:GlobalConcurrencyControls 2026-06-29-10:30:
|
||||
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.
|
||||
FNXC:GlobalConcurrencyControls 2026-07-15-17:30:
|
||||
FN-8007 supersedes FN-7160/FN-7235's 0-based utilization ratio: the current-use dot must share the native range thumb's min-relative coordinates so it lines up with the running-count value. With sliderMin 1, one running agent maps to the visible track start; over-cap use pins to the cap thumb rather than the expanded sliderMax endpoint.
|
||||
*/
|
||||
function getUseMarkerRatio(current: number, max: number) {
|
||||
if (max <= 0) return 0;
|
||||
return clamp(current / max, 0, 1);
|
||||
function getUseMarkerRatio(currentRunning: number, capValue: number, sliderMin: number, sliderMax: number) {
|
||||
if (sliderMax <= sliderMin) return 0;
|
||||
return clamp((Math.min(currentRunning, capValue) - sliderMin) / (sliderMax - sliderMin), 0, 1);
|
||||
}
|
||||
|
||||
function getUseMarkerStyle(ratio: number): CSSProperties {
|
||||
@@ -407,8 +407,13 @@ export const EngineControlMenu = forwardRef<EngineControlMenuHandle, EngineContr
|
||||
const globalSliderValue = pendingGlobalConcurrencyValue ?? gc.value;
|
||||
const globalSliderMax = Math.max(gc.sliderMax, globalSliderValue);
|
||||
const maxConcurrentSliderMax = getConcurrencySliderMax("maxConcurrent", concurrencyValues.maxConcurrent);
|
||||
const globalUseMarkerRatio = getUseMarkerRatio(gc.currentlyActive, globalSliderMax);
|
||||
const projectUseMarkerRatio = getUseMarkerRatio(projectActive, maxConcurrentSliderMax);
|
||||
const globalUseMarkerRatio = getUseMarkerRatio(gc.currentlyActive, globalSliderValue, gc.min, globalSliderMax);
|
||||
const projectUseMarkerRatio = getUseMarkerRatio(
|
||||
projectActive,
|
||||
concurrencyValues.maxConcurrent,
|
||||
CONCURRENCY_SLIDER_LIMITS.maxConcurrent.min,
|
||||
maxConcurrentSliderMax,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="engine-control-menu" ref={menuRef}>
|
||||
|
||||
@@ -124,8 +124,8 @@ describe("EngineControlMenu", () => {
|
||||
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("--engine-control-range-thumb-size: calc(var(--space-lg) + var(--space-xs) / 2);");
|
||||
expect(commandWrap).toContain("--cc-controls-range-thumb-size: calc(var(--space-lg) + var(--space-xs) / 2);");
|
||||
expect(footerWrap).toContain("position: relative;");
|
||||
expect(footerWrap).toContain("display: flex;");
|
||||
expect(footerWrap).toContain("align-items: center;");
|
||||
@@ -134,6 +134,12 @@ describe("EngineControlMenu", () => {
|
||||
expect(footerRange).toContain("accent-color: var(--accent);");
|
||||
expect(footerRange).toContain("touch-action: none;");
|
||||
expect(footerRange).not.toContain("touch-action: pan-y;");
|
||||
// FNXC:GlobalConcurrencyControls 2026-07-15-18:10: FN-8007 must locally size WebKit and Gecko desktop thumbs from the same token used by the marker inset, rather than relying on browser defaults.
|
||||
for (const selector of [".engine-control-menu__range::-webkit-slider-thumb", ".engine-control-menu__range::-moz-range-thumb"]) {
|
||||
const thumb = cssRule(engineControlMenuCss, selector);
|
||||
expect(thumb).toContain("width: var(--engine-control-range-thumb-size);");
|
||||
expect(thumb).toContain("height: var(--engine-control-range-thumb-size);");
|
||||
}
|
||||
expect(commandTouchSliderRule).toContain("inline-size: 100%;");
|
||||
expect(commandTouchSliderRule).toContain("min-block-size: var(--space-xl);");
|
||||
expect(commandTouchSliderRule).toContain("accent-color: var(--accent);");
|
||||
@@ -539,69 +545,43 @@ describe("EngineControlMenu", () => {
|
||||
expect(legacyMocks.updateGlobalConcurrency).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("renders running counts and current-use markers with clamped absolute utilization", async () => {
|
||||
legacyMocks.fetchSettings.mockResolvedValue({
|
||||
...defaultSettings,
|
||||
maxConcurrent: 50,
|
||||
});
|
||||
mockGlobalConcurrency({
|
||||
globalMaxConcurrent: 40,
|
||||
currentlyActive: 40,
|
||||
projectsActive: { proj_123: 90 },
|
||||
});
|
||||
|
||||
await openMenu();
|
||||
|
||||
expect(await screen.findByTestId("engine-control-global-running")).toHaveTextContent("40 running (all projects)");
|
||||
expect(screen.getByTestId("engine-control-project-running")).toHaveTextContent("90 running (this project)");
|
||||
expect(screen.getByTestId("engine-control-global-use-marker")).toHaveStyle({ "--use-pct": "100%" });
|
||||
expect(screen.getByTestId("engine-control-project-use-marker")).toHaveStyle({ "--use-pct": "100%" });
|
||||
});
|
||||
|
||||
it("positions current-use markers by absolute utilization instead of slider-coordinate math", async () => {
|
||||
legacyMocks.fetchSettings.mockResolvedValue({
|
||||
...defaultSettings,
|
||||
maxConcurrent: 50,
|
||||
});
|
||||
mockGlobalConcurrency({
|
||||
globalMaxConcurrent: 50,
|
||||
currentlyActive: 17,
|
||||
projectsActive: { proj_123: 17 },
|
||||
});
|
||||
|
||||
await openMenu();
|
||||
|
||||
await screen.findByTestId("engine-control-global-use-marker");
|
||||
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("positions mid-track footer markers using absolute utilization", async () => {
|
||||
legacyMocks.fetchSettings.mockResolvedValue({
|
||||
...defaultSettings,
|
||||
maxConcurrent: 10,
|
||||
});
|
||||
// FNXC:GlobalConcurrencyControls 2026-07-15-12:00: FN-8007 replaces FN-7160/FN-7235's utilization ratio with native range-thumb coordinates so markers share the running value's min-relative track position.
|
||||
it("aligns footer global and project markers with their native thumbs", async () => {
|
||||
legacyMocks.fetchSettings.mockResolvedValue({ ...defaultSettings, maxConcurrent: 12 });
|
||||
mockGlobalConcurrency({
|
||||
globalMaxConcurrent: 10,
|
||||
currentlyActive: 6,
|
||||
projectsActive: { proj_123: 6 },
|
||||
currentlyActive: 10,
|
||||
projectsActive: { proj_123: 10 },
|
||||
});
|
||||
|
||||
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%");
|
||||
expectUseMarkerPct("engine-control-global-use-marker", `${((10 - 1) / (32 - 1)) * 100}%`);
|
||||
expectUseMarkerPct("engine-control-project-use-marker", `${((10 - 1) / (50 - 1)) * 100}%`);
|
||||
expectFooterUseOffset("engine-control-global-use-marker", (10 - 1) / (32 - 1));
|
||||
expectFooterUseOffset("engine-control-project-use-marker", (10 - 1) / (50 - 1));
|
||||
});
|
||||
|
||||
it("keeps one active agent visibly above zero on both footer markers", async () => {
|
||||
legacyMocks.fetchSettings.mockResolvedValue({
|
||||
...defaultSettings,
|
||||
maxConcurrent: 10,
|
||||
it("pins footer over-cap markers at the cap thumb instead of the track end", async () => {
|
||||
legacyMocks.fetchSettings.mockResolvedValue({ ...defaultSettings, maxConcurrent: 12 });
|
||||
mockGlobalConcurrency({
|
||||
globalMaxConcurrent: 10,
|
||||
currentlyActive: 40,
|
||||
projectsActive: { proj_123: 40 },
|
||||
});
|
||||
|
||||
await openMenu();
|
||||
|
||||
await screen.findByTestId("engine-control-global-use-marker");
|
||||
expectUseMarkerPct("engine-control-global-use-marker", `${((10 - 1) / (32 - 1)) * 100}%`);
|
||||
expectUseMarkerPct("engine-control-project-use-marker", `${((12 - 1) / (50 - 1)) * 100}%`);
|
||||
expect(screen.getByTestId("engine-control-global-use-marker").style.getPropertyValue("--use-pct")).not.toBe("100%");
|
||||
expect(screen.getByTestId("engine-control-project-use-marker").style.getPropertyValue("--use-pct")).not.toBe("100%");
|
||||
});
|
||||
|
||||
it("maps one running agent to the visible footer slider start", async () => {
|
||||
legacyMocks.fetchSettings.mockResolvedValue({ ...defaultSettings, maxConcurrent: 12 });
|
||||
mockGlobalConcurrency({
|
||||
globalMaxConcurrent: 10,
|
||||
currentlyActive: 1,
|
||||
@@ -611,10 +591,10 @@ describe("EngineControlMenu", () => {
|
||||
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%");
|
||||
expectUseMarkerPct("engine-control-global-use-marker", "0%");
|
||||
expectUseMarkerPct("engine-control-project-use-marker", "0%");
|
||||
expectFooterUseOffset("engine-control-global-use-marker", 0);
|
||||
expectFooterUseOffset("engine-control-project-use-marker", 0);
|
||||
});
|
||||
|
||||
it("positions zero running at the start of both footer markers", async () => {
|
||||
@@ -651,15 +631,15 @@ describe("EngineControlMenu", () => {
|
||||
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%");
|
||||
expectUseMarkerPct("engine-control-global-use-marker", `${((16 - 1) / (48 - 1)) * 100}%`);
|
||||
expectUseMarkerPct("engine-control-project-use-marker", `${((30 - 1) / (60 - 1)) * 100}%`);
|
||||
|
||||
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);
|
||||
expectUseMarkerPct("engine-control-global-use-marker", `${((16 - 1) / (48 - 1)) * 100}%`);
|
||||
expectUseMarkerPct("engine-control-project-use-marker", `${((30 - 1) / (50 - 1)) * 100}%`);
|
||||
expectFooterUseOffset("engine-control-global-use-marker", (16 - 1) / (48 - 1));
|
||||
expectFooterUseOffset("engine-control-project-use-marker", (30 - 1) / (50 - 1));
|
||||
});
|
||||
|
||||
it("suppresses footer running counts and markers while utilization is loading", async () => {
|
||||
|
||||
@@ -114,8 +114,12 @@
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:GlobalConcurrencyControls 2026-07-15-17:30:
|
||||
FN-8007 makes the current-use dot's half-thumb edge inset match the native desktop range thumb. This preserves the min-relative thumb alignment used by the marker: one running agent stays visible at the start and over-cap use stays on the cap thumb.
|
||||
*/
|
||||
.cc-controls-range-wrap {
|
||||
--cc-controls-range-thumb-size: var(--space-md);
|
||||
--cc-controls-range-thumb-size: calc(var(--space-lg) + var(--space-xs) / 2);
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -147,6 +151,22 @@ FN-7973 requires touch-action:none because the mobile pan-y ancestor lock otherw
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:GlobalConcurrencyControls 2026-07-15-18:10:
|
||||
FN-8007 defines both desktop pseudo-thumbs locally with the marker's shared size token. This prevents browser-default thumb dimensions from changing the marker travel geometry; WebKit and Gecko must use the same dimensions as the half-thumb inset.
|
||||
*/
|
||||
.cc-controls-slider input[type="range"]::-webkit-slider-thumb,
|
||||
.cc-controls-touch-slider::-webkit-slider-thumb {
|
||||
width: var(--cc-controls-range-thumb-size);
|
||||
height: var(--cc-controls-range-thumb-size);
|
||||
}
|
||||
|
||||
.cc-controls-slider input[type="range"]::-moz-range-thumb,
|
||||
.cc-controls-touch-slider::-moz-range-thumb {
|
||||
width: var(--cc-controls-range-thumb-size);
|
||||
height: var(--cc-controls-range-thumb-size);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.cc-controls-grid {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
|
||||
@@ -70,12 +70,12 @@ function getConcurrencySliderMax(key: keyof ConcurrencyValues, value: number) {
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:GlobalConcurrencyControls 2026-06-27-00:00:
|
||||
FN-7160 requires the current-use marker to show absolute utilization on a 0..cap scale. Do not reuse slider value coordinates here: the slider floor is 1, and subtracting it makes one active agent render as 0% utilization.
|
||||
FNXC:GlobalConcurrencyControls 2026-07-15-17:30:
|
||||
FN-8007 supersedes FN-7160's 0-based utilization ratio: the current-use dot must share the native range thumb's min-relative coordinates so it lines up with the running-count value. With sliderMin 1, one running agent maps to the visible track start; over-cap use pins to the cap thumb rather than the expanded sliderMax endpoint.
|
||||
*/
|
||||
function getUseMarkerRatio(current: number, max: number) {
|
||||
if (max <= 0) return 0;
|
||||
return clamp(current / max, 0, 1);
|
||||
function getUseMarkerRatio(currentRunning: number, capValue: number, sliderMin: number, sliderMax: number) {
|
||||
if (sliderMax <= sliderMin) return 0;
|
||||
return clamp((Math.min(currentRunning, capValue) - sliderMin) / (sliderMax - sliderMin), 0, 1);
|
||||
}
|
||||
|
||||
function getUseMarkerStyle(ratio: number): CSSProperties {
|
||||
@@ -293,8 +293,13 @@ export function CommandCenterControls({ projectId, colorTheme, themeMode, shadcn
|
||||
const globalSliderValue = pendingGlobalConcurrencyValue ?? gc.value;
|
||||
const globalSliderMax = Math.max(gc.sliderMax, globalSliderValue);
|
||||
const maxConcurrentSliderMax = getConcurrencySliderMax("maxConcurrent", concurrencyValues.maxConcurrent);
|
||||
const globalUseMarkerRatio = getUseMarkerRatio(gc.currentlyActive, globalSliderMax);
|
||||
const projectUseMarkerRatio = getUseMarkerRatio(projectActive, maxConcurrentSliderMax);
|
||||
const globalUseMarkerRatio = getUseMarkerRatio(gc.currentlyActive, globalSliderValue, gc.min, globalSliderMax);
|
||||
const projectUseMarkerRatio = getUseMarkerRatio(
|
||||
projectActive,
|
||||
concurrencyValues.maxConcurrent,
|
||||
CONCURRENCY_SLIDER_LIMITS.maxConcurrent.min,
|
||||
maxConcurrentSliderMax,
|
||||
);
|
||||
// FNXC:GlobalConcurrencyControls 2026-06-25-22:45: Mirror the per-project slider save-state labels for the shared global cap.
|
||||
// FNXC:GlobalConcurrencyControls 2026-06-26-06:05: Explicit load-error branch — a failed initial load leaves saveState "idle", so the label otherwise fell through to "Ready" while the slider was disabled and an error alert shown.
|
||||
const globalSaveLabel = gc.status === "loading" || gc.status === "idle"
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
||||
import { CommandCenterControls } from "../CommandCenterControls";
|
||||
import { ConfirmDialogProvider } from "../../../hooks/useConfirm";
|
||||
|
||||
const commandCenterControlsCss = readFileSync(
|
||||
join(process.cwd(), "app/components/command-center/CommandCenterControls.css"),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const legacyMocks = vi.hoisted(() => ({
|
||||
fetchConfig: vi.fn(),
|
||||
fetchSettings: vi.fn(),
|
||||
updateSettings: vi.fn(),
|
||||
fetchGlobalConcurrency: vi.fn(),
|
||||
updateGlobalConcurrency: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../../../api/legacy", () => legacyMocks);
|
||||
vi.mock("../../../hooks/useAppSettings", () => ({
|
||||
useAppSettings: () => ({
|
||||
globalPaused: false,
|
||||
toggleGlobalPause: vi.fn(),
|
||||
refresh: vi.fn().mockResolvedValue(undefined),
|
||||
}),
|
||||
}));
|
||||
|
||||
const defaultSettings = {
|
||||
maxConcurrent: 12,
|
||||
maxTriageConcurrent: 1,
|
||||
maxWorktrees: 4,
|
||||
};
|
||||
|
||||
function renderControls(projectId = "proj_123") {
|
||||
render(
|
||||
<ConfirmDialogProvider>
|
||||
<CommandCenterControls
|
||||
projectId={projectId}
|
||||
colorTheme="default"
|
||||
themeMode="dark"
|
||||
onColorThemeChange={vi.fn()}
|
||||
onThemeModeChange={vi.fn()}
|
||||
/>
|
||||
</ConfirmDialogProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
function mockGlobalConcurrency(overrides: Partial<{
|
||||
globalMaxConcurrent: number;
|
||||
currentlyActive: number;
|
||||
projectsActive: Record<string, number>;
|
||||
}> = {}) {
|
||||
legacyMocks.fetchGlobalConcurrency.mockResolvedValue({
|
||||
globalMaxConcurrent: 10,
|
||||
currentlyActive: 10,
|
||||
queuedCount: 0,
|
||||
projectsActive: { proj_123: 10 },
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
function expectUseMarkerPct(testId: string, pct: string) {
|
||||
expect(screen.getByTestId(testId).style.getPropertyValue("--use-pct")).toBe(pct);
|
||||
}
|
||||
|
||||
function expectCommandCenterUseOffset(testId: string, ratio: number) {
|
||||
expect(screen.getByTestId(testId).style.getPropertyValue("--use-offset")).toBe(
|
||||
`calc((var(--cc-controls-range-thumb-size) / 2) + ((100% - var(--cc-controls-range-thumb-size)) * ${ratio}))`,
|
||||
);
|
||||
}
|
||||
|
||||
describe("CommandCenterControls concurrency markers", () => {
|
||||
beforeEach(() => {
|
||||
legacyMocks.fetchConfig.mockResolvedValue({ maxConcurrent: 12, rootDir: "/workspace/project" });
|
||||
legacyMocks.fetchSettings.mockResolvedValue({ ...defaultSettings });
|
||||
legacyMocks.updateSettings.mockResolvedValue({ ...defaultSettings });
|
||||
legacyMocks.updateGlobalConcurrency.mockResolvedValue({});
|
||||
mockGlobalConcurrency();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
document.body.innerHTML = "";
|
||||
});
|
||||
|
||||
// FNXC:GlobalConcurrencyControls 2026-07-15-12:00: FN-8007 requires dashboard markers to use the exact native-thumb coordinate system when the expanded range max exceeds the persisted cap.
|
||||
it("aligns dashboard global and project markers with their native thumbs", async () => {
|
||||
renderControls();
|
||||
|
||||
await screen.findByTestId("cc-global-use-marker");
|
||||
expectUseMarkerPct("cc-global-use-marker", `${((10 - 1) / (32 - 1)) * 100}%`);
|
||||
expectUseMarkerPct("cc-project-use-marker", `${((10 - 1) / (50 - 1)) * 100}%`);
|
||||
expectCommandCenterUseOffset("cc-global-use-marker", (10 - 1) / (32 - 1));
|
||||
expectCommandCenterUseOffset("cc-project-use-marker", (10 - 1) / (50 - 1));
|
||||
});
|
||||
|
||||
it("pins dashboard over-cap markers at the cap thumb instead of the track end", async () => {
|
||||
mockGlobalConcurrency({ currentlyActive: 40, projectsActive: { proj_123: 40 } });
|
||||
renderControls();
|
||||
|
||||
await screen.findByTestId("cc-global-use-marker");
|
||||
expectUseMarkerPct("cc-global-use-marker", `${((10 - 1) / (32 - 1)) * 100}%`);
|
||||
expectUseMarkerPct("cc-project-use-marker", `${((12 - 1) / (50 - 1)) * 100}%`);
|
||||
expect(screen.getByTestId("cc-global-use-marker").style.getPropertyValue("--use-pct")).not.toBe("100%");
|
||||
expect(screen.getByTestId("cc-project-use-marker").style.getPropertyValue("--use-pct")).not.toBe("100%");
|
||||
});
|
||||
|
||||
it("maps one running agent to the visible dashboard slider start", async () => {
|
||||
mockGlobalConcurrency({ currentlyActive: 1, projectsActive: { proj_123: 1 } });
|
||||
renderControls();
|
||||
|
||||
await screen.findByTestId("cc-global-use-marker");
|
||||
expectUseMarkerPct("cc-global-use-marker", "0%");
|
||||
expectUseMarkerPct("cc-project-use-marker", "0%");
|
||||
expectCommandCenterUseOffset("cc-global-use-marker", 0);
|
||||
expectCommandCenterUseOffset("cc-project-use-marker", 0);
|
||||
});
|
||||
|
||||
it("suppresses dashboard marker shells while global concurrency is loading or unavailable", async () => {
|
||||
let resolveGlobalConcurrency!: (value: {
|
||||
globalMaxConcurrent: number;
|
||||
currentlyActive: number;
|
||||
queuedCount: number;
|
||||
projectsActive: Record<string, number>;
|
||||
}) => void;
|
||||
legacyMocks.fetchGlobalConcurrency.mockReturnValue(new Promise((resolve) => {
|
||||
resolveGlobalConcurrency = resolve;
|
||||
}));
|
||||
renderControls();
|
||||
|
||||
expect(screen.queryByTestId("cc-global-use-marker")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("cc-project-use-marker")).not.toBeInTheDocument();
|
||||
|
||||
resolveGlobalConcurrency({ globalMaxConcurrent: 10, currentlyActive: 0, queuedCount: 0, projectsActive: {} });
|
||||
await screen.findByTestId("cc-global-use-marker");
|
||||
cleanup();
|
||||
|
||||
legacyMocks.fetchGlobalConcurrency.mockRejectedValue(new Error("global concurrency unavailable"));
|
||||
renderControls();
|
||||
|
||||
await waitFor(() => expect(screen.getByRole("alert")).toBeInTheDocument());
|
||||
expect(screen.queryByTestId("cc-global-use-marker")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("cc-project-use-marker")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("matches the desktop and mobile native thumb-size CSS contract", () => {
|
||||
expect(commandCenterControlsCss).toContain(
|
||||
"--cc-controls-range-thumb-size: calc(var(--space-lg) + var(--space-xs) / 2);",
|
||||
);
|
||||
// FNXC:GlobalConcurrencyControls 2026-07-15-18:10: FN-8007 keeps desktop browser thumb travel deterministic by sizing both pseudo-thumb implementations from the marker inset token.
|
||||
for (const selector of [
|
||||
".cc-controls-slider input[type=\"range\"]::-webkit-slider-thumb,\n.cc-controls-touch-slider::-webkit-slider-thumb",
|
||||
".cc-controls-slider input[type=\"range\"]::-moz-range-thumb,\n.cc-controls-touch-slider::-moz-range-thumb",
|
||||
]) {
|
||||
expect(commandCenterControlsCss).toContain(selector);
|
||||
}
|
||||
expect(commandCenterControlsCss).toContain("width: var(--cc-controls-range-thumb-size);");
|
||||
expect(commandCenterControlsCss).toContain("height: var(--cc-controls-range-thumb-size);");
|
||||
expect(commandCenterControlsCss).toContain("@media (max-width: 768px)");
|
||||
expect(commandCenterControlsCss).toContain("--cc-controls-range-thumb-size: var(--space-xl);");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user