import { useCallback, useState } from "react"; import type { Task, TaskDetail } from "@fusion/core"; import type { SectionId } from "../components/SettingsModal"; import type { ToastType } from "./useToast"; export type DetailTaskTab = | "definition" | "logs" | "changes" | "comments" | "model" | "workflow" | "retries"; export type DetailTaskOrigin = "list-mobile"; interface UseModalManagerOptions { projectId?: string; planningSessions: Array<{ id: string }>; } /** * State and handler contract for App-level modal/overlay orchestration. */ export interface ModalManager { // State newTaskModalOpen: boolean; isPlanningOpen: boolean; planningInitialPlan: string | null; planningResumeSessionId: string | undefined; isSubtaskOpen: boolean; subtaskInitialDescription: string | null; subtaskResumeSessionId: string | undefined; // Can be Task (optimistic open) or TaskDetail (full data with prompt) detailTask: (Task | TaskDetail) | null; detailTaskInitialTab: DetailTaskTab; detailTaskOrigin: DetailTaskOrigin | null; settingsOpen: boolean; settingsInitialSection: SectionId | undefined; schedulesOpen: boolean; githubImportOpen: boolean; usageOpen: boolean; usageAnchorRect: DOMRect | null; systemStatsOpen: boolean; terminalOpen: boolean; terminalInitialCommand: string | undefined; filesOpen: boolean; todosOpen: boolean; fileBrowserWorkspace: string; fileBrowserInitialFile: string | null; activityLogOpen: boolean; gitManagerOpen: boolean; workflowStepsOpen: boolean; agentsOpen: boolean; scriptsOpen: boolean; setupWizardOpen: boolean; modelOnboardingOpen: boolean; anyModalOpen: boolean; // Handlers openNewTask: () => void; closeNewTask: () => void; openPlanning: () => void; openPlanningWithInitialPlan: (initialPlan: string) => void; resumePlanning: () => void; openPlanningWithSession: (sessionId: string) => void; closePlanning: () => void; openSubtaskBreakdown: (description: string) => void; openSubtaskWithSession: (sessionId: string) => void; closeSubtask: () => void; openDetailTask: ( task: Task | TaskDetail, initialTab?: DetailTaskTab, options?: { origin?: DetailTaskOrigin }, ) => void; openDetailWithChangesTab: (task: Task | TaskDetail) => void; updateDetailTask: (updated: Partial) => void; closeDetailTask: () => void; openSettings: (section?: SectionId) => void; closeSettings: () => void; openSchedules: () => void; closeSchedules: () => void; openGitHubImport: () => void; closeGitHubImport: () => void; openUsage: (anchorRect?: DOMRect | null) => void; closeUsage: () => void; openSystemStats: () => void; closeSystemStats: () => void; toggleTerminal: () => void; closeTerminal: () => void; openFiles: (workspace?: string, initialFile?: string | null) => void; closeFiles: () => void; openTodos: () => void; closeTodos: () => void; setFileWorkspace: (workspace: string) => void; openActivityLog: () => void; closeActivityLog: () => void; openGitManager: () => void; closeGitManager: () => void; openWorkflowSteps: () => void; closeWorkflowSteps: () => void; openAgents: () => void; closeAgents: () => void; openScripts: () => void; closeScripts: () => void; runScript: (name: string, command: string) => Promise; openSetupWizard: () => void; closeSetupWizard: () => void; openModelOnboarding: () => void; closeModelOnboarding: () => void; onPlanningTaskCreated: (task: Task, addToast: (message: string, type?: ToastType) => void) => void; onPlanningTasksCreated: (tasks: Task[], addToast: (message: string, type?: ToastType) => void) => void; onSubtaskTasksCreated: (tasks: Task[], addToast: (message: string, type?: ToastType) => void) => void; } /** * Centralized modal manager for dashboard App-level UI state. * * Encapsulates all modal open/close booleans, related resume/initial payloads, * and cross-modal transitions (for example, script runner -> terminal handoff). */ export function useModalManager(options: UseModalManagerOptions): ModalManager { const { planningSessions } = options; const [newTaskModalOpen, setNewTaskModalOpen] = useState(false); const [isPlanningOpen, setIsPlanningOpen] = useState(false); const [planningInitialPlan, setPlanningInitialPlan] = useState(null); const [planningResumeSessionId, setPlanningResumeSessionId] = useState(undefined); const [isSubtaskOpen, setIsSubtaskOpen] = useState(false); const [subtaskInitialDescription, setSubtaskInitialDescription] = useState(null); const [subtaskResumeSessionId, setSubtaskResumeSessionId] = useState(undefined); // Can be Task (optimistic open) or TaskDetail (full data with prompt) const [detailTask, setDetailTask] = useState<(Task | TaskDetail) | null>(null); const [detailTaskInitialTab, setDetailTaskInitialTab] = useState("definition"); const [detailTaskOrigin, setDetailTaskOrigin] = useState(null); const [settingsOpen, setSettingsOpen] = useState(false); const [settingsInitialSection, setSettingsInitialSection] = useState(undefined); const [schedulesOpen, setSchedulesOpen] = useState(false); const [githubImportOpen, setGitHubImportOpen] = useState(false); const [usageOpen, setUsageOpen] = useState(false); const [usageAnchorRect, setUsageAnchorRect] = useState(null); const [systemStatsOpen, setSystemStatsOpen] = useState(false); const [terminalOpen, setTerminalOpen] = useState(false); const [terminalInitialCommand, setTerminalInitialCommand] = useState(undefined); const [filesOpen, setFilesOpen] = useState(false); const [todosOpen, setTodosOpen] = useState(false); const [fileBrowserWorkspace, setFileBrowserWorkspace] = useState("project"); const [fileBrowserInitialFile, setFileBrowserInitialFile] = useState(null); const [activityLogOpen, setActivityLogOpen] = useState(false); const [gitManagerOpen, setGitManagerOpen] = useState(false); const [workflowStepsOpen, setWorkflowStepsOpen] = useState(false); const [agentsOpen, setAgentsOpen] = useState(false); const [scriptsOpen, setScriptsOpen] = useState(false); const [setupWizardOpen, setSetupWizardOpen] = useState(false); const [modelOnboardingOpen, setModelOnboardingOpen] = useState(false); const anyModalOpen = Boolean( detailTask || settingsOpen || newTaskModalOpen || isPlanningOpen || isSubtaskOpen || terminalOpen || filesOpen || todosOpen || activityLogOpen || gitManagerOpen || workflowStepsOpen || scriptsOpen || agentsOpen || usageOpen || systemStatsOpen || schedulesOpen || githubImportOpen || setupWizardOpen || modelOnboardingOpen, ); const openNewTask = useCallback(() => setNewTaskModalOpen(true), []); const closeNewTask = useCallback(() => setNewTaskModalOpen(false), []); const openPlanning = useCallback(() => setIsPlanningOpen(true), []); const openPlanningWithInitialPlan = useCallback((initialPlan: string) => { setPlanningInitialPlan(initialPlan); setIsPlanningOpen(true); }, []); const resumePlanning = useCallback(() => { const session = planningSessions[0]; if (!session) return; setPlanningResumeSessionId(session.id); setIsPlanningOpen(true); }, [planningSessions]); const openPlanningWithSession = useCallback((sessionId: string) => { setPlanningResumeSessionId(sessionId); setIsPlanningOpen(true); }, []); const closePlanning = useCallback(() => { setIsPlanningOpen(false); setPlanningInitialPlan(null); setPlanningResumeSessionId(undefined); }, []); const openSubtaskBreakdown = useCallback((description: string) => { setSubtaskInitialDescription(description); setIsSubtaskOpen(true); }, []); const openSubtaskWithSession = useCallback((sessionId: string) => { setSubtaskResumeSessionId(sessionId); setIsSubtaskOpen(true); }, []); const closeSubtask = useCallback(() => { setIsSubtaskOpen(false); setSubtaskInitialDescription(null); setSubtaskResumeSessionId(undefined); }, []); const openDetailTask = useCallback(( task: Task | TaskDetail, initialTab: DetailTaskTab = "definition", options?: { origin?: DetailTaskOrigin }, ) => { setDetailTask(task); setDetailTaskInitialTab(initialTab); setDetailTaskOrigin(options?.origin ?? null); }, []); const openDetailWithChangesTab = useCallback((task: Task | TaskDetail) => { setDetailTask(task); setDetailTaskInitialTab("changes"); setDetailTaskOrigin(null); }, []); const updateDetailTask = useCallback((updated: Partial) => { setDetailTask((prev) => { if (!prev) return prev; if (updated.id !== undefined && updated.id !== prev.id) { return prev; } return { ...prev, ...updated }; }); }, []); const closeDetailTask = useCallback(() => { setDetailTask(null); setDetailTaskOrigin(null); }, []); const openSettings = useCallback((section?: SectionId) => { setSettingsInitialSection(section); setSettingsOpen(true); }, []); const closeSettings = useCallback(() => { setSettingsOpen(false); setSettingsInitialSection(undefined); }, []); const openSchedules = useCallback(() => setSchedulesOpen(true), []); const closeSchedules = useCallback(() => setSchedulesOpen(false), []); const openGitHubImport = useCallback(() => setGitHubImportOpen(true), []); const closeGitHubImport = useCallback(() => setGitHubImportOpen(false), []); const openUsage = useCallback((anchorRect?: DOMRect | null) => { setUsageAnchorRect(anchorRect ?? null); setUsageOpen(true); }, []); const closeUsage = useCallback(() => { setUsageOpen(false); setUsageAnchorRect(null); }, []); const openSystemStats = useCallback(() => setSystemStatsOpen(true), []); const closeSystemStats = useCallback(() => setSystemStatsOpen(false), []); const toggleTerminal = useCallback(() => { setTerminalOpen((prev) => !prev); }, []); const closeTerminal = useCallback(() => { setTerminalOpen(false); setTerminalInitialCommand(undefined); }, []); const openFiles = useCallback((workspace?: string, initialFile?: string | null) => { if (typeof workspace === "string" && workspace) { setFileBrowserWorkspace(workspace); } if (typeof initialFile === "string" || initialFile === null) { setFileBrowserInitialFile(initialFile); } else { setFileBrowserInitialFile(null); } setFilesOpen(true); }, []); const closeFiles = useCallback(() => { setFilesOpen(false); setFileBrowserInitialFile(null); }, []); const openTodos = useCallback(() => setTodosOpen(true), []); const closeTodos = useCallback(() => setTodosOpen(false), []); const setFileWorkspace = useCallback((workspace: string) => { if (typeof workspace === "string" && workspace) { setFileBrowserWorkspace(workspace); } }, []); const openActivityLog = useCallback(() => setActivityLogOpen(true), []); const closeActivityLog = useCallback(() => setActivityLogOpen(false), []); const openGitManager = useCallback(() => setGitManagerOpen(true), []); const closeGitManager = useCallback(() => setGitManagerOpen(false), []); const openWorkflowSteps = useCallback(() => setWorkflowStepsOpen(true), []); const closeWorkflowSteps = useCallback(() => setWorkflowStepsOpen(false), []); const openAgents = useCallback(() => setAgentsOpen(true), []); const closeAgents = useCallback(() => setAgentsOpen(false), []); const openScripts = useCallback(() => setScriptsOpen(true), []); const closeScripts = useCallback(() => setScriptsOpen(false), []); const runScript = useCallback(async (_name: string, command: string) => { setScriptsOpen(false); setTerminalInitialCommand(command); setTerminalOpen(true); }, []); const openSetupWizard = useCallback(() => setSetupWizardOpen(true), []); const closeSetupWizard = useCallback(() => setSetupWizardOpen(false), []); const openModelOnboarding = useCallback(() => setModelOnboardingOpen(true), []); const closeModelOnboarding = useCallback(() => setModelOnboardingOpen(false), []); const onPlanningTaskCreated = useCallback((task: Task, addToast: (message: string, type?: ToastType) => void) => { addToast(`Created ${task.id} from planning mode`, "success"); setIsPlanningOpen(false); setPlanningInitialPlan(null); }, []); const onPlanningTasksCreated = useCallback((tasks: Task[], addToast: (message: string, type?: ToastType) => void) => { const ids = tasks.map((task) => task.id).join(", "); addToast(`Created ${ids} from planning mode`, "success"); setIsPlanningOpen(false); setPlanningInitialPlan(null); }, []); const onSubtaskTasksCreated = useCallback((tasks: Task[], addToast: (message: string, type?: ToastType) => void) => { const ids = tasks.map((task) => task.id).join(", "); addToast(`Created ${ids} from subtask breakdown`, "success"); setIsSubtaskOpen(false); setSubtaskInitialDescription(null); }, []); return { newTaskModalOpen, isPlanningOpen, planningInitialPlan, planningResumeSessionId, isSubtaskOpen, subtaskInitialDescription, subtaskResumeSessionId, detailTask, detailTaskInitialTab, detailTaskOrigin, settingsOpen, settingsInitialSection, schedulesOpen, githubImportOpen, usageOpen, usageAnchorRect, systemStatsOpen, terminalOpen, terminalInitialCommand, filesOpen, todosOpen, fileBrowserWorkspace, fileBrowserInitialFile, activityLogOpen, gitManagerOpen, workflowStepsOpen, agentsOpen, scriptsOpen, setupWizardOpen, modelOnboardingOpen, anyModalOpen, openNewTask, closeNewTask, openPlanning, openPlanningWithInitialPlan, resumePlanning, openPlanningWithSession, closePlanning, openSubtaskBreakdown, openSubtaskWithSession, closeSubtask, openDetailTask, openDetailWithChangesTab, updateDetailTask, closeDetailTask, openSettings, closeSettings, openSchedules, closeSchedules, openGitHubImport, closeGitHubImport, openUsage, closeUsage, openSystemStats, closeSystemStats, toggleTerminal, closeTerminal, openFiles, closeFiles, openTodos, closeTodos, setFileWorkspace, openActivityLog, closeActivityLog, openGitManager, closeGitManager, openWorkflowSteps, closeWorkflowSteps, openAgents, closeAgents, openScripts, closeScripts, runScript, openSetupWizard, closeSetupWizard, openModelOnboarding, closeModelOnboarding, onPlanningTaskCreated, onPlanningTasksCreated, onSubtaskTasksCreated, }; }