feat: global concurrency slider (footer + dashboard) and scoped settings (#1786)

## What

Adds a **Global Max Concurrent** slider to two operator surfaces and
groups the Scheduling settings by scope.

- **Footer engine menu** (`EngineControlMenu`) and **Command Center →
Concurrency card** (`CommandCenterControls`) each get a global-cap
slider.
- **Settings → Scheduling** (`SchedulingSection`) now groups fields
under labeled **"Global — applies to all projects"** and **"This
project"** subheadings with scope badges (clearer on mobile, and
surfaces the existing `globalMaxConcurrent` field that was easy to
miss).

## How

Both sliders are backed by a single shared `useGlobalConcurrency` hook
(module-level store + subscribers), so they read/write **one source of
truth** and **revalidate after every `PUT /api/global-concurrency`**.
This avoids the last-writer-wins / stale-clobber races that two
independent per-component caches would cause (which would ironically
re-create a "global cap resets" symptom). The hook also:

- treats a fetch error as **non-interactive** (slider disabled, not
stuck showing `1` and persistable),
- surfaces a **save-state** indicator (Saving…/Saved/Save failed)
mirroring the project sliders,
- **flushes a pending edit on menu close / unmount** so a quick
drag-then-dismiss isn't silently dropped.

## Review

These race/feedback behaviors were hardened in response to a multi-agent
`/code-review` (correctness, adversarial, reliability, frontend-races
all flagged the per-component-cache approach). Independent of the
companion fix PR (global-settings reset).

## Verification

- `pnpm --filter @fusion/dashboard run typecheck` — clean
- eslint on changed files — clean
- Not yet browser-verified (next step).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- stage-review-badge-begin -->

---

<a href="https://stagereview.app/Runfusion/Fusion/pull/1786">
  <picture>
<source media="(prefers-color-scheme: dark)"
srcset="https://stagereview.app/assets/gh-open-in-stage-dark.svg">
<img src="https://stagereview.app/assets/gh-open-in-stage-light.svg"
alt="Open in Stage">
  </picture>
</a>

<!-- stage-review-badge-end -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
  * Added a global concurrency slider that applies across all projects.
* Global and per-project scheduling controls are now clearly separated
with scope labels and badges.
* The concurrency setting now stays in sync across different dashboard
views and saves automatically after brief pauses.

* **Bug Fixes**
  * Improved feedback for loading, saving, and error states.
* Prevented confusion between global settings and project-specific
settings, especially on smaller screens.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
gsxdsm
2026-06-25 23:27:23 -07:00
committed by GitHub
8 changed files with 383 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": minor
---
summary: Adjust the global concurrency cap from the footer and dashboard; settings grouped by global vs project scope.
category: feature
dev: Added a Global Max Concurrent slider (wired to fetch/updateGlobalConcurrency) to EngineControlMenu (footer) and the dashboard CommandCenterControls Concurrency card, with debounced saves matching the existing project sliders. SchedulingSection now groups fields under labeled "Global — all projects" and "This project" subheadings with scope badges so the global cap is not mistaken for a per-project setting (clearer on mobile).

View File

