From 5db90675e95b77717877bf8b030f15ebc281bab4 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Tue, 23 Jun 2026 22:12:04 -0700 Subject: [PATCH] refactor(dashboard): extract DashboardBanners component from App.tsx Extract the conditional banner cluster (~14 banners each gated on viewMode === "project" && currentProject) into a presentational component packages/dashboard/app/components/dashboard/DashboardBanners.tsx with a typed DashboardBannersProps interface (39 fields) added to types.ts. The TaskIdIntegrityBanner setDashboardHealth updater and all FNXC comments move verbatim; banner components are imported directly from siblings. App.tsx: 1,704 -> 1,636 lines. Behavior-preserving; App.test.tsx identical (5 pre-existing experimental-flag failures, none introduced). Verified by typecheck and eslint. Completes U7 (App.tsx module-breakup plan). --- packages/dashboard/app/App.tsx | 164 +++++------------ .../components/dashboard/DashboardBanners.tsx | 168 ++++++++++++++++++ .../app/components/dashboard/types.ts | 59 +++++- 3 files changed, 274 insertions(+), 117 deletions(-) create mode 100644 packages/dashboard/app/components/dashboard/DashboardBanners.tsx diff --git a/packages/dashboard/app/App.tsx b/packages/dashboard/app/App.tsx index 0d895d72a0..beb4a7fb8a 100644 --- a/packages/dashboard/app/App.tsx +++ b/packages/dashboard/app/App.tsx @@ -12,20 +12,7 @@ import { AppModals } from "./components/AppModals"; import { DashboardLoader, type DashboardLoaderStage } from "./components/DashboardLoader"; import { TopProgressBar } from "./components/TopProgressBar"; import { ExecutorStatusBar } from "./components/ExecutorStatusBar"; -import { SessionNotificationBanner, type CliActionId } from "./components/SessionNotificationBanner"; -import { CliBinaryInstallBanner } from "./components/CliBinaryInstallBanner"; -import { SetupWarningBanner } from "./components/SetupWarningBanner"; -import { TestModeBanner } from "./components/TestModeBanner"; -import { EngineUnavailableBanner } from "./components/EngineUnavailableBanner"; -import { OAuthReloginBanner } from "./components/OAuthReloginBanner"; -import { TaskIdIntegrityBanner } from "./components/TaskIdIntegrityBanner"; -import { DbCorruptionBanner } from "./components/DbCorruptionBanner"; -import { UpdateAvailableBanner } from "./components/UpdateAvailableBanner"; -import MergeAdvanceNotice from "./components/MergeAdvanceNotice"; -import { ApprovalNotificationBanner } from "./components/ApprovalNotificationBanner"; -import { GitHubStarPrompt } from "./components/GitHubStarPrompt"; -import { OnboardingResumeCard } from "./components/OnboardingResumeCard"; -import { PostOnboardingRecommendations } from "./components/PostOnboardingRecommendations"; +import { type CliActionId } from "./components/SessionNotificationBanner"; import { isOnboardingCompleted, isOnboardingResumable, @@ -120,7 +107,8 @@ export { import { subscribeSse } from "./sse-bus"; import { AuthTokenRecoveryDialog } from "./components/AuthTokenRecoveryDialog"; import { MainContent } from "./components/dashboard/MainContent"; -import type { MainContentProps } from "./components/dashboard/types"; +import { DashboardBanners } from "./components/dashboard/DashboardBanners"; +import type { DashboardBannersProps, MainContentProps } from "./components/dashboard/types"; // ChatView's CSS is imported eagerly so the styles bundle into the main // CSS file. Without this, the lazy ChatView JS chunk loaded its own CSS @@ -1254,6 +1242,50 @@ function AppInner() { // Top progress bar reflects any in-flight revalidation: projects, current-project, or tasks. // Add new sources here, not inside TopProgressBar. const isRevalidating = projectsLoading || currentProjectLoading || isStale; + + // Props for the extracted cluster (see components/dashboard/DashboardBanners.tsx). + // Every value is passed by its App name; the cluster renders the same banners as before. + const dashboardBannersProps: DashboardBannersProps = { + viewMode, + currentProject, + isTestMode, + dashboardHealth, + setDashboardHealth, + taskView, + modalManager, + sessionBannersHidden, + sessionsNeedingInput, + handleOpenBackgroundSession, + handleDismissNeedingInputSession, + handleDismissAllNeedingInputSessions, + handleCliAction, + getCliActionDisabledReasonForBanner, + openSettingsWithNav, + showOnboardingResumeCard, + showPostOnboardingRecommendations, + updateAvailable, + latestVersion, + currentVersion, + updateBannerDismissed, + dismissUpdateBanner, + refreshDbCorruptionHealth, + dbCorruptionRefreshing, + dbCorruptionRefreshError, + setupReadinessLoading, + hasWarnings, + setupWarningDismissed, + handleDismissSetupWarning, + hasAiProvider, + hasGithub, + approvalBannerCandidate, + dismissApproval, + mailboxPendingApprovalCount, + handleTaskViewChange, + showGitHubStarPrompt, + gitHubStarPromptShown, + markGitHubStarPromptShown, + setShowGitHubStarPrompt, + }; const rightDock = useRightDockController({ active: rightDockActive, projectId: currentProject?.id, addToast, settingsLoaded, researchReadinessVersion, goalAnchorId, tasks: isRemote && remoteData.tasks.length > 0 ? remoteData.tasks : tasks, workflowSteps, subscribePluginEvents, openDetailTask, openFileInBrowser, openSettings: (section?: string) => openSettingsWithNav(section as SectionId), onOpenUsage: openUsageWithNav, onOpenActivityLog: openActivityLogWithNav, onOpenGitHubImport: openGitHubImportWithNav, onOpenGitManager: openGitManagerWithNav, onOpenSchedules: openSchedulesWithNav, onSendSelectionToTask: modalManager.openNewTaskWithDescription, onCreateTaskFromInsight: handleInsightTaskCreate, onNavigateToMission: handleOpenMission, onTaskCreated: (task: Task) => ingestCreatedTasks([task]), workflowStepNameLookup, prAuthAvailable, autoMerge, visibilityOptions: { experimentalFeatures: { insights: insightsEnabled, memoryView: memoryEnabled, devServerView: devServerEnabled, researchView: researchEnabled, evalsView: evalsEnabled, goalsView: goalsEnabled }, showSkillsTab: skillsEnabled, todosEnabled, pluginDashboardViews }, footerVisible: executorFooterVisible }); return ( @@ -1339,107 +1371,7 @@ function AppInner() { ) : undefined } /> - {viewMode === "project" && currentProject && ( - <> - - - openSettingsWithNav("authentication" as SectionId)} - /> - - )} - {viewMode === "project" && currentProject && taskView !== "missions" && !modalManager.isPlanningOpen && !sessionBannersHidden && ( - - )} - {viewMode === "project" && currentProject && ( - openSettingsWithNav("general" as SectionId)} - /> - )} - {viewMode === "project" && currentProject && showOnboardingResumeCard && ( - - )} - {viewMode === "project" && currentProject && showPostOnboardingRecommendations && ( - openSettingsWithNav(section as SectionId)} - /> - )} - {viewMode === "project" && currentProject && updateAvailable && latestVersion && currentVersion && !updateBannerDismissed && ( - - )} - {viewMode === "project" && currentProject && ( - - )} - {viewMode === "project" && currentProject && dashboardHealth?.taskIdIntegrity?.status === "anomaly" && dashboardHealth.taskIdIntegrity.recommendedAction && ( - { - setDashboardHealth((current) => { - if (!current) { - return null; - } - return { - ...current, - status: - report.status === "anomaly" - || !current.database.healthy - || current.database.corruptionDetected - ? "degraded" - : "ok", - taskIdIntegrity: { - ...report, - recommendedAction, - }, - }; - }); - }} - /> - )} - {viewMode === "project" && currentProject && dashboardHealth?.database?.corruptionDetected === true && ( - - )} - {viewMode === "project" && currentProject && !setupReadinessLoading && hasWarnings && !setupWarningDismissed && ( - - )} - {viewMode === "project" && currentProject && approvalBannerCandidate && ( - handleTaskViewChange("mailbox")} - onDismiss={() => dismissApproval(approvalBannerCandidate)} - /> - )} - {/* FNXC:Onboarding 2026-06-22-03:11: The one-time GitHub star prompt stays tied to first completed task, but first-run setup must finish the optional persistent-agent create/skip step before any star ask can surface. Do not add a second setup-specific star prompt. */} - {viewMode === "project" && currentProject && showGitHubStarPrompt && !gitHubStarPromptShown && !modalManager.setupWizardOpen && ( - { - markGitHubStarPromptShown(); - setShowGitHubStarPrompt(false); - }} - /> - )} +
{sidebarActive && ( + {viewMode === "project" && currentProject && ( + <> + + + openSettingsWithNav("authentication" as SectionId)} + /> + + )} + {viewMode === "project" && currentProject && taskView !== "missions" && !modalManager.isPlanningOpen && !sessionBannersHidden && ( + + )} + {viewMode === "project" && currentProject && ( + openSettingsWithNav("general" as SectionId)} + /> + )} + {viewMode === "project" && currentProject && showOnboardingResumeCard && ( + + )} + {viewMode === "project" && currentProject && showPostOnboardingRecommendations && ( + openSettingsWithNav(section as SectionId)} + /> + )} + {viewMode === "project" && currentProject && updateAvailable && latestVersion && currentVersion && !updateBannerDismissed && ( + + )} + {viewMode === "project" && currentProject && ( + + )} + {viewMode === "project" && currentProject && dashboardHealth?.taskIdIntegrity?.status === "anomaly" && dashboardHealth.taskIdIntegrity.recommendedAction && ( + { + setDashboardHealth((current) => { + if (!current) { + return null; + } + return { + ...current, + status: + report.status === "anomaly" + || !current.database.healthy + || current.database.corruptionDetected + ? "degraded" + : "ok", + taskIdIntegrity: { + ...report, + recommendedAction, + }, + }; + }); + }} + /> + )} + {viewMode === "project" && currentProject && dashboardHealth?.database?.corruptionDetected === true && ( + + )} + {viewMode === "project" && currentProject && !setupReadinessLoading && hasWarnings && !setupWarningDismissed && ( + + )} + {viewMode === "project" && currentProject && approvalBannerCandidate && ( + handleTaskViewChange("mailbox")} + onDismiss={() => dismissApproval(approvalBannerCandidate)} + /> + )} + {/* FNXC:Onboarding 2026-06-22-03:11: The one-time GitHub star prompt stays tied to first completed task, but first-run setup must finish the optional persistent-agent create/skip step before any star ask can surface. Do not add a second setup-specific star prompt. */} + {viewMode === "project" && currentProject && showGitHubStarPrompt && !gitHubStarPromptShown && !modalManager.setupWizardOpen && ( + { + markGitHubStarPromptShown(); + setShowGitHubStarPrompt(false); + }} + /> + )} + + ); +} diff --git a/packages/dashboard/app/components/dashboard/types.ts b/packages/dashboard/app/components/dashboard/types.ts index cf645bae82..6e5769883f 100644 --- a/packages/dashboard/app/components/dashboard/types.ts +++ b/packages/dashboard/app/components/dashboard/types.ts @@ -20,7 +20,14 @@ import type { ThemeMode, WorkflowStep, } from "@fusion/core"; -import type { ModelInfo, NodeInfo, ProjectInfo, ProjectInfoWithSource } from "../../api"; +import type { + AiSessionSummary, + DashboardHealthResponse, + ModelInfo, + NodeInfo, + ProjectInfo, + ProjectInfoWithSource, +} from "../../api"; import type { FusionShellApi } from "../../types/native-shell"; import type { DetailTaskOrigin, DetailTaskTab, ModalManager } from "../../hooks/useModalManager"; import type { PluginTaskView, TaskView, ViewMode } from "../../hooks/useViewState"; @@ -28,6 +35,8 @@ import type { ToastType } from "../../hooks/useToast"; import type { QuickChatButtonMode } from "../../hooks/useAppSettings"; import type { UseRemoteNodeDataResult } from "../../hooks/useRemoteNodeData"; import type { SectionId } from "../SettingsModal"; +import type { CliActionId } from "../SessionNotificationBanner"; +import type { ApprovalBannerCandidate } from "../../utils/appLifecycle"; // The lazy view components are value exports; importing them as values lets us // spell their types via `typeof` so MainContent's JSX gets full prop checking. import { SettingsView } from "../SettingsModal"; @@ -215,3 +224,51 @@ export interface MainContentProps { _SettingsView: LazyExoticComponent; _WorkflowEditorView: LazyExoticComponent; } + +/** + * Props for DashboardBanners — the conditional banner cluster rendered above + * the dashboard-project-shell, extracted verbatim from AppInner's main return + * JSX. Every field is an AppInner-scoped value the cluster closes over; the + * banner components are imported directly by DashboardBanners.tsx. + */ +export interface DashboardBannersProps { + viewMode: ViewMode; + currentProject: ProjectInfo | null; + isTestMode: boolean; + dashboardHealth: DashboardHealthResponse | null; + setDashboardHealth: Dispatch>; + taskView: TaskView; + modalManager: ModalManager; + sessionBannersHidden: boolean; + sessionsNeedingInput: AiSessionSummary[]; + handleOpenBackgroundSession: (session: AiSessionSummary) => void; + handleDismissNeedingInputSession: () => void; + handleDismissAllNeedingInputSessions: () => void; + handleCliAction: (session: AiSessionSummary, action: CliActionId) => Promise; + getCliActionDisabledReasonForBanner: (session: AiSessionSummary, action: CliActionId) => string | null; + openSettingsWithNav: (section?: SectionId) => void; + showOnboardingResumeCard: boolean; + showPostOnboardingRecommendations: boolean; + updateAvailable: boolean; + latestVersion: string | null; + currentVersion: string | null; + updateBannerDismissed: boolean; + dismissUpdateBanner: () => void; + refreshDbCorruptionHealth: () => Promise; + dbCorruptionRefreshing: boolean; + dbCorruptionRefreshError: string | null; + setupReadinessLoading: boolean; + hasWarnings: boolean; + setupWarningDismissed: boolean; + handleDismissSetupWarning: () => void; + hasAiProvider: boolean; + hasGithub: boolean; + approvalBannerCandidate: ApprovalBannerCandidate | null; + dismissApproval: (candidate: ApprovalBannerCandidate) => void; + mailboxPendingApprovalCount: number; + handleTaskViewChange: (newView: TaskView) => void; + showGitHubStarPrompt: boolean; + gitHubStarPromptShown: boolean; + markGitHubStarPromptShown: () => void; + setShowGitHubStarPrompt: Dispatch>; +}