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:
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user