import { useCallback, useEffect, 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 { PlanningModeModal } from "./PlanningModeModal"; import { SubtaskBreakdownModal } from "./SubtaskBreakdownModal"; import { TerminalModal } from "./TerminalModal"; import { ScriptsModal } from "./ScriptsModal"; import { FileBrowserModal } from "./FileBrowserModal"; import { TodoModal } from "./TodoModal"; import { UsageIndicator } from "./UsageIndicator"; import { ScheduledTasksModal } from "./ScheduledTasksModal"; import { NewTaskModal } from "./NewTaskModal"; import { SystemStatsModal } from "./SystemStatsModal"; import { ActivityLogModal } from "./ActivityLogModal"; import { GitManagerModal } from "./GitManagerModal"; import { WorkflowStepManager } from "./WorkflowStepManager"; 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 }))); 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; 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; themeMode: ThemeMode; colorTheme: ColorTheme; dashboardFontScalePct: number; setThemeMode: (mode: ThemeMode) => void; setColorTheme: (theme: ColorTheme) => void; setDashboardFontScalePct: (scalePct: number) => 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; } export function AppModals({ projectId, tasks, projects, currentProject, addToast, toasts, removeToast, modalManager, projectActions, taskHandlers, taskOperations, deepLink, settings, onSettingsClose, onReopenOnboarding, onOpenApprovals, }: AppModalsProps) { const { pushNav } = useNavigationHistoryContext(); const [firstCreatedTask, setFirstCreatedTask] = useState(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; 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); pushNav({ type: "modal", close: modalManager.closeDetailTask }); }, [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.groupModalGroupId && ( { const memberTask = tasks.find((task) => task.id === taskId); if (memberTask) { openDetailTaskWithNav(memberTask); } }} /> )} {modalManager.settingsOpen && ( )} {modalManager.filesOpen && ( )} {modalManager.todosOpen && ( )} {modalManager.schedulesOpen && ( )} { const task = tasks.find((candidate) => candidate.id === taskId); if (task) { openDetailTaskWithNav(task); } }} /> {modalManager.setupWizardOpen && ( )} {modalManager.modelOnboardingOpen && ( )} ); }