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).
This commit is contained in:
gsxdsm
2026-06-23 22:12:04 -07:00
parent e6729a6115
commit 5db90675e9
3 changed files with 274 additions and 117 deletions

View File

@@ -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 <DashboardBanners> 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 && (
<>
<TestModeBanner isActive={isTestMode} />
<EngineUnavailableBanner isVisible={dashboardHealth?.engine?.available === false} />
<OAuthReloginBanner
onReLogin={(_providerId) => openSettingsWithNav("authentication" as SectionId)}
/>
</>
)}
{viewMode === "project" && currentProject && taskView !== "missions" && !modalManager.isPlanningOpen && !sessionBannersHidden && (
<SessionNotificationBanner
sessions={sessionsNeedingInput}
onResumeSession={handleOpenBackgroundSession}
onDismissSession={handleDismissNeedingInputSession}
onDismissAll={handleDismissAllNeedingInputSessions}
onCliAction={handleCliAction}
getCliActionDisabledReason={getCliActionDisabledReasonForBanner}
/>
)}
{viewMode === "project" && currentProject && (
<CliBinaryInstallBanner
onOpenSettings={() => openSettingsWithNav("general" as SectionId)}
/>
)}
{viewMode === "project" && currentProject && showOnboardingResumeCard && (
<OnboardingResumeCard onResume={modalManager.openModelOnboarding} />
)}
{viewMode === "project" && currentProject && showPostOnboardingRecommendations && (
<PostOnboardingRecommendations
onOpenModelOnboarding={modalManager.openModelOnboarding}
onOpenSettings={(section) => openSettingsWithNav(section as SectionId)}
/>
)}
{viewMode === "project" && currentProject && updateAvailable && latestVersion && currentVersion && !updateBannerDismissed && (
<UpdateAvailableBanner
latestVersion={latestVersion}
currentVersion={currentVersion}
onDismiss={dismissUpdateBanner}
/>
)}
{viewMode === "project" && currentProject && (
<MergeAdvanceNotice projectId={currentProject.id} />
)}
{viewMode === "project" && currentProject && dashboardHealth?.taskIdIntegrity?.status === "anomaly" && dashboardHealth.taskIdIntegrity.recommendedAction && (
<TaskIdIntegrityBanner
report={dashboardHealth.taskIdIntegrity}
recommendedAction={dashboardHealth.taskIdIntegrity.recommendedAction}
onRefresh={(report, 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 && (
<DbCorruptionBanner
errors={dashboardHealth.database.corruptionErrors}
lastCheckedAt={dashboardHealth.database.lastCheckedAt}
onRefresh={refreshDbCorruptionHealth}
refreshing={dbCorruptionRefreshing}
refreshError={dbCorruptionRefreshError}
/>
)}
{viewMode === "project" && currentProject && !setupReadinessLoading && hasWarnings && !setupWarningDismissed && (
<SetupWarningBanner
hasAiProvider={hasAiProvider}
hasGithub={hasGithub}
onDismiss={handleDismissSetupWarning}
/>
)}
{viewMode === "project" && currentProject && approvalBannerCandidate && (
<ApprovalNotificationBanner
pendingCount={Math.max(mailboxPendingApprovalCount, 1)}
onOpenMailbox={() => 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 && (
<GitHubStarPrompt
onDismiss={() => {
markGitHubStarPromptShown();
setShowGitHubStarPrompt(false);
}}
/>
)}
<DashboardBanners {...dashboardBannersProps} />
<div className={`dashboard-project-shell${sidebarActive ? " dashboard-project-shell--with-sidebar" : ""}${rightDockActive ? " dashboard-project-shell--with-right-dock" : ""}`} data-testid="dashboard-project-shell">
{sidebarActive && (
<LeftSidebarNav

View File

@@ -0,0 +1,168 @@
/*
FNXC:DashboardBanners 2026-06-24-00:00:
DashboardBanners is the conditional banner cluster rendered above the dashboard-project-shell, extracted verbatim from AppInner's main return JSX. It is a pure render of the same gated banners (every condition, prop, FNXC comment, and the TaskIdIntegrityBanner setDashboardHealth updater preserved byte-for-byte); the banner components are imported directly from their siblings.
*/
import type { DashboardBannersProps } from "./types";
import type { SectionId } from "../SettingsModal";
import { TestModeBanner } from "../TestModeBanner";
import { EngineUnavailableBanner } from "../EngineUnavailableBanner";
import { OAuthReloginBanner } from "../OAuthReloginBanner";
import { SessionNotificationBanner } from "../SessionNotificationBanner";
import { CliBinaryInstallBanner } from "../CliBinaryInstallBanner";
import { OnboardingResumeCard } from "../OnboardingResumeCard";
import { PostOnboardingRecommendations } from "../PostOnboardingRecommendations";
import { UpdateAvailableBanner } from "../UpdateAvailableBanner";
import MergeAdvanceNotice from "../MergeAdvanceNotice";
import { TaskIdIntegrityBanner } from "../TaskIdIntegrityBanner";
import { DbCorruptionBanner } from "../DbCorruptionBanner";
import { SetupWarningBanner } from "../SetupWarningBanner";
import { ApprovalNotificationBanner } from "../ApprovalNotificationBanner";
import { GitHubStarPrompt } from "../GitHubStarPrompt";
export function DashboardBanners({
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,
}: DashboardBannersProps) {
return (
<>
{viewMode === "project" && currentProject && (
<>
<TestModeBanner isActive={isTestMode} />
<EngineUnavailableBanner isVisible={dashboardHealth?.engine?.available === false} />
<OAuthReloginBanner
onReLogin={(_providerId) => openSettingsWithNav("authentication" as SectionId)}
/>
</>
)}
{viewMode === "project" && currentProject && taskView !== "missions" && !modalManager.isPlanningOpen && !sessionBannersHidden && (
<SessionNotificationBanner
sessions={sessionsNeedingInput}
onResumeSession={handleOpenBackgroundSession}
onDismissSession={handleDismissNeedingInputSession}
onDismissAll={handleDismissAllNeedingInputSessions}
onCliAction={handleCliAction}
getCliActionDisabledReason={getCliActionDisabledReasonForBanner}
/>
)}
{viewMode === "project" && currentProject && (
<CliBinaryInstallBanner
onOpenSettings={() => openSettingsWithNav("general" as SectionId)}
/>
)}
{viewMode === "project" && currentProject && showOnboardingResumeCard && (
<OnboardingResumeCard onResume={modalManager.openModelOnboarding} />
)}
{viewMode === "project" && currentProject && showPostOnboardingRecommendations && (
<PostOnboardingRecommendations
onOpenModelOnboarding={modalManager.openModelOnboarding}
onOpenSettings={(section) => openSettingsWithNav(section as SectionId)}
/>
)}
{viewMode === "project" && currentProject && updateAvailable && latestVersion && currentVersion && !updateBannerDismissed && (
<UpdateAvailableBanner
latestVersion={latestVersion}
currentVersion={currentVersion}
onDismiss={dismissUpdateBanner}
/>
)}
{viewMode === "project" && currentProject && (
<MergeAdvanceNotice projectId={currentProject.id} />
)}
{viewMode === "project" && currentProject && dashboardHealth?.taskIdIntegrity?.status === "anomaly" && dashboardHealth.taskIdIntegrity.recommendedAction && (
<TaskIdIntegrityBanner
report={dashboardHealth.taskIdIntegrity}
recommendedAction={dashboardHealth.taskIdIntegrity.recommendedAction}
onRefresh={(report, 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 && (
<DbCorruptionBanner
errors={dashboardHealth.database.corruptionErrors}
lastCheckedAt={dashboardHealth.database.lastCheckedAt}
onRefresh={refreshDbCorruptionHealth}
refreshing={dbCorruptionRefreshing}
refreshError={dbCorruptionRefreshError}
/>
)}
{viewMode === "project" && currentProject && !setupReadinessLoading && hasWarnings && !setupWarningDismissed && (
<SetupWarningBanner
hasAiProvider={hasAiProvider}
hasGithub={hasGithub}
onDismiss={handleDismissSetupWarning}
/>
)}
{viewMode === "project" && currentProject && approvalBannerCandidate && (
<ApprovalNotificationBanner
pendingCount={Math.max(mailboxPendingApprovalCount, 1)}
onOpenMailbox={() => 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 && (
<GitHubStarPrompt
onDismiss={() => {
markGitHubStarPromptShown();
setShowGitHubStarPrompt(false);
}}
/>
)}
</>
);
}

View File

@@ -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<typeof SettingsView>;
_WorkflowEditorView: LazyExoticComponent<typeof WorkflowNodeEditor>;
}
/**
* 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<SetStateAction<DashboardHealthResponse | null>>;
taskView: TaskView;
modalManager: ModalManager;
sessionBannersHidden: boolean;
sessionsNeedingInput: AiSessionSummary[];
handleOpenBackgroundSession: (session: AiSessionSummary) => void;
handleDismissNeedingInputSession: () => void;
handleDismissAllNeedingInputSessions: () => void;
handleCliAction: (session: AiSessionSummary, action: CliActionId) => Promise<void>;
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<void>;
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<SetStateAction<boolean>>;
}