fix(FN-2572): remove dead onboarding modal exports

- Delete obsolete AppModals code that was accidentally left in ModelOnboardingModal.tsx
- Restore the Task type import used by the onboarding modal after dead code cleanup
- Drop the unused SettingsModal.css import tied to removed code paths
- Extend ModelOnboardingModal tests to guard against dead export regressions
This commit is contained in:
Fusion
2026-04-25 20:21:11 -07:00
committed by gsxdsm
parent c644daca77
commit 7555ba61c6
3 changed files with 12 additions and 350 deletions

View File

@@ -1,7 +1,7 @@
import "./ModelOnboardingModal.css";
import "./SettingsModal.css";
import { useState, useEffect, useCallback, useRef } from "react";
import { X, Loader2, CheckCircle, Key, Zap, GitPullRequest, Rocket, Plus, ChevronRight } from "lucide-react";
import type { Task } from "@fusion/core";
import type { AuthProvider, ModelInfo } from "../api";
import {
fetchAuthStatus,
@@ -502,7 +502,6 @@ import {
type OnboardingStep,
} from "./model-onboarding-state";
import { trackOnboardingEvent } from "./onboarding-events";
import type { SectionId } from "./SettingsModal";
export interface ModelOnboardingModalProps {
/** Called when onboarding is complete or dismissed */
@@ -2492,349 +2491,3 @@ export function ModelOnboardingModal({
);
}
import type { ProjectInfo } from "../api";
import type { ColorTheme, Column, MergeResult, Task, ThemeMode } from "@fusion/core";
import type { UseProjectActionsResult } from "../hooks/useProjectActions";
import type { ModalManager } from "../hooks/useModalManager";
import type { UseTaskHandlersResult } from "../hooks/useTaskHandlers";
import type { Toast } from "../hooks/useToast";
import { ModalErrorBoundary } from "./ErrorBoundary";
import { TaskDetailModal } from "./TaskDetailModal";
import { SettingsModal } from "./SettingsModal";
import { GitHubImportModal } from "./GitHubImportModal";
import { PlanningModeModal } from "./PlanningModeModal";
import { SubtaskBreakdownModal } from "./SubtaskBreakdownModal";
import { TerminalModal } from "./TerminalModal";
import { ScriptsModal } from "./ScriptsModal";
import { FileBrowserModal } from "./FileBrowserModal";
import { UsageIndicator } from "./UsageIndicator";
import { ScheduledTasksModal } from "./ScheduledTasksModal";
import { NewTaskModal } from "./NewTaskModal";
import { ActivityLogModal } from "./ActivityLogModal";
import { GitManagerModal } from "./GitManagerModal";
import { WorkflowStepManager } from "./WorkflowStepManager";
import { AgentListModal } from "./AgentListModal";
import { SetupWizardModal } from "./SetupWizardModal";
import { ToastContainer } from "./ToastContainer";
interface AppModalsProps {
projectId?: string;
tasks: Task[];
projects: ProjectInfo[];
currentProject: ProjectInfo | null;
addToast: (message: string, type?: ToastType) => void;
toasts: Toast[];
removeToast: (id: number) => void;
modalManager: ModalManager;
projectActions: Pick<UseProjectActionsResult, "handleSetupComplete" | "handleModelOnboardingComplete">;
taskHandlers: Pick<UseTaskHandlersResult, "handleModalCreate" | "handlePlanningTaskCreated" | "handlePlanningTasksCreated" | "handleSubtaskTasksCreated" | "handleGitHubImport">;
taskOperations: {
moveTask: (taskId: string, column: Column, position?: number) => Promise<Task>;
deleteTask: (taskId: string) => Promise<Task>;
mergeTask: (taskId: string) => Promise<MergeResult>;
retryTask: (taskId: string) => Promise<Task>;
duplicateTask: (taskId: string) => Promise<Task>;
};
deepLink: {
handleDetailClose: () => void;
};
settings: {
prAuthAvailable: boolean;
themeMode: ThemeMode;
colorTheme: ColorTheme;
setThemeMode: (mode: ThemeMode) => void;
setColorTheme: (theme: ColorTheme) => void;
};
/** Optional override for the settings modal close handler. When provided, this is called instead of modalManager.closeSettings. */
onSettingsClose?: () => void;
}
export function AppModals({
projectId,
tasks,
projects,
currentProject,
addToast,
toasts,
removeToast,
modalManager,
projectActions,
taskHandlers,
taskOperations,
deepLink,
settings,
onSettingsClose,
}: AppModalsProps) {
// Use the override handler if provided, otherwise fall back to modalManager.closeSettings
const handleSettingsClose = onSettingsClose ?? modalManager.closeSettings;
// Handlers for onboarding CTAs
const handleOpenNewTask = useCallback(() => {
modalManager.openNewTask();
}, [modalManager]);
const handleOpenGitHubImport = useCallback(() => {
modalManager.openGitHubImport();
}, [modalManager]);
return (
<>
{modalManager.detailTask && (
<ModalErrorBoundary>
<TaskDetailModal
task={modalManager.detailTask}
projectId={projectId}
tasks={tasks}
onClose={deepLink.handleDetailClose}
onOpenDetail={modalManager.openDetailTask}
onMoveTask={taskOperations.moveTask}
onDeleteTask={taskOperations.deleteTask}
onMergeTask={taskOperations.mergeTask}
onRetryTask={taskOperations.retryTask}
onDuplicateTask={taskOperations.duplicateTask}
onTaskUpdated={modalManager.updateDetailTask}
addToast={addToast}
prAuthAvailable={settings.prAuthAvailable}
initialTab={modalManager.detailTaskInitialTab}
/>
</ModalErrorBoundary>
)}
{modalManager.settingsOpen && (
<ModalErrorBoundary>
<SettingsModal
onClose={handleSettingsClose}
addToast={addToast}
initialSection={modalManager.settingsInitialSection}
projectId={projectId}
themeMode={settings.themeMode}
colorTheme={settings.colorTheme}
onThemeModeChange={settings.setThemeMode}
onColorThemeChange={settings.setColorTheme}
/>
</ModalErrorBoundary>
)}
<GitHubImportModal
isOpen={modalManager.githubImportOpen}
onClose={modalManager.closeGitHubImport}
onImport={taskHandlers.handleGitHubImport}
tasks={tasks}
projectId={projectId}
/>
<ModalErrorBoundary>
<PlanningModeModal
isOpen={modalManager.isPlanningOpen}
onClose={modalManager.closePlanning}
onTaskCreated={taskHandlers.handlePlanningTaskCreated}
onTasksCreated={taskHandlers.handlePlanningTasksCreated}
tasks={tasks}
initialPlan={modalManager.planningInitialPlan ?? undefined}
projectId={projectId}
resumeSessionId={modalManager.planningResumeSessionId}
/>
</ModalErrorBoundary>
<ModalErrorBoundary>
<SubtaskBreakdownModal
isOpen={modalManager.isSubtaskOpen}
onClose={modalManager.closeSubtask}
initialDescription={modalManager.subtaskInitialDescription ?? ""}
onTasksCreated={taskHandlers.handleSubtaskTasksCreated}
projectId={projectId}
resumeSessionId={modalManager.subtaskResumeSessionId}
/>
</ModalErrorBoundary>
<TerminalModal
isOpen={modalManager.terminalOpen}
onClose={modalManager.closeTerminal}
initialCommand={modalManager.terminalInitialCommand}
projectId={projectId}
/>
<ScriptsModal
isOpen={modalManager.scriptsOpen}
onClose={modalManager.closeScripts}
addToast={addToast}
onRunScript={modalManager.runScript}
projectId={projectId}
/>
{modalManager.filesOpen && (
<FileBrowserModal
initialWorkspace={modalManager.fileBrowserWorkspace}
isOpen={true}
onClose={modalManager.closeFiles}
onWorkspaceChange={modalManager.setFileWorkspace}
projectId={projectId}
/>
)}
<UsageIndicator
isOpen={modalManager.usageOpen}
onClose={modalManager.closeUsage}
projectId={projectId}
/>
{modalManager.schedulesOpen && (
<ScheduledTasksModal
onClose={modalManager.closeSchedules}
addToast={addToast}
/>
)}
<ModalErrorBoundary>
<NewTaskModal
isOpen={modalManager.newTaskModalOpen}
onClose={modalManager.closeNewTask}
tasks={tasks}
onCreateTask={taskHandlers.handleModalCreate}
addToast={addToast}
projectId={projectId}
onPlanningMode={modalManager.openPlanningWithInitialPlan}
onSubtaskBreakdown={modalManager.openSubtaskBreakdown}
/>
</ModalErrorBoundary>
<ActivityLogModal
isOpen={modalManager.activityLogOpen}
onClose={modalManager.closeActivityLog}
tasks={tasks}
projectId={projectId}
projects={projects}
currentProject={currentProject}
onOpenTaskDetail={(taskId) => {
const task = tasks.find((candidate) => candidate.id === taskId);
if (task) {
modalManager.openDetailTask(task);
}
}}
/>
<ModalErrorBoundary>
<GitManagerModal
isOpen={modalManager.gitManagerOpen}
onClose={modalManager.closeGitManager}
tasks={tasks}
addToast={addToast}
projectId={projectId}
/>
</ModalErrorBoundary>
<ModalErrorBoundary>
<WorkflowStepManager
isOpen={modalManager.workflowStepsOpen}
onClose={modalManager.closeWorkflowSteps}
addToast={addToast}
projectId={projectId}
/>
</ModalErrorBoundary>
<AgentListModal
isOpen={modalManager.agentsOpen}
onClose={modalManager.closeAgents}
addToast={addToast}
projectId={projectId}
/>
{modalManager.setupWizardOpen && (
<SetupWizardModal
onProjectRegistered={projectActions.handleSetupComplete}
onClose={modalManager.closeSetupWizard}
/>
)}
{modalManager.modelOnboardingOpen && (
<ModelOnboardingModal
onComplete={projectActions.handleModelOnboardingComplete}
addToast={addToast}
onOpenNewTask={handleOpenNewTask}
onOpenGitHubImport={handleOpenGitHubImport}
/>
)}
<ToastContainer toasts={toasts} onRemove={removeToast} />
</>
);
}
export interface UseAuthOnboardingOptions {
projectId?: string;
openModelOnboarding: () => void;
openSettings: (section?: SectionId) => void;
}
/**
* Runs auth/onboarding checks and opens the appropriate setup modal.
*
* This hook implements a one-shot guard: the auto-trigger logic runs at most
* once per hook instance (regardless of effect re-runs due to dependency changes).
* This prevents repeat auto-opens on incidental rerenders or project context churn.
*
* Trigger behavior:
* - First-run (onboarding incomplete): opens model onboarding wizard
* - Completed onboarding + unauthenticated providers: opens Settings → Authentication
* - Already configured: no auto-open
*/
export function useAuthOnboarding({
projectId,
openModelOnboarding,
openSettings,
}: UseAuthOnboardingOptions): void {
// One-shot guard: prevents the auto-trigger logic from running more than once
// per hook instance, even if the effect re-runs due to dependency changes.
const hasTriggeredRef = useRef(false);
useEffect(() => {
// Skip if we've already triggered (one-shot guard)
if (hasTriggeredRef.current) return;
// Mark as triggered immediately to prevent any race condition on re-runs
hasTriggeredRef.current = true;
let shouldOpenOnboarding = false;
let shouldOpenSettings = false;
fetchAuthStatus()
.then(({ providers }) => {
const hasAuthenticatedProvider = providers.some((provider) => provider.authenticated);
const needsSetup = providers.length > 0 && !hasAuthenticatedProvider;
if (needsSetup || (providers.length > 0 && hasAuthenticatedProvider)) {
return fetchGlobalSettings()
.then((globalSettings) => {
const hasDefaultModel = !!(
globalSettings.defaultProvider && globalSettings.defaultModelId
);
// Explicit first-run detection: onboarding is incomplete when
// modelOnboardingComplete is false or undefined
const onboardingIncomplete =
globalSettings.modelOnboardingComplete === false ||
globalSettings.modelOnboardingComplete === undefined;
const setupIncomplete = !hasAuthenticatedProvider || !hasDefaultModel;
if (onboardingIncomplete && setupIncomplete) {
shouldOpenOnboarding = true;
} else if (!hasAuthenticatedProvider) {
// Completed onboarding but no authenticated provider → fallback
// to Settings Authentication section
shouldOpenSettings = true;
}
});
}
})
.then(() => {
// Execute after the promise chain resolves
if (shouldOpenOnboarding) {
openModelOnboarding();
} else if (shouldOpenSettings) {
openSettings("authentication");
}
})
.catch(() => {
// Fail silently - non-blocking behavior preserves dashboard usability.
// Onboarding can be manually triggered later via Settings if needed.
});
}, [projectId, openModelOnboarding, openSettings]);
}

View File

@@ -1,7 +1,6 @@
/* === SettingsModal: all settings-related layout, panel, and component styles ===
Extracted from styles.css as part of the Sweep-3 CSS extraction effort.
Imported by SettingsModal.tsx (and MemoryView.tsx / ModelOnboardingModal.tsx
for the classes they share). */
Imported by SettingsModal.tsx (and MemoryView.tsx for shared classes). */
/* === Settings Layout === */
.settings-modal-heading {

View File

@@ -173,6 +173,16 @@ afterEach(() => {
localStorage.removeItem("fn.authToken");
});
describe("ModelOnboardingModal module exports", () => {
it("does not export dead AppModals/auth onboarding symbols", async () => {
const moduleExports = await import("../ModelOnboardingModal");
expect("AppModals" in moduleExports).toBe(false);
expect("useAuthOnboarding" in moduleExports).toBe(false);
expect("UseAuthOnboardingOptions" in moduleExports).toBe(false);
});
});
describe("ModelOnboardingModal", () => {
describe("step structure", () => {
it("renders the AI Setup step by default with all three step indicators", async () => {