import { useState, useCallback, useEffect, useMemo, useRef } from "react"; import type { Task, TaskDetail } from "@fusion/core"; import { Header, useViewportMode } from "./components/Header"; import { Board } from "./components/Board"; import { ListView } from "./components/ListView"; import { ProjectOverview } from "./components/ProjectOverview"; import { AgentsView } from "./components/AgentsView"; import { DocumentsView } from "./components/DocumentsView"; import { InsightsView } from "./components/InsightsView"; import { MissionManager } from "./components/MissionManager"; import { NodesView } from "./components/NodesView"; import { ChatView } from "./components/ChatView"; import { RoadmapsView } from "./components/RoadmapsView"; import { SkillsView } from "./components/SkillsView"; import { MailboxView } from "./components/MailboxView"; import { MemoryView } from "./components/MemoryView"; import { DevServerView } from "./components/DevServerView"; import { PageErrorBoundary } from "./components/ErrorBoundary"; import { AppModals } from "./components/AppModals"; import { DashboardLoader, type DashboardLoaderStage } from "./components/DashboardLoader"; import { ExecutorStatusBar } from "./components/ExecutorStatusBar"; import { SessionNotificationBanner } from "./components/SessionNotificationBanner"; import { SetupWarningBanner } from "./components/SetupWarningBanner"; import { OnboardingResumeCard } from "./components/OnboardingResumeCard"; import { PostOnboardingRecommendations } from "./components/PostOnboardingRecommendations"; import { isOnboardingCompleted, isOnboardingResumable, isPostOnboardingDismissed, } from "./components/model-onboarding-state"; import type { SectionId } from "./components/SettingsModal"; import { MobileNavBar } from "./components/MobileNavBar"; import { QuickChatFAB } from "./components/QuickChatFAB"; import { ToastContainer } from "./components/ToastContainer"; import { useBackgroundSessions } from "./hooks/useBackgroundSessions"; import { useTasks } from "./hooks/useTasks"; import { useProjects } from "./hooks/useProjects"; import { useNodes } from "./hooks/useNodes"; import { useCurrentProject } from "./hooks/useCurrentProject"; import { ToastProvider, useToast } from "./hooks/useToast"; import { useTheme } from "./hooks/useTheme"; import { useModalManager } from "./hooks/useModalManager"; import { useAppSettings } from "./hooks/useAppSettings"; import { useDeepLink } from "./hooks/useDeepLink"; import { useFavorites } from "./hooks/useFavorites"; import { useAuthOnboarding } from "./hooks/useAuthOnboarding"; import { useSetupReadiness } from "./hooks/useSetupReadiness"; import { useViewState, type TaskView } from "./hooks/useViewState"; import { useProjectActions } from "./hooks/useProjectActions"; import { useTaskHandlers } from "./hooks/useTaskHandlers"; import { useRemoteNodeData } from "./hooks/useRemoteNodeData"; import { useRemoteNodeEvents } from "./hooks/useRemoteNodeEvents"; import { NodeProvider, useNodeContext } from "./context/NodeContext"; import type { AiSessionSummary } from "./api"; import { fetchAiSession, fetchUnreadCount, reportDashboardPerf } from "./api"; import { getScopedItem, setScopedItem } from "./utils/projectStorage"; const SETUP_WARNING_DISMISSED_KEY = "kb-setup-warning-dismissed"; function AppInner() { const { toasts, addToast, removeToast } = useToast(); const isElectron = typeof window !== "undefined" && Boolean((window as Window & { electronAPI?: unknown }).electronAPI); // Project management hooks - MUST be called before any conditional logic const { projects, loading: projectsLoading, error: projectsError, refresh: refreshProjects, register: registerProject, update: updateProjectHook, unregister: unregisterProjectHook } = useProjects(); const { nodes } = useNodes(); // Node context for local/remote node switching - must be called before useCurrentProject const { currentNode, currentNodeId, isRemote, setCurrentNode, clearCurrentNode } = useNodeContext(); // Current project with node-aware persistence const { currentProject, setCurrentProject, clearCurrentProject, loading: currentProjectLoading } = useCurrentProject(projects, { nodeId: currentNodeId }); const { hasAiProvider, hasGithub, loading: setupReadinessLoading, hasWarnings, } = useSetupReadiness(currentProject?.id); // Sync node context with useNodes() results: // - Resolve saved node ID to full NodeConfig when nodes list loads // - Fall back to local if selected node is missing or deleted useEffect(() => { // If we have a saved node ID but no currentNode yet (initial hydration), // resolve it from the nodes list if (currentNodeId && !currentNode && nodes.length > 0) { const foundNode = nodes.find((n) => n.id === currentNodeId); if (foundNode) { setCurrentNode(foundNode); return; } } // If we have a currentNode but the saved ID no longer exists in nodes list, // fall back to local view if (currentNodeId && nodes.length > 0) { const nodeExists = nodes.some((n) => n.id === currentNodeId); if (!nodeExists) { // Selected node was deleted or unregistered - fall back to local clearCurrentNode(); } } }, [currentNodeId, currentNode, nodes, setCurrentNode, clearCurrentNode]); // Search query state - must be defined before useTasks const [searchQuery, setSearchQuery] = useState(""); // Remote node data and events when in remote mode (pass searchQuery for server-side filtering) const remoteData = useRemoteNodeData(currentNodeId, { projectId: currentProject?.id, searchQuery: searchQuery || undefined }); const remoteEvents = useRemoteNodeEvents(currentNodeId); // Use remote data when in remote mode, local data otherwise const effectiveProjects = isRemote && remoteData.projects.length > 0 ? remoteData.projects : projects; const effectiveTasks = isRemote && remoteData.tasks.length > 0 ? remoteData.tasks : []; // Theme management - required before useViewState const { themeMode, colorTheme, setThemeMode, setColorTheme } = useTheme(); // Background AI sessions - required before useModalManager const { sessions: bgSessions, generating: bgGenerating, needsInput: bgNeedsInput, planningSessions: bgPlanningSessions, dismissSession: bgDismiss } = useBackgroundSessions(currentProject?.id); const sessionsNeedingInput = bgSessions.filter( (session) => session.status === "awaiting_input" || session.status === "error" ); // Modal state/handlers - required before useViewState const modalManager = useModalManager({ projectId: currentProject?.id, planningSessions: bgPlanningSessions, }); // View state must be defined before useTasks since useTasks depends on taskView for SSE gating const { viewMode, setViewMode, taskView, handleChangeTaskView, handleToggleTheme } = useViewState({ projectsLoading, currentProjectLoading, currentProject, projectsLength: projects.length, setupWizardOpen: modalManager.setupWizardOpen, openSetupWizard: modalManager.openSetupWizard, themeMode, setThemeMode, }); const handleTaskViewChange = useCallback((newView: TaskView) => { if (newView === "missions") { setMissionResumeSessionId(undefined); setMissionTargetId(undefined); setMilestoneSliceResumeSessionId(undefined); } handleChangeTaskView(newView); }, [handleChangeTaskView]); // Tasks hook with project context and search query // SSE is only enabled for board/list views to free connection slots for mission detail fetches const taskSseEnabled = taskView === "board" || taskView === "list"; const { tasks, createTask, moveTask, deleteTask, mergeTask, retryTask, updateTask, duplicateTask, archiveTask, unarchiveTask, archiveAllDone, loadArchivedTasks, lastFetchTimeMs } = useTasks( { ...(currentProject ? { projectId: currentProject.id } : {}), searchQuery: searchQuery || undefined, sseEnabled: taskSseEnabled, } ); const [initialLoadComplete, setInitialLoadComplete] = useState(false); const mountTimeRef = useRef(performance.now()); const projectsReadyLoggedRef = useRef(false); const projectReadyLoggedRef = useRef(false); const loadingStage = useMemo(() => { if (projectsLoading) return "projects"; if (currentProjectLoading) return "project"; return "tasks"; }, [projectsLoading, currentProjectLoading]); useEffect(() => { if (!projectsLoading && !projectsReadyLoggedRef.current) { projectsReadyLoggedRef.current = true; const msg = `projects loaded at ${Math.round(performance.now() - mountTimeRef.current)}ms from mount`; console.log(`[App] ${msg}`); reportDashboardPerf("[App]", msg); } if (!currentProjectLoading && !projectReadyLoggedRef.current) { projectReadyLoggedRef.current = true; const msg = `current-project resolved at ${Math.round(performance.now() - mountTimeRef.current)}ms from mount`; console.log(`[App] ${msg}`); reportDashboardPerf("[App]", msg); } }, [projectsLoading, currentProjectLoading]); useEffect(() => { if (initialLoadComplete) { return; } if (projectsLoading || currentProjectLoading) { return; } const settleStart = performance.now(); const settleTimer = window.setTimeout(() => { const msg = `dashboard ready at ${Math.round(performance.now() - mountTimeRef.current)}ms from mount (settle delay=${Math.round(performance.now() - settleStart)}ms)`; console.log(`[App] ${msg}`); reportDashboardPerf("[App]", msg); setInitialLoadComplete(true); }, 200); return () => { window.clearTimeout(settleTimer); }; }, [initialLoadComplete, projectsLoading, currentProjectLoading]); const viewportMode = useViewportMode(); const isMobile = viewportMode === "mobile"; // App-level mailbox unread count state (used for header badge) const [mailboxUnreadCount, setMailboxUnreadCount] = useState(0); // Initial fetch of mailbox unread count useEffect(() => { fetchUnreadCount(currentProject?.id) .then((data: { unreadCount: number }) => { setMailboxUnreadCount(data.unreadCount); }) .catch((err) => { console.warn("[App] Failed to fetch mailbox unread count:", err); }); }, [currentProject?.id]); // Nodes management is an overlay view (not a modal), so it stays local to App. const [nodesOpen, setNodesOpen] = useState(false); const [missionResumeSessionId, setMissionResumeSessionId] = useState(undefined); const [missionTargetId, setMissionTargetId] = useState(undefined); const [milestoneSliceResumeSessionId, setMilestoneSliceResumeSessionId] = useState(undefined); const [quickChatOpen, setQuickChatOpen] = useState(false); const [setupWarningDismissed, setSetupWarningDismissed] = useState( () => getScopedItem(SETUP_WARNING_DISMISSED_KEY, currentProject?.id) === "true", ); useEffect(() => { setSetupWarningDismissed( getScopedItem(SETUP_WARNING_DISMISSED_KEY, currentProject?.id) === "true", ); }, [currentProject?.id]); const handleDismissSetupWarning = useCallback(() => { setScopedItem(SETUP_WARNING_DISMISSED_KEY, "true", currentProject?.id); setSetupWarningDismissed(true); }, [currentProject?.id]); // Settings state const { maxConcurrent, rootDir, autoMerge, globalPaused, enginePaused, taskStuckTimeoutMs, showQuickChatFAB, githubTokenConfigured, experimentalFeatures, insightsEnabled, roadmapEnabled, memoryEnabled, devServerEnabled, toggleAutoMerge, toggleGlobalPause, toggleEnginePause, refresh: refreshAppSettings, } = useAppSettings(currentProject?.id); const skillsEnabled = experimentalFeatures.skillsView === true; const nodesEnabled = experimentalFeatures.nodesView === true; const agentsEnabled = true; // Redirect to board if feature-gated views are disabled. // Only run after settings have been loaded (experimentalFeatures is non-empty) useEffect(() => { if (Object.keys(experimentalFeatures).length === 0) return; if (taskView === "insights" && !insightsEnabled) { handleChangeTaskView("board"); } if (taskView === "roadmaps" && !roadmapEnabled) { handleChangeTaskView("board"); } if (taskView === "agents" && !agentsEnabled) { handleChangeTaskView("board"); } if (taskView === "memory" && !memoryEnabled) { handleChangeTaskView("board"); } if ((taskView === "devserver" || taskView === "dev-server") && !devServerEnabled) { handleChangeTaskView("board"); } }, [taskView, insightsEnabled, roadmapEnabled, experimentalFeatures, handleChangeTaskView, agentsEnabled, memoryEnabled, devServerEnabled]); // Auto-close nodes overlay if feature flag is toggled off while overlay is open useEffect(() => { if (nodesOpen && !nodesEnabled) { setNodesOpen(false); } }, [nodesOpen, nodesEnabled]); const { availableModels, favoriteProviders, favoriteModels, toggleFavoriteProvider, toggleFavoriteModel, } = useFavorites(); // Auth and onboarding bootstrap logic extracted to a dedicated hook. useAuthOnboarding({ projectId: currentProject?.id, openModelOnboarding: modalManager.openModelOnboarding, openSettings: modalManager.openSettings, }); const { handleSelectProject, handleViewAllProjects, handleOpenSettings, handleAddProject, handleSetupComplete, handleModelOnboardingComplete, handlePauseProject, handleResumeProject, handleRemoveProject, handleToggleFavorite, handleToggleModelFavorite, } = useProjectActions({ setCurrentProject, clearCurrentProject, setViewMode, currentProject, refreshProjects, toggleFavoriteProvider, toggleFavoriteModel, addToast, openSettings: modalManager.openSettings, openSetupWizard: modalManager.openSetupWizard, closeSetupWizard: modalManager.closeSetupWizard, closeModelOnboarding: modalManager.closeModelOnboarding, }); const { handleDetailClose } = useDeepLink({ projectId: currentProject?.id, projects, projectsLoading, currentProject, setCurrentProject, addToast, openTaskDetail: modalManager.openDetailTask, closeTaskDetail: modalManager.closeDetailTask, }); // Task handlers const { handleBoardQuickCreate, handleModalCreate, handlePlanningTaskCreated, handlePlanningTasksCreated, handleSubtaskTasksCreated, handleGitHubImport, } = useTaskHandlers({ createTask, onPlanningTaskCreated: modalManager.onPlanningTaskCreated, onPlanningTasksCreated: modalManager.onPlanningTasksCreated, onSubtaskTasksCreated: modalManager.onSubtaskTasksCreated, addToast, }); const handleOpenDetailWithTab = useCallback((task: Task | TaskDetail, initialTab: "changes") => { if (initialTab === "changes") { modalManager.openDetailWithChangesTab(task); return; } modalManager.openDetailTask(task, initialTab); }, [modalManager]); const handleOpenNodes = useCallback(() => { if (!nodesEnabled) return; setNodesOpen((prev) => !prev); }, [nodesEnabled]); const handleOpenMissionsView = useCallback(() => { setMissionTargetId(undefined); setMissionResumeSessionId(undefined); handleChangeTaskView("missions"); }, [handleChangeTaskView]); const handleOpenMission = useCallback((missionId: string) => { setMissionTargetId(missionId); setMissionResumeSessionId(undefined); handleChangeTaskView("missions"); }, [handleChangeTaskView]); const handleOpenBackgroundSession = useCallback((session: AiSessionSummary) => { if (session.type === "planning") { modalManager.openPlanningWithSession(session.id); } else if (session.type === "subtask") { modalManager.openSubtaskWithSession(session.id); } else if (session.type === "mission_interview") { setMissionTargetId(undefined); setMissionResumeSessionId(session.id); setMilestoneSliceResumeSessionId(undefined); handleChangeTaskView("missions"); } else if (session.type === "milestone_interview" || session.type === "slice_interview") { // For milestone/slice interviews, we need to fetch the session to get the target ID // Then navigate to missions view with the resume session ID setMissionResumeSessionId(undefined); setMissionTargetId(undefined); setMilestoneSliceResumeSessionId(session.id); handleChangeTaskView("missions"); } }, [handleChangeTaskView, modalManager]); const handleDismissAllNeedingInputSessions = useCallback(() => { for (const session of sessionsNeedingInput) { bgDismiss(session.id); } }, [bgDismiss, sessionsNeedingInput]); // Render main content based on view mode const renderMainContent = () => { if (nodesOpen) { return (
setNodesOpen(false)} />
); } if (viewMode === "overview") { return ( ); } // Project view if (taskView === "skills") { if (!skillsEnabled) { // Redirect to board if skills view is not enabled handleChangeTaskView("board"); return null; } return ( handleChangeTaskView("board")} /> ); } if (taskView === "chat") { return ( ); } if (taskView === "mailbox") { return ( ); } if (taskView === "roadmaps") { return ( ); } if (taskView === "missions") { return ( { setMissionTargetId(undefined); setMissionResumeSessionId(undefined); setMilestoneSliceResumeSessionId(undefined); handleChangeTaskView("board"); }} addToast={addToast} projectId={currentProject?.id} onSelectTask={(taskId) => { const task = tasks.find((t) => t.id === taskId); if (task) modalManager.openDetailTask(task as TaskDetail); }} availableTasks={tasks.map((t) => ({ id: t.id, title: t.title }))} resumeSessionId={missionResumeSessionId} targetMissionId={missionTargetId} milestoneSliceResumeSessionId={milestoneSliceResumeSessionId} onMilestoneSliceResumeFetchError={() => setMilestoneSliceResumeSessionId(undefined)} /> ); } if (taskView === "agents" && agentsEnabled) { return ( ); } if (taskView === "documents") { return ( ); } if (taskView === "insights") { return ( handleChangeTaskView("board")} /> ); } if (taskView === "memory") { return ( ); } if (taskView === "devserver" || taskView === "dev-server") { return ( ); } if (taskView === "board") { return ( 0 ? remoteData.tasks : tasks} projectId={currentProject?.id} maxConcurrent={maxConcurrent} onMoveTask={moveTask} onOpenDetail={modalManager.openDetailTask} addToast={addToast} onQuickCreate={handleBoardQuickCreate} onNewTask={modalManager.openNewTask} onPlanningMode={modalManager.openPlanningWithInitialPlan} onSubtaskBreakdown={modalManager.openSubtaskBreakdown} autoMerge={autoMerge} onToggleAutoMerge={toggleAutoMerge} globalPaused={globalPaused} onUpdateTask={updateTask} onArchiveTask={archiveTask} onUnarchiveTask={unarchiveTask} onDeleteTask={deleteTask} onArchiveAllDone={archiveAllDone} onLoadArchivedTasks={loadArchivedTasks} searchQuery={searchQuery} availableModels={availableModels} onOpenDetailWithTab={handleOpenDetailWithTab} favoriteProviders={favoriteProviders} favoriteModels={favoriteModels} onToggleFavorite={handleToggleFavorite} onToggleModelFavorite={handleToggleModelFavorite} taskStuckTimeoutMs={taskStuckTimeoutMs} onOpenMission={handleOpenMission} lastFetchTimeMs={lastFetchTimeMs} /> ); } // List view return ( 0 ? remoteData.tasks : tasks} projectId={currentProject?.id} onMoveTask={moveTask} onOpenDetail={modalManager.openDetailTask} addToast={addToast} globalPaused={globalPaused} onNewTask={modalManager.openNewTask} onQuickCreate={handleBoardQuickCreate} onPlanningMode={modalManager.openPlanningWithInitialPlan} onSubtaskBreakdown={modalManager.openSubtaskBreakdown} availableModels={availableModels} favoriteProviders={favoriteProviders} favoriteModels={favoriteModels} onToggleFavorite={handleToggleFavorite} onToggleModelFavorite={handleToggleModelFavorite} taskStuckTimeoutMs={taskStuckTimeoutMs} searchQuery={searchQuery} lastFetchTimeMs={lastFetchTimeMs} /> ); }; if (!initialLoadComplete) { return ( <> ); } const showOnboardingResumeCard = !modalManager.modelOnboardingOpen && isOnboardingResumable(); const showPostOnboardingRecommendations = !modalManager.modelOnboardingOpen && !showOnboardingResumeCard && isOnboardingCompleted() && !isPostOnboardingDismissed(); return ( <>
handleTaskViewChange("mailbox")} mailboxUnreadCount={mailboxUnreadCount} onOpenSchedules={modalManager.openSchedules} onOpenGitManager={modalManager.openGitManager} onOpenNodes={handleOpenNodes} showNodesButton={nodesEnabled} onOpenWorkflowSteps={modalManager.openWorkflowSteps} onOpenScripts={modalManager.openScripts} onRunScript={modalManager.runScript} onToggleTerminal={modalManager.toggleTerminal} onOpenFiles={modalManager.openFiles} filesOpen={modalManager.filesOpen} globalPaused={globalPaused} enginePaused={enginePaused} onToggleGlobalPause={toggleGlobalPause} onToggleEnginePause={toggleEnginePause} view={taskView} onChangeView={viewMode === "project" && currentProject ? handleTaskViewChange : undefined} showSkillsTab={skillsEnabled} showAgentsTab={agentsEnabled} searchQuery={searchQuery} onSearchChange={setSearchQuery} projects={effectiveProjects} currentProject={currentProject} onSelectProject={handleSelectProject} onViewAllProjects={handleViewAllProjects} projectId={currentProject?.id} mobileNavEnabled={isMobile} // Node switching props availableNodes={nodes} currentNode={currentNode} onSelectNode={(node) => { if (node === null) { clearCurrentNode(); } else { setCurrentNode(node); } }} isRemote={isRemote} experimentalFeatures={{ insights: insightsEnabled, roadmap: roadmapEnabled, memoryView: memoryEnabled, devServer: devServerEnabled, devServerView: devServerEnabled, }} /> {viewMode === "project" && currentProject && !nodesOpen && taskView !== "missions" && !modalManager.isPlanningOpen && ( )} {viewMode === "project" && currentProject && showOnboardingResumeCard && ( )} {viewMode === "project" && currentProject && showPostOnboardingRecommendations && ( modalManager.openSettings(section as SectionId)} /> )} {viewMode === "project" && currentProject && !setupReadinessLoading && hasWarnings && !setupWarningDismissed && ( )}
{renderMainContent()}
{viewMode === "project" && currentProject && !nodesOpen && ( 0 ? remoteData.tasks : tasks} projectId={currentProject.id} taskStuckTimeoutMs={taskStuckTimeoutMs} backgroundSessions={bgSessions} backgroundGenerating={bgGenerating} backgroundNeedsInput={bgNeedsInput} onOpenBackgroundSession={handleOpenBackgroundSession} onDismissBackgroundSession={bgDismiss} lastFetchTimeMs={lastFetchTimeMs} /> )} {}} footerVisible={viewMode === "project" && !!currentProject} modalOpen={modalManager.anyModalOpen} onOpenSettings={handleOpenSettings} onOpenActivityLog={modalManager.openActivityLog} onOpenMailbox={() => handleTaskViewChange("mailbox")} mailboxUnreadCount={mailboxUnreadCount} onOpenGitManager={modalManager.openGitManager} onOpenWorkflowSteps={modalManager.openWorkflowSteps} onOpenSchedules={modalManager.openSchedules} onOpenScripts={modalManager.openScripts} onToggleTerminal={modalManager.toggleTerminal} onOpenFiles={modalManager.openFiles} onOpenGitHubImport={modalManager.openGitHubImport} onOpenPlanning={modalManager.openPlanning} onResumePlanning={modalManager.resumePlanning} activePlanningSessionCount={bgPlanningSessions.length} onOpenUsage={modalManager.openUsage} onViewAllProjects={handleViewAllProjects} onRunScript={modalManager.runScript} onOpenQuickChat={() => setQuickChatOpen(true)} projectId={currentProject?.id} showSkillsTab={skillsEnabled} experimentalFeatures={{ insights: insightsEnabled, roadmap: roadmapEnabled, memoryView: memoryEnabled, devServer: devServerEnabled, devServerView: devServerEnabled, }} /> {viewMode === "project" && currentProject && taskView !== "chat" && taskView !== "mailbox" && taskView !== "insights" && taskView !== "devserver" && taskView !== "dev-server" && ( )} { modalManager.closeSettings(); void refreshAppSettings(); }} onReopenOnboarding={() => { modalManager.closeSettings(); modalManager.openModelOnboarding(); }} /> ); } export function App() { return ( ); }