import { useCallback, useEffect, useRef, useState, lazy, Suspense } from "react"; import type { ProjectInfo } from "../api"; import type { ColorTheme, Column, MergeResult, Task, TaskCreateInput, ThemeMode, GithubIssueAction } from "@fusion/core"; import type { UseProjectActionsResult } from "../hooks/useProjectActions"; import type { ModalManager } from "../hooks/useModalManager"; import type { UseTaskHandlersResult } from "../hooks/useTaskHandlers"; import type { Toast, ToastType } from "../hooks/useToast"; import { ModalErrorBoundary } from "./ErrorBoundary"; import { TaskDetailModal } from "./TaskDetailModal"; import { GitHubImportModal } from "./GitHubImportModal"; 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 { AgentListModal } from "./AgentListModal"; import { ModelOnboardingModal } from "./ModelOnboardingModal"; import { ToastContainer } from "./ToastContainer"; import { GroupTaskModal } from "./GroupTaskModal"; import { useNavigationHistoryContext } from "../hooks/useNavigationHistory"; const SetupWizardModal = lazy(() => import("./SetupWizardModal").then((m) => ({ default: m.SetupWizardModal }))); const SettingsModal = lazy(() => import("./SettingsModal").then((m) => ({ default: m.SettingsModal }))); const WorkflowNodeEditor = lazy(() => import("./WorkflowNodeEditor").then((m) => ({ default: m.WorkflowNodeEditor }))); function prefetchSettingsModal() { const idle: (cb: () => void, opts?: { timeout?: number }) => number = (typeof window !== "undefined" && (window as Window & { requestIdleCallback?: (cb: () => void, opts?: { timeout?: number }) => number; }).requestIdleCallback) || ((cb: () => void) => globalThis.setTimeout(cb, 200) as unknown as number); idle(() => { void import("./SettingsModal"); }, { timeout: 1_500 }); } 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; taskHandlers: Pick; onPlanningMode?: (initialPlan: string, workflowId?: string | null) => void; onSubtaskBreakdown?: (description: string, workflowId?: string | null) => void; taskOperations: { moveTask: (taskId: string, column: Column, optionsOrPosition?: { preserveProgress?: boolean } | number) => Promise; deleteTask: (taskId: string, options?: { removeDependencyReferences?: boolean; removeLineageReferences?: boolean; githubIssueAction?: GithubIssueAction; allowResurrection?: boolean; }) => Promise; mergeTask: (taskId: string) => Promise; archiveTask: (taskId: string, options?: { removeLineageReferences?: boolean }) => Promise; retryTask: (taskId: string) => Promise; resetTask: (taskId: string) => Promise; duplicateTask: (taskId: string) => Promise; }; deepLink: { handleDetailClose: () => void; }; settings: { prAuthAvailable: boolean; autoMerge: boolean; themeMode: ThemeMode; colorTheme: ColorTheme; dashboardFontScalePct: number; shadcnCustomColors: Record; resolvedThemeMode: "dark" | "light"; setThemeMode: (mode: ThemeMode) => void; setColorTheme: (theme: ColorTheme) => void; setDashboardFontScalePct: (scalePct: number) => void; setShadcnCustomColors: (colors: Record) => void; setQuickChatButtonModeImmediate: (mode: "floating" | "footer" | "off") => void; }; /** Optional override for the settings modal close handler. When provided, this is called instead of modalManager.closeSettings. */ onSettingsClose?: () => void; /** Optional callback to reopen the onboarding guide from Settings. Closes Settings and opens ModelOnboardingModal. */ onReopenOnboarding?: () => void; /** Optional callback to open mailbox approvals from Settings. */ onOpenApprovals?: (approvalId?: string) => void; /** Enables planning-style agent onboarding entry points inside setup. */ agentOnboardingEnabled?: boolean; } export function AppModals({ projectId, tasks, projects, currentProject, addToast, toasts, removeToast, modalManager, projectActions, taskHandlers, onPlanningMode, onSubtaskBreakdown, taskOperations, deepLink, settings, onSettingsClose, onReopenOnboarding, onOpenApprovals, agentOnboardingEnabled = false, }: AppModalsProps) { const { pushNav, removeNav } = useNavigationHistoryContext(); const [firstCreatedTask, setFirstCreatedTask] = useState(null); const detailNavCloseRef = useRef<(() => void) | null>(null); const detailTask = modalManager.detailTask ? (() => { const liveTask = tasks.find((task) => task.id === modalManager.detailTask?.id); if (!liveTask) { return modalManager.detailTask; } if ("prompt" in modalManager.detailTask) { return { ...modalManager.detailTask, ...liveTask, prompt: modalManager.detailTask.prompt, log: modalManager.detailTask.log, }; } return liveTask; })() : null; // Use the override handler if provided, otherwise fall back to modalManager.closeSettings const handleSettingsClose = onSettingsClose ?? modalManager.closeSettings; /* FNXC:TaskDetailBack 2026-06-25-00:00: Modal task detail uses the same idempotent close path for explicit Close and browser/Android Back so deep-link URL cleanup is not skipped during popstate. Each open records the pushed history callback because nested task-detail links can create multiple detail entries with otherwise identical close behavior. */ const closeDetailFromHistory = useCallback(() => { modalManager.closeDetailTask(); deepLink.handleDetailClose(); detailNavCloseRef.current = null; }, [deepLink, modalManager]); const closeDetailWithNav = useCallback(() => { removeNav(detailNavCloseRef.current ?? closeDetailFromHistory); closeDetailFromHistory(); }, [closeDetailFromHistory, removeNav]); const closeGroupWithNav = useCallback(() => { removeNav(modalManager.closeGroupModal); modalManager.closeGroupModal(); }, [modalManager.closeGroupModal, removeNav]); const closeSettingsWithNav = useCallback(() => { removeNav(handleSettingsClose); handleSettingsClose(); }, [handleSettingsClose, removeNav]); const closeGitHubImportWithNav = useCallback(() => { removeNav(modalManager.closeGitHubImport); modalManager.closeGitHubImport(); }, [modalManager.closeGitHubImport, removeNav]); const closeSubtaskWithNav = useCallback(() => { removeNav(modalManager.closeSubtask); modalManager.closeSubtask(); }, [modalManager.closeSubtask, removeNav]); const closeTerminalWithNav = useCallback(() => { removeNav(modalManager.closeTerminal); modalManager.closeTerminal(); }, [modalManager.closeTerminal, removeNav]); const closeScriptsWithNav = useCallback(() => { removeNav(modalManager.closeScripts); modalManager.closeScripts(); }, [modalManager.closeScripts, removeNav]); const closeFilesWithNav = useCallback(() => { removeNav(modalManager.closeFiles); modalManager.closeFiles(); }, [modalManager.closeFiles, removeNav]); const closeUsageWithNav = useCallback(() => { removeNav(modalManager.closeUsage); modalManager.closeUsage(); }, [modalManager.closeUsage, removeNav]); const closeSchedulesWithNav = useCallback(() => { removeNav(modalManager.closeSchedules); modalManager.closeSchedules(); }, [modalManager.closeSchedules, removeNav]); const closeNewTaskWithNav = useCallback(() => { removeNav(modalManager.closeNewTask); modalManager.closeNewTask(); }, [modalManager.closeNewTask, removeNav]); const closeActivityLogWithNav = useCallback(() => { removeNav(modalManager.closeActivityLog); modalManager.closeActivityLog(); }, [modalManager.closeActivityLog, removeNav]); const closeGitManagerWithNav = useCallback(() => { removeNav(modalManager.closeGitManager); modalManager.closeGitManager(); }, [modalManager.closeGitManager, removeNav]); const closeWorkflowEditorWithNav = useCallback(() => { removeNav(modalManager.closeWorkflowEditor); modalManager.closeWorkflowEditor(); }, [modalManager.closeWorkflowEditor, removeNav]); const closeAgentsWithNav = useCallback(() => { removeNav(modalManager.closeAgents); modalManager.closeAgents(); }, [modalManager.closeAgents, removeNav]); const closeSetupWizardWithNav = useCallback(() => { removeNav(modalManager.closeSetupWizard); modalManager.closeSetupWizard(); }, [modalManager.closeSetupWizard, removeNav]); const handleOpenNewTask = useCallback(() => { modalManager.openNewTask(); }, [modalManager]); const handleOpenGitHubImport = useCallback(() => { modalManager.openGitHubImport(); }, [modalManager]); const openDetailTaskWithNav = useCallback( ( task: Parameters[0], tab?: Parameters[1], ) => { modalManager.openDetailTask(task, tab); const closeFromHistory = () => { modalManager.closeDetailTask(); deepLink.handleDetailClose(); if (detailNavCloseRef.current === closeFromHistory) { detailNavCloseRef.current = null; } }; detailNavCloseRef.current = closeFromHistory; pushNav({ type: "modal", close: closeFromHistory }); }, [deepLink, modalManager, pushNav], ); const openGroupModalWithNav = useCallback((groupId: string) => { modalManager.openGroupModal(groupId); pushNav({ type: "modal", close: modalManager.closeGroupModal }); }, [modalManager, pushNav]); const handleOnboardingViewTask = useCallback((task: Task) => { setFirstCreatedTask(null); modalManager.closeModelOnboarding(); openDetailTaskWithNav(task); }, [modalManager, openDetailTaskWithNav]); const handleModalCreateWithOnboardingTracking = useCallback( async (input: TaskCreateInput): Promise => { const task = await taskHandlers.handleModalCreate(input); if (modalManager.modelOnboardingOpen) { setFirstCreatedTask(task); } return task; }, [taskHandlers.handleModalCreate, modalManager.modelOnboardingOpen], ); useEffect(() => { if (!modalManager.modelOnboardingOpen && firstCreatedTask) { setFirstCreatedTask(null); } }, [modalManager.modelOnboardingOpen, firstCreatedTask]); useEffect(() => { prefetchSettingsModal(); }, []); return ( <> {detailTask && ( modalManager.openWorkflowEditor()} initialTab={modalManager.detailTaskInitialTab} /> )} {modalManager.groupModalGroupId && ( { const memberTask = tasks.find((task) => task.id === taskId); if (memberTask) { openDetailTaskWithNav(memberTask); } }} /> )} {modalManager.settingsOpen && ( { closeSettingsWithNav(); modalManager.openWorkflowEditor("settings"); }} /> )} {modalManager.filesOpen && ( )} {modalManager.schedulesOpen && ( )} { const task = tasks.find((candidate) => candidate.id === taskId); if (task) { openDetailTaskWithNav(task); } }} /> {modalManager.workflowEditorOpen && ( )} {modalManager.setupWizardOpen && ( )} {/* FNXC:Onboarding 2026-06-22-05:06: Brand-new onboarding owns AI/GitHub first, then opens the project setup wizard only as the Project step sub-flow. Hide model onboarding while that project wizard is mounted so users never see both flows at once. */} {modalManager.modelOnboardingOpen && !modalManager.setupWizardOpen && ( )} ); }