diff --git a/.changeset/fn-8007-concurrency-dot-alignment.md b/.changeset/fn-8007-concurrency-dot-alignment.md new file mode 100644 index 0000000000..29e8368db5 --- /dev/null +++ b/.changeset/fn-8007-concurrency-dot-alignment.md @@ -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. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index 152b9f716c..bc432f1351 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -1148,7 +1148,7 @@ Features: - + @@ -1338,11 +1338,11 @@ Use this panel when upgrading a project with pre-FN-6245/FN-6277 in-review rows ### 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. 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. + +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. 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. diff --git a/packages/dashboard/app/components/EngineControlMenu.css b/packages/dashboard/app/components/EngineControlMenu.css index 780eb7cae5..7f5b899bcb 100644 --- a/packages/dashboard/app/components/EngineControlMenu.css +++ b/packages/dashboard/app/components/EngineControlMenu.css @@ -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%; diff --git a/packages/dashboard/app/components/EngineControlMenu.tsx b/packages/dashboard/app/components/EngineControlMenu.tsx index e9f39dbfbc..ae09999d2f 100644 --- a/packages/dashboard/app/components/EngineControlMenu.tsx +++ b/packages/dashboard/app/components/EngineControlMenu.tsx @@ -78,12 +78,12 @@ function getConcurrencyChangeSummary(t: ReturnType["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 diff --git a/packages/dashboard/app/components/__tests__/EngineControlMenu.test.tsx b/packages/dashboard/app/components/__tests__/EngineControlMenu.test.tsx index 6c37c23b8d..ce54374989 100644 --- a/packages/dashboard/app/components/__tests__/EngineControlMenu.test.tsx +++ b/packages/dashboard/app/components/__tests__/EngineControlMenu.test.tsx @@ -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 () => { diff --git a/packages/dashboard/app/components/command-center/CommandCenterControls.css b/packages/dashboard/app/components/command-center/CommandCenterControls.css index 6794ae5025..d8dddf5c85 100644 --- a/packages/dashboard/app/components/command-center/CommandCenterControls.css +++ b/packages/dashboard/app/components/command-center/CommandCenterControls.css @@ -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); diff --git a/packages/dashboard/app/components/command-center/CommandCenterControls.tsx b/packages/dashboard/app/components/command-center/CommandCenterControls.tsx index e4b072b149..6691da99f6 100644 --- a/packages/dashboard/app/components/command-center/CommandCenterControls.tsx +++ b/packages/dashboard/app/components/command-center/CommandCenterControls.tsx @@ -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" diff --git a/packages/dashboard/app/components/command-center/__tests__/CommandCenterControls.test.tsx b/packages/dashboard/app/components/command-center/__tests__/CommandCenterControls.test.tsx new file mode 100644 index 0000000000..ca9ceff603 --- /dev/null +++ b/packages/dashboard/app/components/command-center/__tests__/CommandCenterControls.test.tsx @@ -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( + + + , + ); +} + +function mockGlobalConcurrency(overrides: Partial<{ + globalMaxConcurrent: number; + currentlyActive: number; + projectsActive: Record; +}> = {}) { + 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; + }) => 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);"); + }); +});