@@ -71,6 +71,14 @@
font-weight: 500;
}
/* FNXC:GlobalConcurrencyControls 2026-06-25-22:45: "All projects" scope caption sits between the global-cap title and its save-state indicator; muted so the save-state remains the emphasized signal. */
.engine-control-menu__scope-caption {
margin-inline-start: auto;
color: var(--text-muted);
font-size: var(--font-size-xs);
font-weight: 500;
}
.engine-control-menu__save-state--saving {
color: var(--color-warning);
}
@@ -84,6 +92,12 @@
color: var(--color-error);
}
/* FNXC:GlobalConcurrencyControls 2026-06-25-14:10: The global cap is a cross-project setting; separate it visually from the per-project sliders below with a divider and an "All projects" caption. */
.engine-control-menu__section--global {
padding-bottom: var(--space-sm);
border-bottom: 1px solid var(--border);
}
.engine-control-menu__slider {
display: flex;
flex-direction: column;

View File

@@ -5,6 +5,8 @@ import { DEFAULT_PROJECT_SETTINGS } from "@fusion/core";
import { Pause, Play, SlidersHorizontal, Square } from "lucide-react";
import { fetchConfig, fetchSettings, updateSettings } from "../api/legacy";
import { useAppSettings } from "../hooks/useAppSettings";
// FNXC:GlobalConcurrencyControls 2026-06-25-22:45: Footer menu adopts the shared global-concurrency hook so it and the Command Center card read/write ONE source of truth (no more duplicated fetch/debounce/clobber logic).
import { useGlobalConcurrency } from "../hooks/useGlobalConcurrency";
export interface EngineControlMenuHandle {
open: () => void;
@@ -70,6 +72,8 @@ export const EngineControlMenu = forwardRef<EngineControlMenuHandle, EngineContr
const [concurrencyState, setConcurrencyState] = useState<AsyncState<ConcurrencyValues>>({ status: "idle", data: null, error: null });
const [concurrencyDirty, setConcurrencyDirty] = useState(false);
const [concurrencySaveState, setConcurrencySaveState] = useState<"idle" | "saving" | "saved" | "error">("idle");
// FNXC:GlobalConcurrencyControls 2026-06-25-22:45: Fetch is gated on the menu being open; the hook flushes any pending debounced write when `open` flips false.
const gc = useGlobalConcurrency({ activeWhen: open });
const closeMenu = useCallback(() => setOpen(false), []);
const openMenu = useCallback(() => setOpen(true), []);
@@ -167,6 +171,19 @@ export const EngineControlMenu = forwardRef<EngineControlMenuHandle, EngineContr
};
const concurrencyValues = concurrencyState.data ?? DEFAULT_CONCURRENCY_VALUES;
// FNXC:GlobalConcurrencyControls 2026-06-25-22:45: Mirror the per-project slider save-state labels for the shared global cap (Loading…/Load failed/Saving…/Saved/Save failed/Ready).
// FNXC:GlobalConcurrencyControls 2026-06-26-06:05: A failed initial load leaves saveState "idle", so without an explicit error branch the label fell through to "Ready" while the slider was disabled and an error alert was shown. Surface the load error instead.
const globalSaveLabel = gc.status === "loading" || gc.status === "idle"
? t("commandCenter.controls.status.loading", "Loading…")
: gc.status === "error"
? t("commandCenter.controls.status.loadError", "Load failed")
: gc.saveState === "saving"
? t("commandCenter.controls.status.saving", "Saving…")
: gc.saveState === "saved"
? t("commandCenter.controls.status.saved", "Saved")
: gc.saveState === "error"
? t("commandCenter.controls.status.saveError", "Save failed")
: t("commandCenter.controls.status.ready", "Ready");
const saveLabel = concurrencyState.status === "loading"
? t("commandCenter.controls.status.loading", "Loading…")
: concurrencySaveState === "saving"
@@ -219,6 +236,37 @@ export const EngineControlMenu = forwardRef<EngineControlMenuHandle, EngineContr
</button>
</div>
{/*
FNXC:GlobalConcurrencyControls 2026-06-25-14:10:
Operators need to adjust the global cross-project concurrency cap from the footer engine menu and the dashboard Concurrency card, not just the Settings modal; global cap is distinct from per-project maxConcurrent and persists via the central /api/global-concurrency endpoint.
*/}
<div className="engine-control-menu__section engine-control-menu__section--sliders engine-control-menu__section--global">
<div className="engine-control-menu__section-header">
<span>{t("settings.scheduling.globalMaxConcurrent", "Global Max Concurrent")}</span>
<span className="engine-control-menu__scope-caption">{t("commandCenter.controls.scope.allProjects", "All projects")}</span>
<span className={`engine-control-menu__save-state engine-control-menu__save-state--${gc.saveState}`} aria-live="polite">
{globalSaveLabel}
</span>
</div>
<label className="engine-control-menu__slider" htmlFor="engine-control-global-max-concurrent">
<span className="engine-control-menu__slider-label">
{t("settings.scheduling.maximumConcurrentAgentsAcrossAllProjects", "Maximum concurrent agents across all projects")}
<strong>{gc.value}</strong>
</span>
<input
id="engine-control-global-max-concurrent"
className="engine-control-menu__range input"
type="range"
min={gc.min}
max={gc.sliderMax}
value={gc.value}
disabled={!gc.interactive}
onChange={(event) => gc.setValue(event.target.value)}
/>
</label>
{gc.status === "error" ? <p className="engine-control-menu__error" role="alert">{t("commandCenter.controls.concurrency.error", "Unable to load concurrency settings")}</p> : null}
</div>
<div className="engine-control-menu__section engine-control-menu__section--sliders">
<div className="engine-control-menu__section-header">
<span>{t("commandCenter.controls.concurrency.title", "Concurrency")}</span>

View File

@@ -1778,6 +1778,55 @@ Settings section headings should preserve hierarchy through spacing and type onl
color: var(--text-muted);
}
/*
FNXC:SettingsScopeGrouping 2026-06-25-10:42:
Mobile settings must make clear which controls are global (all projects) vs project-scoped; group scheduling fields under labeled Global/This-project subheadings with a scope badge so operators don't mistake the global concurrency cap for a per-project setting.
The header row wraps so the badge drops below the heading on narrow widths instead of overflowing.
*/
.settings-scope-group {
margin-top: var(--space-md);
}
.settings-scope-group-header {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--space-sm);
}
.settings-scope-group-header .settings-section-heading {
padding-bottom: 0;
margin-bottom: 0;
}
.settings-scope-badge {
display: inline-flex;
align-items: center;
padding: 2px 8px;
border-radius: 10px;
font-size: 11px;
font-weight: 500;
white-space: nowrap;
}
.settings-scope-badge--global {
background-color: color-mix(in srgb, var(--accent, #4a90e2) 18%, transparent);
color: var(--accent, #4a90e2);
}
.settings-scope-badge--project {
background-color: color-mix(in srgb, var(--text-muted) 15%, transparent);
color: var(--text-muted);
}
.settings-scope-caption {
display: block;
margin: var(--space-xs) 0 var(--space-md);
color: var(--text-muted);
font-size: 12px;
line-height: 1.4;
}
.settings-description {
font-size: 13px;
color: var(--text-dim);

View File

@@ -90,6 +90,18 @@
font-size: 0.8125rem;
}
/* FNXC:GlobalConcurrencyControls 2026-06-25-14:10: The global cap is a cross-project setting; span it full-width at the top of the Concurrency card and separate it from the per-project sliders with a divider plus an "Across all projects" caption. */
.cc-controls-slider--global {
grid-column: 1 / -1;
padding-bottom: var(--space-md);
border-bottom: 1px solid var(--border);
}
.cc-controls-slider-caption {
color: var(--text-muted);
font-size: 0.75rem;
}
.cc-controls-slider-label {
display: flex;
align-items: center;

View File

@@ -4,6 +4,8 @@ import { Power } from "lucide-react";
import { DEFAULT_PROJECT_SETTINGS, type ColorTheme, type ThemeMode } from "@fusion/core";
import { fetchConfig, fetchSettings, updateSettings } from "../../api/legacy";
import { useAppSettings } from "../../hooks/useAppSettings";
// FNXC:GlobalConcurrencyControls 2026-06-25-22:45: Concurrency card adopts the shared global-concurrency hook so it and the footer EngineControlMenu read/write ONE source of truth (no more duplicated fetch/debounce/clobber logic).
import { useGlobalConcurrency } from "../../hooks/useGlobalConcurrency";
import { ThemeDropdown } from "../ThemeDropdown";
import type { TaskView } from "../../hooks/useViewState";
import "./CommandCenterControls.css";
@@ -79,6 +81,8 @@ export function CommandCenterControls({ projectId, colorTheme, themeMode, shadcn
const [concurrencyState, setConcurrencyState] = useState<AsyncState<ConcurrencyValues>>({ status: "loading", data: null, error: null });
const [concurrencyDirty, setConcurrencyDirty] = useState(false);
const [concurrencySaveState, setConcurrencySaveState] = useState<"idle" | "saving" | "saved" | "error">("idle");
// FNXC:GlobalConcurrencyControls 2026-06-25-22:45: No activeWhen — the card is mounted only while visible, so it fetches on mount and flushes pending writes on unmount via the shared hook.
const gc = useGlobalConcurrency();
useEffect(() => {
let cancelled = false;
@@ -145,6 +149,19 @@ export function CommandCenterControls({ projectId, colorTheme, themeMode, shadcn
const effectiveGlobalPaused = globalPaused;
const concurrencyValues = concurrencyState.data ?? DEFAULT_CONCURRENCY_VALUES;
// 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"
? t("commandCenter.controls.status.loading", "Loading…")
: gc.status === "error"
? t("commandCenter.controls.status.loadError", "Load failed")
: gc.saveState === "saving"
? t("commandCenter.controls.status.saving", "Saving…")
: gc.saveState === "saved"
? t("commandCenter.controls.status.saved", "Saved")
: gc.saveState === "error"
? t("commandCenter.controls.status.saveError", "Save failed")
: t("commandCenter.controls.status.ready", "Ready");
/*
FNXC:CommandCenter 2026-06-20-00:20:
@@ -239,6 +256,32 @@ export function CommandCenterControls({ projectId, colorTheme, themeMode, shadcn
</span>
</div>
<div className="cc-controls-sliders">
{/*
FNXC:GlobalConcurrencyControls 2026-06-25-14:10:
Operators need to adjust the global cross-project concurrency cap from the footer engine menu and the dashboard Concurrency card, not just the Settings modal; global cap is distinct from per-project maxConcurrent and persists via the central /api/global-concurrency endpoint.
*/}
<label className="cc-controls-slider cc-controls-slider--global" htmlFor="cc-global-max-concurrent">
<span className="cc-controls-slider-label">
{t("settings.scheduling.globalMaxConcurrent", "Global Max Concurrent")}
<strong>{gc.value}</strong>
</span>
<small className="cc-controls-slider-caption">{t("settings.scheduling.maximumConcurrentAgentsAcrossAllProjects", "Maximum concurrent agents across all projects")}</small>
<input
id="cc-global-max-concurrent"
className="cc-controls-touch-slider"
type="range"
min={gc.min}
max={gc.sliderMax}
value={gc.value}
disabled={!gc.interactive}
onChange={(event) => gc.setValue(event.target.value)}
/>
{/* FNXC:GlobalConcurrencyControls 2026-06-25-22:45: Surface the shared cap's save-state (and a fetch-error message that the card previously lacked) so operators see Saving…/Saved/Save failed and know when the slider is non-interactive due to a load failure. */}
<span className={`cc-controls-save-state cc-controls-save-state--${gc.saveState}`} aria-live="polite">
{globalSaveLabel}
</span>
{gc.status === "error" ? <small className="cc-controls-error" role="alert">{t("commandCenter.controls.concurrency.error", "Unable to load concurrency settings")}</small> : null}
</label>
<label className="cc-controls-slider" htmlFor="cc-max-concurrent">
<span className="cc-controls-slider-label">
{t("commandCenter.controls.concurrency.maxConcurrent", "Max concurrent tasks")}

View File

@@ -17,6 +17,25 @@ export interface SchedulingSectionProps {
onAddOverlapIgnorePath: () => void;
onOpenWorkflowSettings?: () => void;
}
/*
FNXC:SettingsScopeGrouping 2026-06-25-10:42:
Mobile settings must make clear which controls are global (all projects) vs project-scoped; group scheduling fields under labeled Global/This-project subheadings with a scope badge so operators don't mistake the global concurrency cap for a per-project setting.
*/
interface ScopeGroupHeaderProps {
title: string;
caption: string;
badgeLabel: string;
scope: "global" | "project";
}
function ScopeGroupHeader({ title, caption, badgeLabel, scope }: ScopeGroupHeaderProps) {
return (<div className="settings-scope-group">
<div className="settings-scope-group-header">
<h5 className="settings-section-heading">{title}</h5>
<span className={`settings-scope-badge settings-scope-badge--${scope}`}>{badgeLabel}</span>
</div>
<small className="settings-scope-caption">{caption}</small>
</div>);
}
export function SchedulingSection({ scopeBanner, form, setForm, globalMaxConcurrent, concurrencyLoading = false, onGlobalMaxConcurrentChange, onOverlapIgnorePathChange, onOpenOverlapPathPicker, onRemoveOverlapIgnorePath, onAddOverlapIgnorePath, onOpenWorkflowSettings, }: SchedulingSectionProps) {
const { t } = useTranslation("app");
return (<>
@@ -26,6 +45,7 @@ export function SchedulingSection({ scopeBanner, form, setForm, globalMaxConcurr
FNXC:SettingsConcurrency 2026-06-22-20:18:
Concurrency inputs represent live project/global limits. Keep them disabled while their actual values are still loading so users cannot edit a blank fallback and accidentally overwrite the resolved limits.
*/}
<ScopeGroupHeader scope="global" title={t("settings.scheduling.scopeGlobalTitle", "Global — applies to all projects")} caption={t("settings.scheduling.scopeGlobalCaption", "Shared by every project on this machine.")} badgeLabel={t("settings.scheduling.scopeBadgeGlobal", "Global")}/>
<div className="form-group">
<label htmlFor="globalMaxConcurrent">{t("settings.scheduling.globalMaxConcurrent", "Global Max Concurrent")}</label>
<input id="globalMaxConcurrent" type="number" min={0} max={10000} disabled={concurrencyLoading} value={globalMaxConcurrent ?? ""} onChange={(e) => {
@@ -34,6 +54,8 @@ export function SchedulingSection({ scopeBanner, form, setForm, globalMaxConcurr
}}/>
<small className="form-text text-muted">{t("settings.scheduling.maximumConcurrentAgentsAcrossAllProjects", "Maximum concurrent agents across all projects")}</small>
</div>
<div className="settings-section-divider"/>
<ScopeGroupHeader scope="project" title={t("settings.scheduling.scopeProjectTitle", "This project")} caption={t("settings.scheduling.scopeProjectCaption", "Only affects the currently selected project.")} badgeLabel={t("settings.scheduling.scopeBadgeProject", "Project")}/>
<div className="form-group">
<label htmlFor="maxConcurrent">{t("settings.scheduling.maxConcurrentTasks", "Max Concurrent Tasks")}</label>
<input id="maxConcurrent" type="number" min={1} max={10} disabled={concurrencyLoading} value={form.maxConcurrent ?? ""} onChange={(e) => {

View File

@@ -0,0 +1,188 @@
import { useCallback, useEffect, useReducer, useRef, useState } from "react";
import { fetchGlobalConcurrency, updateGlobalConcurrency } from "../api/legacy";
/*
FNXC:GlobalConcurrencyControls 2026-06-25-22:45:
The global Max Concurrent cap is ONE shared, cross-project value persisted via /api/global-concurrency. Two independently-mounted sliders (the footer EngineControlMenu and the Command Center Concurrency card) must read and write a single source of truth, so this hook is backed by a MODULE-LEVEL shared store (a singleton cache plus a Set of subscriber callbacks). Without a shared store the two sliders kept private copies and produced last-writer-wins / stale-clobber bugs: dragging one slider, then opening the other, showed (and could re-persist) a stale value over the real cap.
Invariants this hook enforces, all of which prior duplicated logic broke:
- Revalidate after EVERY successful PUT: commit() calls setCache(), which notifies ALL subscribers so both sliders re-sync to the just-persisted value.
- Treat fetch-error as NON-interactive: a failed load left the old slider enabled showing the floor value (1); a drag then persisted 1 over the real cap. interactive is true ONLY when status === "loaded".
- Surface save-state (saving / saved / error) like the per-project sliders, including a retry-friendly error that KEEPS the user's value (never silently reverts).
- Flush pending debounced edits on close/unmount: dragging then closing the menu (or unmounting the card) used to drop the in-flight write. A cleanup flush commits it immediately.
- Language changes (i18n `t`) must NOT refetch/reset, so `t` is intentionally never in a dependency array here.
*/
const SLIDER_MIN = 1;
const SLIDER_BASE_MAX = 32;
const DEBOUNCE_MS = 500;
type GlobalConcurrencyStatus = "idle" | "loading" | "loaded" | "error";
// FNXC:GlobalConcurrencyControls 2026-06-25-22:45: Module-level singleton cache + subscriber set is the single source of truth shared by every mounted hook instance.
const cache: { value: number | null; status: GlobalConcurrencyStatus } = {
value: null,
status: "idle",
};
const subscribers = new Set<() => void>();
let inFlight: Promise<void> | null = null;
function notify() {
for (const subscriber of subscribers) subscriber();
}
function setCache(next: { value: number | null; status: GlobalConcurrencyStatus }) {
cache.value = next.value;
cache.status = next.status;
notify();
}
// FNXC:GlobalConcurrencyControls 2026-06-25-22:45: Fetch once and dedupe concurrent callers via an in-flight promise. On error keep the previous value and mark status "error" so the slider goes non-interactive instead of falsely showing the floor.
function ensureFetched(force = false): Promise<void> {
if (inFlight) return inFlight;
if (!force && cache.status === "loaded") return Promise.resolve();
setCache({ value: cache.value, status: "loading" });
inFlight = (async () => {
try {
const result = await fetchGlobalConcurrency();
setCache({ value: result.globalMaxConcurrent, status: "loaded" });
} catch {
// Keep the previous value; non-interactive while in error state.
setCache({ value: cache.value, status: "error" });
} finally {
inFlight = null;
}
})();
return inFlight;
}
export interface UseGlobalConcurrencyResult {
value: number;
min: number;
sliderMax: number;
interactive: boolean;
status: GlobalConcurrencyStatus;
saveState: "idle" | "saving" | "saved" | "error";
setValue: (raw: string) => void;
}
function clamp(value: number, min: number, max: number) {
return Math.min(max, Math.max(min, value));
}
/**
* FNXC:GlobalConcurrencyControls 2026-06-25-22:45:
* Shared hook for the cross-project global concurrency cap. `activeWhen` (default true)
* gates fetching so the footer menu only loads when open; the Command Center card calls
* it with no args. Backed by the module-level shared store above.
*/
export function useGlobalConcurrency(opts?: { activeWhen?: boolean }): UseGlobalConcurrencyResult {
const activeWhen = opts?.activeWhen ?? true;
// Force a re-render whenever the shared store notifies this instance.
const [, bump] = useReducer((n: number) => n + 1, 0);
const [saveState, setSaveState] = useState<"idle" | "saving" | "saved" | "error">("idle");
// Local optimistic state for snappy dragging.
const dirtyRef = useRef(false);
const pendingValueRef = useRef<number | null>(null);
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const [localValue, setLocalValue] = useState<number | null>(null);
// Subscribe to the shared store on mount.
useEffect(() => {
subscribers.add(bump);
return () => {
subscribers.delete(bump);
};
}, []);
const currentValue = (dirtyRef.current ? localValue : cache.value) ?? SLIDER_MIN;
// FNXC:GlobalConcurrencyControls 2026-06-25-22:45: sliderMax expands past the base cap so already-persisted values >32 still render truthfully.
const sliderMax = Math.max(SLIDER_BASE_MAX, currentValue);
// FNXC:GlobalConcurrencyControls 2026-06-26-06:05: Track the current value in a ref so setValue (a stable useCallback) clamps against the real ceiling without going stale; used to give the clamp an actual upper bound.
const currentValueRef = useRef(currentValue);
currentValueRef.current = currentValue;
const commit = useCallback((v: number) => {
// FNXC:GlobalConcurrencyControls 2026-06-26-06:05: Synchronously null the pending ref BEFORE the async PUT so the close-flush and unmount-cleanup guards (`dirtyRef && pendingValueRef != null`) are already false — otherwise a close-then-unmount within the in-flight window fires commit() twice and sends a duplicate PUT. dirtyRef stays true until the PUT resolves so the slider keeps showing the user's value (no snap-back) during save.
pendingValueRef.current = null;
setSaveState("saving");
void updateGlobalConcurrency({ globalMaxConcurrent: v })
.then(() => {
// Notifies ALL subscribers → both sliders re-sync to the persisted value.
setCache({ value: v, status: "loaded" });
dirtyRef.current = false;
pendingValueRef.current = null;
setLocalValue(null);
setSaveState("saved");
})
.catch(() => {
// KEEP dirty + the user's value so the next drag retries; never silently revert.
setSaveState("error");
});
}, []);
const setValue = useCallback((raw: string) => {
// FNXC:GlobalConcurrencyControls 2026-06-26-06:05: Clamp against a real ceiling (base cap expanded only by the already-persisted value), not Number(raw) — the latter made the upper bound equal the input, so there was no effective ceiling and a programmatic caller could set an arbitrarily large cap.
const next = clamp(Number(raw), SLIDER_MIN, Math.max(SLIDER_BASE_MAX, currentValueRef.current));
dirtyRef.current = true;
pendingValueRef.current = next;
setLocalValue(next);
setSaveState("saving");
if (timerRef.current) clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => {
timerRef.current = null;
commit(next);
}, DEBOUNCE_MS);
}, [commit]);
// FNXC:GlobalConcurrencyControls 2026-06-26-06:05: Force-revalidate each time the surface activates (menu opens / card mounts). The cap can be written out-of-band — notably the Settings modal persists globalMaxConcurrent directly via updateGlobalConcurrency() without going through this store — so a plain "fetch once then never again" cache would show a stale value after such a save. Forcing on activate keeps every consumer truthful; concurrent forces still dedupe via the in-flight promise. `t` deliberately excluded so language changes never refetch/reset.
useEffect(() => {
if (!activeWhen) return;
void ensureFetched(true);
}, [activeWhen]);
// FNXC:GlobalConcurrencyControls 2026-06-25-22:45: When the shared cache changes and we are not mid-edit, drop the local pending so the slider reflects the new shared value.
useEffect(() => {
if (!dirtyRef.current) {
setLocalValue(null);
pendingValueRef.current = null;
}
}, [cache.value]);
// FLUSH ON CLOSE/UNMOUNT: commit any pending debounced write immediately so a drag-then-close/unmount never drops it.
useEffect(() => {
if (activeWhen) return;
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
if (dirtyRef.current && pendingValueRef.current != null) {
commit(pendingValueRef.current);
}
}, [activeWhen, commit]);
useEffect(() => {
return () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
if (dirtyRef.current && pendingValueRef.current != null) {
commit(pendingValueRef.current);
}
};
}, [commit]);
return {
value: currentValue,
min: SLIDER_MIN,
sliderMax,
// interactive ONLY when loaded; loading/error → disabled slider.
interactive: cache.status === "loaded",
status: cache.status,
saveState,
setValue,
};
}