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

View File

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