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

Add a "Global Max Concurrent" slider to the footer engine menu and the
Command Center Concurrency card, and group the Scheduling settings by
Global vs Project scope so the global cap isn't mistaken for a per-project
setting (clearer on mobile).

Both sliders are backed by a single shared `useGlobalConcurrency` hook
(module-level store) so they read/write one source of truth and revalidate
after every PUT /api/global-concurrency — fixing the last-writer-wins and
stale-clobber races a per-component cache would cause. The hook treats a
fetch error as non-interactive (slider disabled, not stuck at 1), surfaces
a save-state indicator, and flushes a pending edit on menu close / unmount
so a quick drag-then-dismiss is never silently dropped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-06-25 22:19:24 -07:00
parent b6b5583f01
commit 1d860ec310
8 changed files with 371 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; 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 { .engine-control-menu__save-state--saving {
color: var(--color-warning); color: var(--color-warning);
} }
@@ -84,6 +92,12 @@
color: var(--color-error); 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 { .engine-control-menu__slider {
display: flex; display: flex;
flex-direction: column; 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 { Pause, Play, SlidersHorizontal, Square } from "lucide-react";
import { fetchConfig, fetchSettings, updateSettings } from "../api/legacy"; import { fetchConfig, fetchSettings, updateSettings } from "../api/legacy";
import { useAppSettings } from "../hooks/useAppSettings"; 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 { export interface EngineControlMenuHandle {
open: () => void; 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 [concurrencyState, setConcurrencyState] = useState<AsyncState<ConcurrencyValues>>({ status: "idle", data: null, error: null });
const [concurrencyDirty, setConcurrencyDirty] = useState(false); const [concurrencyDirty, setConcurrencyDirty] = useState(false);
const [concurrencySaveState, setConcurrencySaveState] = useState<"idle" | "saving" | "saved" | "error">("idle"); 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 closeMenu = useCallback(() => setOpen(false), []);
const openMenu = useCallback(() => setOpen(true), []); const openMenu = useCallback(() => setOpen(true), []);
@@ -167,6 +171,16 @@ export const EngineControlMenu = forwardRef<EngineControlMenuHandle, EngineContr
}; };
const concurrencyValues = concurrencyState.data ?? DEFAULT_CONCURRENCY_VALUES; 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…/Saving…/Saved/Save failed/Ready).
const globalSaveLabel = gc.status === "loading" || gc.status === "idle"
? t("commandCenter.controls.status.loading", "Loading…")
: 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" const saveLabel = concurrencyState.status === "loading"
? t("commandCenter.controls.status.loading", "Loading…") ? t("commandCenter.controls.status.loading", "Loading…")
: concurrencySaveState === "saving" : concurrencySaveState === "saving"
@@ -219,6 +233,37 @@ export const EngineControlMenu = forwardRef<EngineControlMenuHandle, EngineContr
</button> </button>
</div> </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 engine-control-menu__section--sliders">
<div className="engine-control-menu__section-header"> <div className="engine-control-menu__section-header">
<span>{t("commandCenter.controls.concurrency.title", "Concurrency")}</span> <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); 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 { .settings-description {
font-size: 13px; font-size: 13px;
color: var(--text-dim); color: var(--text-dim);

View File

@@ -90,6 +90,18 @@
font-size: 0.8125rem; 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 { .cc-controls-slider-label {
display: flex; display: flex;
align-items: center; 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 { DEFAULT_PROJECT_SETTINGS, type ColorTheme, type ThemeMode } from "@fusion/core";
import { fetchConfig, fetchSettings, updateSettings } from "../../api/legacy"; import { fetchConfig, fetchSettings, updateSettings } from "../../api/legacy";
import { useAppSettings } from "../../hooks/useAppSettings"; 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 { ThemeDropdown } from "../ThemeDropdown";
import type { TaskView } from "../../hooks/useViewState"; import type { TaskView } from "../../hooks/useViewState";
import "./CommandCenterControls.css"; 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 [concurrencyState, setConcurrencyState] = useState<AsyncState<ConcurrencyValues>>({ status: "loading", data: null, error: null });
const [concurrencyDirty, setConcurrencyDirty] = useState(false); const [concurrencyDirty, setConcurrencyDirty] = useState(false);
const [concurrencySaveState, setConcurrencySaveState] = useState<"idle" | "saving" | "saved" | "error">("idle"); 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(() => { useEffect(() => {
let cancelled = false; let cancelled = false;
@@ -145,6 +149,16 @@ export function CommandCenterControls({ projectId, colorTheme, themeMode, shadcn
const effectiveGlobalPaused = globalPaused; const effectiveGlobalPaused = globalPaused;
const concurrencyValues = concurrencyState.data ?? DEFAULT_CONCURRENCY_VALUES; 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.
const globalSaveLabel = gc.status === "loading" || gc.status === "idle"
? t("commandCenter.controls.status.loading", "Loading…")
: 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: FNXC:CommandCenter 2026-06-20-00:20:
@@ -239,6 +253,32 @@ export function CommandCenterControls({ projectId, colorTheme, themeMode, shadcn
</span> </span>
</div> </div>
<div className="cc-controls-sliders"> <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"> <label className="cc-controls-slider" htmlFor="cc-max-concurrent">
<span className="cc-controls-slider-label"> <span className="cc-controls-slider-label">
{t("commandCenter.controls.concurrency.maxConcurrent", "Max concurrent tasks")} {t("commandCenter.controls.concurrency.maxConcurrent", "Max concurrent tasks")}

View File

@@ -17,6 +17,25 @@ export interface SchedulingSectionProps {
onAddOverlapIgnorePath: () => void; onAddOverlapIgnorePath: () => void;
onOpenWorkflowSettings?: () => 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) { export function SchedulingSection({ scopeBanner, form, setForm, globalMaxConcurrent, concurrencyLoading = false, onGlobalMaxConcurrentChange, onOverlapIgnorePathChange, onOpenOverlapPathPicker, onRemoveOverlapIgnorePath, onAddOverlapIgnorePath, onOpenWorkflowSettings, }: SchedulingSectionProps) {
const { t } = useTranslation("app"); const { t } = useTranslation("app");
return (<> return (<>
@@ -26,6 +45,7 @@ export function SchedulingSection({ scopeBanner, form, setForm, globalMaxConcurr
FNXC:SettingsConcurrency 2026-06-22-20:18: 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. 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"> <div className="form-group">
<label htmlFor="globalMaxConcurrent">{t("settings.scheduling.globalMaxConcurrent", "Global Max Concurrent")}</label> <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) => { <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> <small className="form-text text-muted">{t("settings.scheduling.maximumConcurrentAgentsAcrossAllProjects", "Maximum concurrent agents across all projects")}</small>
</div> </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"> <div className="form-group">
<label htmlFor="maxConcurrent">{t("settings.scheduling.maxConcurrentTasks", "Max Concurrent Tasks")}</label> <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) => { <input id="maxConcurrent" type="number" min={1} max={10} disabled={concurrencyLoading} value={form.maxConcurrent ?? ""} onChange={(e) => {

View File

@@ -0,0 +1,182 @@
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);
const commit = useCallback((v: number) => {
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) => {
const next = clamp(Number(raw), SLIDER_MIN, Math.max(SLIDER_BASE_MAX, Number(raw)));
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]);
// Fetch when activeWhen becomes true. `t` deliberately excluded so language changes never refetch/reset.
useEffect(() => {
if (!activeWhen) return;
void ensureFetched();
}, [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,
};
}