feat(FN-4306): complete Step 3 — gate and dismiss capacity risk banner

Fusion-Task-Id: FN-4306
Fusion-Task-Lineage: b0ab9b60-de02-40a7-ad8b-0ca18c98e16c
This commit is contained in:
Fusion
2026-05-13 06:32:04 -07:00
committed by gsxdsm
parent c77e36d7bd
commit 7edd2a58b4
2 changed files with 51 additions and 2 deletions

View File

@@ -76,7 +76,7 @@ import { ShellConnectionStatus } from "./components/ShellConnectionStatus";
import { getShellConnectionNativeResult, type ShellConnectionNativeResult } from "./shell-native";
import type { AiSessionSummary, DashboardHealthResponse } from "./api";
import { api, fetchDashboardHealth, fetchUnreadCount, fetchTaskDetail, fetchWorkflowSteps } from "./api";
import { getScopedItem, setScopedItem } from "./utils/projectStorage";
import { getScopedItem, removeScopedItem, setScopedItem } from "./utils/projectStorage";
import { subscribeSse } from "./sse-bus";
import { AUTH_TOKEN_RECOVERY_REQUIRED_EVENT } from "./auth";
import { AuthTokenRecoveryDialog } from "./components/AuthTokenRecoveryDialog";
@@ -140,6 +140,7 @@ const WORKING_BRANCH_FILTER_STORAGE_KEY = "kb-dashboard-working-branch-filter";
const BASE_BRANCH_FILTER_STORAGE_KEY = "kb-dashboard-base-branch-filter";
const NO_BRANCH_FILTER_VALUE = "__fusion:no-branch__";
const APPROVAL_BANNER_DISMISSED_STORAGE_KEY = "fusion:approval-banner-dismissed";
const CAPACITY_RISK_DISMISSED_KEY = "kb-capacity-risk-banner-dismissed";
interface ApprovalBannerCandidate {
dedupeKey: string;
@@ -665,6 +666,9 @@ function AppInner() {
const [setupWarningDismissed, setSetupWarningDismissed] = useState(
() => getScopedItem(SETUP_WARNING_DISMISSED_KEY, currentProject?.id) === "true",
);
const [capacityRiskDismissed, setCapacityRiskDismissed] = useState(
() => getScopedItem(CAPACITY_RISK_DISMISSED_KEY, currentProject?.id) === "true",
);
useEffect(() => {
setSetupWarningDismissed(
@@ -672,6 +676,12 @@ function AppInner() {
);
}, [currentProject?.id]);
useEffect(() => {
setCapacityRiskDismissed(
getScopedItem(CAPACITY_RISK_DISMISSED_KEY, currentProject?.id) === "true",
);
}, [currentProject?.id]);
useEffect(() => {
let cancelled = false;
@@ -708,6 +718,11 @@ function AppInner() {
setSetupWarningDismissed(true);
}, [currentProject?.id]);
const handleDismissCapacityRisk = useCallback(() => {
setScopedItem(CAPACITY_RISK_DISMISSED_KEY, "true", currentProject?.id);
setCapacityRiskDismissed(true);
}, [currentProject?.id]);
// Settings state
const {
maxConcurrent,
@@ -716,6 +731,7 @@ function AppInner() {
enginePaused,
taskStuckTimeoutMs,
staleHighFanoutBlockerAgeThresholdMs,
capacityRiskBannerEnabled,
capacityRiskTodoThreshold,
showQuickChatFAB,
prAuthAvailable,
@@ -753,6 +769,33 @@ function AppInner() {
[agentStats?.todoTaskCount, agentStats?.idleNonEphemeralCount, inProgressCount, inReviewCount, capacityRiskTodoThreshold],
);
const previousCapacityRiskBannerEnabledRef = useRef(capacityRiskBannerEnabled);
const previousCapacityRiskTodoThresholdRef = useRef(capacityRiskTodoThreshold);
const previousCapacityRiskProjectIdRef = useRef(currentProject?.id);
useEffect(() => {
if (previousCapacityRiskProjectIdRef.current !== currentProject?.id) {
previousCapacityRiskProjectIdRef.current = currentProject?.id;
previousCapacityRiskBannerEnabledRef.current = capacityRiskBannerEnabled;
previousCapacityRiskTodoThresholdRef.current = capacityRiskTodoThreshold;
return;
}
const wasEnabled = previousCapacityRiskBannerEnabledRef.current;
const previousThreshold = previousCapacityRiskTodoThresholdRef.current;
const bannerEnabledChangedToTrue = !wasEnabled && capacityRiskBannerEnabled;
const thresholdChanged = previousThreshold !== capacityRiskTodoThreshold;
if (bannerEnabledChangedToTrue || thresholdChanged) {
removeScopedItem(CAPACITY_RISK_DISMISSED_KEY, currentProject?.id);
setCapacityRiskDismissed(false);
}
previousCapacityRiskProjectIdRef.current = currentProject?.id;
previousCapacityRiskBannerEnabledRef.current = capacityRiskBannerEnabled;
previousCapacityRiskTodoThresholdRef.current = capacityRiskTodoThreshold;
}, [capacityRiskBannerEnabled, capacityRiskTodoThreshold, currentProject?.id]);
const skillsEnabled = experimentalFeatures.skillsView === true;
const nodesEnabled = experimentalFeatures.nodesView === true;
const researchEnabled = experimentalFeatures.researchView === true;
@@ -1463,7 +1506,9 @@ function AppInner() {
if (taskView === "board") {
return (
<PageErrorBoundary>
<CapacityRiskBanner signal={capacityRiskSignal} />
{capacityRiskBannerEnabled && !capacityRiskDismissed ? (
<CapacityRiskBanner signal={capacityRiskSignal} onDismiss={handleDismissCapacityRisk} />
) : null}
<Board
tasks={filteredBoardTasks}
projectId={currentProject?.id}

View File

@@ -13,6 +13,7 @@ export interface UseAppSettingsResult {
enginePaused: boolean;
taskStuckTimeoutMs: number | undefined;
staleHighFanoutBlockerAgeThresholdMs: number;
capacityRiskBannerEnabled: boolean;
capacityRiskTodoThreshold: number;
showQuickChatFAB: boolean;
prAuthAvailable: boolean;
@@ -43,6 +44,7 @@ export function useAppSettings(projectId?: string): UseAppSettingsResult {
const [enginePaused, setEnginePaused] = useState(false);
const [taskStuckTimeoutMs, setTaskStuckTimeoutMs] = useState<number | undefined>(undefined);
const [staleHighFanoutBlockerAgeThresholdMs, setStaleHighFanoutBlockerAgeThresholdMs] = useState(2 * 60 * 60 * 1000);
const [capacityRiskBannerEnabled, setCapacityRiskBannerEnabled] = useState(false);
const [capacityRiskTodoThreshold, setCapacityRiskTodoThreshold] = useState(20);
const [showQuickChatFAB, setShowQuickChatFAB] = useState(false);
const [prAuthAvailable, setPrAuthAvailable] = useState(false);
@@ -80,6 +82,7 @@ export function useAppSettings(projectId?: string): UseAppSettingsResult {
settings.staleHighFanoutBlockerAgeThresholdMs ?? 2 * 60 * 60 * 1000,
);
setShowQuickChatFAB(settings.showQuickChatFAB === true);
setCapacityRiskBannerEnabled(settings.capacityRiskBannerEnabled === true);
setCapacityRiskTodoThreshold(settings.capacityRiskTodoThreshold ?? 20);
setExperimentalFeatures(settings.experimentalFeatures ?? {});
const features = settings.experimentalFeatures ?? {};
@@ -177,6 +180,7 @@ export function useAppSettings(projectId?: string): UseAppSettingsResult {
enginePaused,
taskStuckTimeoutMs,
staleHighFanoutBlockerAgeThresholdMs,
capacityRiskBannerEnabled,
capacityRiskTodoThreshold,
showQuickChatFAB,
prAuthAvailable,