import { useState, useEffect, useRef, useCallback, useMemo, type KeyboardEvent as ReactKeyboardEvent } from "react"; import { Settings, Pause, Play, Square, LayoutGrid, List, Terminal, Lightbulb, Search, X, Activity, MoreHorizontal, Clock, Folder, History, GitBranch, Monitor, Server, Workflow, Bot, Target, ChevronRight, FileCode, Loader2, Grid3X3, Mail, MessageSquare, ChevronDown, Check, Zap, Sparkles, FileText, Brain, CheckSquare } from "lucide-react"; import "./Header.css"; // Header renders an inline ProjectSelector dropdown using project-selector-* classes. import "./ProjectSelector.css"; import type { ProjectInfo } from "../api"; import type { NodeConfig, ProjectStatus } from "@fusion/core"; import { fetchScripts } from "../api"; import { NodeStatusIndicator } from "./NodeStatusIndicator"; import { NodeHealthDot } from "./NodeHealthDot"; import { PluginSlot } from "./PluginSlot"; import { useViewportMode, type ViewportMode } from "../hooks/useViewportMode"; import { getTrailingPath } from "../utils/pathDisplay"; import type { TaskView } from "../hooks/useViewState"; import type { PluginDashboardViewEntry } from "../api"; import { buildPluginTaskViewId } from "../plugins/pluginViewRegistry"; import { getPluginNavIcon } from "./pluginNavIcon"; export { useViewportMode }; // Status icon config for project selector dropdown const PROJECT_STATUS_CONFIG: Record = { active: { color: "var(--success)" }, paused: { color: "var(--warning)" }, errored: { color: "var(--color-error)" }, initializing: { color: "var(--info)" }, }; /** * ProjectSelector - A component for project navigation. * Shows project dropdown for switching projects and navigating to project management. */ function ProjectSelector({ projects, currentProject, onViewAll, onSelectProject, }: { projects: ProjectInfo[]; currentProject: ProjectInfo | null; onViewAll: () => void; onSelectProject?: (project: ProjectInfo) => void; }) { const [isOpen, setIsOpen] = useState(false); const dropdownRef = useRef(null); // Close dropdown on outside click useEffect(() => { if (!isOpen) return; const handleClickOutside = (e: MouseEvent) => { if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) { setIsOpen(false); } }; document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, [isOpen]); // Close on Escape useEffect(() => { if (!isOpen) return; const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape") { setIsOpen(false); } }; document.addEventListener("keydown", handleKeyDown); return () => document.removeEventListener("keydown", handleKeyDown); }, [isOpen]); const handleSelectProject = useCallback( (project: ProjectInfo) => { onSelectProject?.(project); setIsOpen(false); }, [onSelectProject] ); return (
{projects.length > 0 && ( <> {isOpen && (
{projects.map((project) => { const isCurrent = currentProject?.id === project.id; const statusColor = PROJECT_STATUS_CONFIG[project.status]?.color; return ( ); })}
)} )}
); } // GitHub logo icon (Octocat mark) - uses currentColor for theme compatibility function GitHubLogo({ size = 16 }: { size?: number }) { return ( ); } interface DropdownPosition { top: number; left: number; width: number; } export interface HeaderProps { onOpenSettings?: () => void; onOpenGitHubImport?: () => void; onOpenPlanning?: () => void; /** Resume an in-flight planning session. Takes priority over onOpenPlanning when activePlanningSessionCount > 0 */ onResumePlanning?: () => void; /** Number of active planning sessions. When > 0, shows a badge on the Planning button. */ activePlanningSessionCount?: number; onOpenUsage?: (anchorRect?: DOMRect | null) => void; onOpenActivityLog?: () => void; onOpenSystemStats?: () => void; /** Opens the mailbox view */ onOpenMailbox?: () => void; /** Unread message count for badge display */ mailboxUnreadCount?: number; onOpenSchedules?: () => void; onOpenGitManager?: () => void; onOpenNodes?: () => void; /** When false, hides the Nodes management button. Defaults to true for backward compat. */ showNodesButton?: boolean; onOpenWorkflowSteps?: () => void; onOpenScripts?: () => void; onRunScript?: (name: string, command: string) => void; onToggleTerminal?: () => void; /** Opens the top-level workspace-aware file browser modal. */ onOpenFiles?: () => void; filesOpen?: boolean; onOpenTodos?: () => void; todosOpen?: boolean; todosEnabled?: boolean; globalPaused?: boolean; enginePaused?: boolean; onToggleGlobalPause?: () => void; onToggleEnginePause?: () => void; view?: TaskView; onChangeView?: (view: TaskView) => void; /** Whether to show the skills tab in the view toggle */ showSkillsTab?: boolean; /** When true, shows the Agents view tab button. Hidden by default (experimental feature). */ showAgentsTab?: boolean; searchQuery?: string; onSearchChange?: (query: string) => void; /** Multi-project props */ projects?: ProjectInfo[]; currentProject?: ProjectInfo | null; onSelectProject?: (project: ProjectInfo) => void; onViewAllProjects?: () => void; projectId?: string; isElectron?: boolean; /** When true, the mobile bottom nav bar handles primary navigation and header nav controls are hidden. */ mobileNavEnabled?: boolean; /** Available nodes for the node selector */ availableNodes?: NodeConfig[]; /** Currently selected node (null for local) */ currentNode?: NodeConfig | null; /** Callback when a node is selected */ onSelectNode?: (node: NodeConfig | null) => void; /** Whether the current view is a remote node */ isRemote?: boolean; /** Experimental feature flags controlling visibility of nav items. */ experimentalFeatures?: { insights?: boolean; roadmap?: boolean; memoryView?: boolean; devServer?: boolean; devServerView?: boolean; researchView?: boolean }; pluginDashboardViews?: PluginDashboardViewEntry[]; } export function Header({ onOpenSettings, onOpenGitHubImport, onOpenPlanning, onResumePlanning, activePlanningSessionCount = 0, onOpenUsage, onOpenActivityLog, onOpenSystemStats, onOpenMailbox, mailboxUnreadCount = 0, onOpenSchedules, onOpenGitManager, onOpenNodes, showNodesButton, onOpenWorkflowSteps, onOpenScripts, onRunScript, onToggleTerminal, onOpenFiles, filesOpen, onOpenTodos, todosOpen, todosEnabled, globalPaused, enginePaused, onToggleGlobalPause, onToggleEnginePause, view = "board", onChangeView, showSkillsTab, showAgentsTab, searchQuery = "", onSearchChange, projects = [], currentProject, onSelectProject, onViewAllProjects, projectId, isElectron = false, mobileNavEnabled, availableNodes = [], currentNode, onSelectNode, isRemote = false, experimentalFeatures, pluginDashboardViews = [], }: HeaderProps) { const mode: ViewportMode = useViewportMode(); const isMobile = mode === "mobile"; const isTablet = mode === "tablet"; const isCompact = isMobile || isTablet; const hideFullNav = isMobile && mobileNavEnabled; const [isMobileSearchOpen, setIsMobileSearchOpen] = useState(false); const [isNonMobileSearchOpen, setIsNonMobileSearchOpen] = useState(false); // Track when user has explicitly closed the search (used for toggle visibility) const [isNonMobileSearchExplicitlyClosed, setIsNonMobileSearchExplicitlyClosed] = useState(false); const [isOverflowMenuOpen, setIsOverflowMenuOpen] = useState(false); const [isTerminalSubmenuOpen, setIsTerminalSubmenuOpen] = useState(false); const [isNodeSelectorOpen, setIsNodeSelectorOpen] = useState(false); const [isMobileProjectSwitchOpen, setIsMobileProjectSwitchOpen] = useState(false); const [isViewOverflowOpen, setIsViewOverflowOpen] = useState(false); const [isDesktopOverflowOpen, setIsDesktopOverflowOpen] = useState(false); const [isEngineMenuOpen, setIsEngineMenuOpen] = useState(false); const [isScriptsOpen, setIsScriptsOpen] = useState(false); const [scripts, setScripts] = useState>({}); const [scriptsLoading, setScriptsLoading] = useState(false); const [highlightedScriptIndex, setHighlightedScriptIndex] = useState(-1); const [scriptsDropdownPosition, setScriptsDropdownPosition] = useState(null); const [overflowScripts, setOverflowScripts] = useState>({}); const [overflowScriptsLoading, setOverflowScriptsLoading] = useState(false); const overflowButtonRef = useRef(null); const overflowMenuRef = useRef(null); const desktopOverflowTriggerRef = useRef(null); const desktopOverflowRef = useRef(null); const mobileSearchRef = useRef(null); const mobileSearchInputRef = useRef(null); const terminalSubmenuOpenRef = useRef(false); const nodeSelectorRef = useRef(null); const mobileProjectSwitchRef = useRef(null); const viewOverflowRef = useRef(null); const viewOverflowTriggerRef = useRef(null); const engineMenuRef = useRef(null); const scriptsSplitButtonRef = useRef(null); const scriptsChevronButtonRef = useRef(null); const scriptsMenuRef = useRef(null); const scriptsOpenRef = useRef(false); // Get remote nodes only (exclude local node type) const remoteNodes = useMemo(() => availableNodes.filter((node) => node.type === "remote"), [availableNodes] ); const showNodeSelector = remoteNodes.length > 0; // Script entries sorted alphabetically for desktop scripts dropdown const scriptEntries = useMemo(() => { return Object.entries(scripts).sort(([a], [b]) => a.localeCompare(b)); }, [scripts]); const showScriptsFooter = scriptEntries.length > 0; const totalScriptItems = scriptEntries.length + (showScriptsFooter ? 1 : 0); // Script entries sorted alphabetically for overflow submenu const overflowScriptEntries = useMemo(() => { return Object.entries(overflowScripts).sort(([a], [b]) => a.localeCompare(b)); }, [overflowScripts]); const hasRoadmapsPluginView = useMemo( () => pluginDashboardViews.some((entry) => entry.pluginId === "fusion-plugin-roadmap"), [pluginDashboardViews], ); const hasViewOverflowItems = useMemo(() => { return !!( experimentalFeatures?.researchView || todosEnabled || experimentalFeatures?.insights || (experimentalFeatures?.roadmap && !hasRoadmapsPluginView) || showSkillsTab || experimentalFeatures?.memoryView || experimentalFeatures?.devServerView || !hideFullNav || pluginDashboardViews.some((entry) => entry.view.placement !== "primary") ); }, [experimentalFeatures, todosEnabled, showSkillsTab, hideFullNav, pluginDashboardViews, hasRoadmapsPluginView]); const getEffectiveViewport = useCallback(() => { const vv = window.visualViewport; if (vv && vv.width > 0 && vv.height > 0) { return { width: vv.width, height: vv.height, offsetTop: vv.offsetTop, offsetLeft: vv.offsetLeft, }; } return { width: window.innerWidth, height: window.innerHeight, offsetTop: 0, offsetLeft: 0, }; }, []); const updateScriptsDropdownPosition = useCallback(() => { const trigger = scriptsChevronButtonRef.current; if (!trigger) return; const rect = trigger.getBoundingClientRect(); const menu = scriptsMenuRef.current; const { width: viewportWidth, height: viewportHeight, offsetTop, offsetLeft } = getEffectiveViewport(); const horizontalPadding = 16; const verticalPadding = 16; const gap = 6; const measuredWidth = menu?.offsetWidth || Math.max(rect.width, 260); const width = Math.min( measuredWidth, Math.max(viewportWidth - horizontalPadding * 2, 160), ); const measuredHeight = menu?.offsetHeight || 280; const constrainedHeight = Math.min( measuredHeight, Math.max(viewportHeight - verticalPadding * 2, 160), ); const triggerTop = rect.top - offsetTop; const triggerBottom = rect.bottom - offsetTop; const triggerRight = rect.right - offsetLeft; const spaceBelow = viewportHeight - triggerBottom; const spaceAbove = triggerTop; const openUpward = spaceBelow < constrainedHeight && spaceAbove > spaceBelow; const left = Math.min( Math.max(triggerRight - width, horizontalPadding), viewportWidth - horizontalPadding - width, ) + offsetLeft; const top = openUpward ? Math.max(verticalPadding + offsetTop, triggerTop - constrainedHeight - gap + offsetTop) : Math.min( triggerBottom + gap + offsetTop, viewportHeight + offsetTop - verticalPadding - constrainedHeight, ); setScriptsDropdownPosition({ top, left, width }); }, [getEffectiveViewport]); const handleRunQuickScript = useCallback( (name: string, command: string) => { onRunScript?.(name, command); setIsScriptsOpen(false); setHighlightedScriptIndex(-1); }, [onRunScript], ); const handleManageScripts = useCallback(() => { onOpenScripts?.(); setIsScriptsOpen(false); setHighlightedScriptIndex(-1); }, [onOpenScripts]); const handleScriptsDropdownKeyDown = useCallback( (e: ReactKeyboardEvent) => { switch (e.key) { case "ArrowDown": e.preventDefault(); if (totalScriptItems > 0) { setHighlightedScriptIndex((prev) => (prev < totalScriptItems - 1 ? prev + 1 : 0)); } break; case "ArrowUp": e.preventDefault(); if (totalScriptItems > 0) { setHighlightedScriptIndex((prev) => (prev > 0 ? prev - 1 : totalScriptItems - 1)); } break; case "Enter": e.preventDefault(); if (highlightedScriptIndex >= 0) { if (highlightedScriptIndex < scriptEntries.length) { const [name, command] = scriptEntries[highlightedScriptIndex]; handleRunQuickScript(name, command); } else if (showScriptsFooter && highlightedScriptIndex === scriptEntries.length) { handleManageScripts(); } } break; case "Home": e.preventDefault(); if (totalScriptItems > 0) { setHighlightedScriptIndex(0); } break; case "End": e.preventDefault(); if (totalScriptItems > 0) { setHighlightedScriptIndex(totalScriptItems - 1); } break; } }, [handleManageScripts, handleRunQuickScript, highlightedScriptIndex, scriptEntries, showScriptsFooter, totalScriptItems], ); // Keep ref in sync with state useEffect(() => { terminalSubmenuOpenRef.current = isTerminalSubmenuOpen; }, [isTerminalSubmenuOpen]); useEffect(() => { scriptsOpenRef.current = isScriptsOpen; }, [isScriptsOpen]); // Fetch scripts when terminal submenu opens in compact mode useEffect(() => { if (!isTerminalSubmenuOpen || !isCompact) return; let cancelled = false; setOverflowScriptsLoading(true); fetchScripts(projectId) .then((data) => { if (!cancelled) { setOverflowScripts(data); } }) .catch(() => { if (!cancelled) { setOverflowScripts({}); } }) .finally(() => { if (!cancelled) { setOverflowScriptsLoading(false); } }); return () => { cancelled = true; }; }, [isTerminalSubmenuOpen, isCompact, projectId]); // Fetch scripts when desktop scripts dropdown opens useEffect(() => { if (!isScriptsOpen || isCompact) return; let cancelled = false; setScriptsLoading(true); fetchScripts(projectId) .then((data) => { if (!cancelled) { setScripts(data); } }) .catch(() => { if (!cancelled) { setScripts({}); } }) .finally(() => { if (!cancelled) { setScriptsLoading(false); } }); return () => { cancelled = true; }; }, [isScriptsOpen, isCompact, projectId]); // Close desktop scripts dropdown on outside click useEffect(() => { if (!isScriptsOpen) return; const handleClickOutside = (e: MouseEvent) => { if ( scriptsSplitButtonRef.current && !scriptsSplitButtonRef.current.contains(e.target as Node) ) { setIsScriptsOpen(false); } }; document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, [isScriptsOpen]); // Close desktop scripts dropdown on Escape useEffect(() => { if (!isScriptsOpen) return; const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape") { setIsScriptsOpen(false); scriptsChevronButtonRef.current?.focus(); } }; document.addEventListener("keydown", handleKeyDown); return () => document.removeEventListener("keydown", handleKeyDown); }, [isScriptsOpen]); // Reset highlight and focus menu when dropdown opens useEffect(() => { if (isScriptsOpen) { setHighlightedScriptIndex(-1); const timeoutId = window.setTimeout(() => scriptsMenuRef.current?.focus(), 0); return () => window.clearTimeout(timeoutId); } setScriptsDropdownPosition(null); }, [isScriptsOpen]); // Position scripts dropdown when opening and content changes useEffect(() => { if (!isScriptsOpen) return; const rafId = requestAnimationFrame(() => { updateScriptsDropdownPosition(); }); return () => cancelAnimationFrame(rafId); }, [isScriptsOpen, scriptsLoading, scriptEntries.length, showScriptsFooter, updateScriptsDropdownPosition]); // Keep scripts dropdown anchored on viewport changes useEffect(() => { if (!isScriptsOpen) return; const handleReposition = () => updateScriptsDropdownPosition(); window.addEventListener("resize", handleReposition); window.addEventListener("scroll", handleReposition, true); const vv = window.visualViewport; if (vv) { vv.addEventListener("resize", handleReposition); vv.addEventListener("scroll", handleReposition); } return () => { window.removeEventListener("resize", handleReposition); window.removeEventListener("scroll", handleReposition, true); if (vv) { vv.removeEventListener("resize", handleReposition); vv.removeEventListener("scroll", handleReposition); } }; }, [isScriptsOpen, updateScriptsDropdownPosition]); useEffect(() => { if (isCompact) { setIsScriptsOpen(false); setHighlightedScriptIndex(-1); } }, [isCompact]); // Keep mobile search open if there's an active search query const shouldShowMobileSearch = isMobileSearchOpen || searchQuery.length > 0; // Non-mobile search: toggled open OR has active query, but not if explicitly closed const shouldShowNonMobileSearch = (isNonMobileSearchOpen || searchQuery.length > 0) && !isNonMobileSearchExplicitlyClosed; // Show toggle when search is available, NOT currently shown, NOT explicitly closed, AND query is empty const canShowNonMobileSearchToggle = (view === "board" || view === "list") && !isMobile && onSearchChange && !isNonMobileSearchExplicitlyClosed && searchQuery.length === 0; const canShowNonMobileSearch = (view === "board" || view === "list") && !isMobile && onSearchChange; // Reset explicit close flag when query becomes empty (so toggle reappears) useEffect(() => { if (searchQuery === "") { setIsNonMobileSearchExplicitlyClosed(false); } }, [searchQuery]); // Close overflow menu on outside click useEffect(() => { if (!isOverflowMenuOpen) return; const handleClickOutside = (e: MouseEvent) => { if ( overflowMenuRef.current && !overflowMenuRef.current.contains(e.target as Node) && overflowButtonRef.current && !overflowButtonRef.current.contains(e.target as Node) ) { setIsOverflowMenuOpen(false); } }; document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, [isOverflowMenuOpen]); // Close desktop overflow menu on outside click useEffect(() => { if (!isDesktopOverflowOpen) return; const handleClickOutside = (e: MouseEvent) => { if ( desktopOverflowRef.current && !desktopOverflowRef.current.contains(e.target as Node) && desktopOverflowTriggerRef.current && !desktopOverflowTriggerRef.current.contains(e.target as Node) ) { setIsDesktopOverflowOpen(false); } }; document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, [isDesktopOverflowOpen]); // Close node selector on outside click useEffect(() => { if (!isNodeSelectorOpen) return; const handleClickOutside = (e: MouseEvent) => { if ( nodeSelectorRef.current && !nodeSelectorRef.current.contains(e.target as Node) ) { setIsNodeSelectorOpen(false); } }; document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, [isNodeSelectorOpen]); // Close menus on Escape key useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape") { setIsViewOverflowOpen(false); setIsDesktopOverflowOpen(false); if (terminalSubmenuOpenRef.current) { setIsTerminalSubmenuOpen(false); return; } if (scriptsOpenRef.current) { setIsScriptsOpen(false); scriptsChevronButtonRef.current?.focus(); return; } setIsOverflowMenuOpen(false); setIsMobileSearchOpen(false); setIsNodeSelectorOpen(false); setIsMobileProjectSwitchOpen(false); } }; document.addEventListener("keydown", handleKeyDown); return () => document.removeEventListener("keydown", handleKeyDown); }, []); // Close mobile project switch on outside click useEffect(() => { if (!isMobileProjectSwitchOpen) return; const handleClickOutside = (e: MouseEvent) => { if ( mobileProjectSwitchRef.current && !mobileProjectSwitchRef.current.contains(e.target as Node) ) { setIsMobileProjectSwitchOpen(false); } }; document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, [isMobileProjectSwitchOpen]); // Close engine controls dropdown on outside click useEffect(() => { if (!isEngineMenuOpen) return; const handleClickOutside = (e: MouseEvent) => { if (engineMenuRef.current && !engineMenuRef.current.contains(e.target as Node)) { setIsEngineMenuOpen(false); } }; document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, [isEngineMenuOpen]); // Close engine controls dropdown on Escape useEffect(() => { if (!isEngineMenuOpen) return; const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape") setIsEngineMenuOpen(false); }; document.addEventListener("keydown", handleKeyDown); return () => document.removeEventListener("keydown", handleKeyDown); }, [isEngineMenuOpen]); // Close view toggle overflow on outside click useEffect(() => { if (!isViewOverflowOpen) return; const handleClickOutside = (e: MouseEvent) => { if ( viewOverflowRef.current && !viewOverflowRef.current.contains(e.target as Node) && viewOverflowTriggerRef.current && !viewOverflowTriggerRef.current.contains(e.target as Node) ) { setIsViewOverflowOpen(false); } }; document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, [isViewOverflowOpen]); const handleMobileSearchToggle = useCallback(() => { setIsMobileSearchOpen((prev) => !prev); }, []); const handleNonMobileSearchToggle = useCallback(() => { setIsNonMobileSearchOpen(true); setIsNonMobileSearchExplicitlyClosed(false); }, []); const handleNonMobileSearchClose = useCallback(() => { setIsNonMobileSearchOpen(false); setIsNonMobileSearchExplicitlyClosed(true); if (onSearchChange) onSearchChange(""); }, [onSearchChange]); const handleOverflowToggle = useCallback(() => { setIsOverflowMenuOpen((prev) => !prev); }, []); const handleOverflowAction = useCallback((callback?: () => void) => { if (callback) callback(); setIsOverflowMenuOpen(false); setIsTerminalSubmenuOpen(false); }, []); const handleMobileSearchClose = useCallback(() => { setIsMobileSearchOpen(false); if (onSearchChange) onSearchChange(""); }, [onSearchChange]); return (

Fusion

{/* Mobile Project Switch - dropdown trigger next to logo when at least one project exists (mobile only) */} {isMobile && projects.length >= 1 && onSelectProject && (
{isMobileProjectSwitchOpen && (
{projects.map((project) => { const isCurrent = currentProject?.id === project.id; const statusColor = PROJECT_STATUS_CONFIG[project.status]?.color; return ( ); })}
)}
)} {/* Project Selector - Back button when project selected, dropdown when 2+ projects (desktop only) */} {!isCompact && projects.length >= 1 && onViewAllProjects && ( )} {/* Node selector and status indicator */} {showNodeSelector && (
{/* Node status indicator - always visible */} {/* Node selector dropdown - desktop/tablet only */} {!isMobile && ( <> {/* Node selector dropdown menu */} {isNodeSelectorOpen && (
{/* Local option */} {/* Remote nodes */} {remoteNodes.map((node) => ( ))}
)} )}
)}
{/* Mobile View Toggle - compact board/list switcher in header when mobile nav is active */} {hideFullNav && onChangeView && (view === "board" || view === "list") && (
)} {/* Mobile Search Trigger - only on mobile, show trigger button in header */} {onSearchChange && isMobile && (hideFullNav || view === "board" || view === "list") && !shouldShowMobileSearch && ( )} {/* Desktop/Tablet Search Toggle - show icon when search is available but hidden */} {canShowNonMobileSearchToggle && ( )} {/* Usage button on mobile when mobile bottom nav is active */} {isMobile && hideFullNav && onOpenUsage && ( )} {/* View Toggle - always inline, even on mobile */} {!hideFullNav && onChangeView && (
{showAgentsTab && ( )} {pluginDashboardViews .filter((entry) => entry.view.placement === "primary") .sort((a, b) => (a.view.order ?? Number.MAX_SAFE_INTEGER) - (b.view.order ?? Number.MAX_SAFE_INTEGER)) .map((entry) => { const pluginTaskView = buildPluginTaskViewId(entry.pluginId, entry.view.viewId); const PluginIcon = getPluginNavIcon(entry.view.icon); return ( ); })} {hasViewOverflowItems && ( <> {isViewOverflowOpen && (
{experimentalFeatures?.researchView && ( )} {experimentalFeatures?.insights && ( )} {experimentalFeatures?.roadmap && !hasRoadmapsPluginView && ( )} {showSkillsTab && ( )} {experimentalFeatures?.memoryView && ( )} {experimentalFeatures?.devServerView && ( )} {todosEnabled && onOpenTodos && ( )} {pluginDashboardViews .filter((entry) => entry.view.placement !== "primary") .sort((a, b) => (a.view.order ?? Number.MAX_SAFE_INTEGER) - (b.view.order ?? Number.MAX_SAFE_INTEGER)) .map((entry) => { const pluginTaskView = buildPluginTaskViewId(entry.pluginId, entry.view.viewId); const PluginIcon = getPluginNavIcon(entry.view.icon); return ( ); })}
)} )}
)} {/* Usage button - desktop only (moved to overflow on mobile/tablet) */} {!isCompact && onOpenUsage && ( )} {/* System Stats button - desktop only */} {!isCompact && onOpenSystemStats && ( )} {/* Activity Log button - desktop only (moved to overflow on mobile/tablet) */} {!isCompact && onOpenActivityLog && ( )} {/* Desktop actions */} {!isCompact && !isElectron && ( )} {!isCompact && ( )} {/* Terminal split button - desktop only (moved to overflow on mobile/tablet) */} {!isCompact && (
{onOpenScripts && onRunScript && ( <> {isScriptsOpen && (
{scriptsLoading ? (
Loading scripts...
) : scriptEntries.length === 0 ? (

No scripts configured

) : ( <>
{scriptEntries.map(([name, command], index) => ( ))}
)}
)} )}
)} {/* Files button - desktop only (moved to overflow on mobile/tablet) */} {!isCompact && onOpenFiles && ( )} {!isCompact && todosEnabled && onOpenTodos && ( )} {/* Git Manager button - desktop only (moved to overflow on mobile/tablet) */} {!isCompact && onOpenGitManager && ( )} {/* Workflow Steps - desktop only (moved to overflow on mobile/tablet) */} {!isCompact && onOpenWorkflowSteps && ( )} {/* Desktop overflow menu for Nodes and Schedules */} {!isCompact && (
{isDesktopOverflowOpen && (
{onOpenNodes && showNodesButton !== false && ( )}
)}
)} {/* Engine control split-button: main=stop/start, chevron dropdown=pause triage */}
{isEngineMenuOpen && (
)}
{/* Settings - always inline on desktop, placed after engine controls */} {!isCompact && ( )} {/* Plugin UI slot for header actions */} {/* Compact overflow menu trigger (mobile + tablet) */} {isCompact && !hideFullNav && ( )} {/* Compact overflow menu (mobile + tablet) */} {isCompact && !hideFullNav && isOverflowMenuOpen && (
{/* Projects - in overflow on mobile */} {projects.length >= 1 && onViewAllProjects && ( )} {/* Files - in overflow on mobile */} {onOpenFiles && ( )} {/* Git Manager - in overflow on mobile */} {onOpenGitManager && ( )} {/* Nodes - in overflow on mobile */} {onOpenNodes && showNodesButton !== false && ( )} {!isElectron && ( )}
{isTerminalSubmenuOpen && (
{overflowScriptsLoading ? (
Loading scripts…
) : overflowScriptEntries.length > 0 ? ( <> {overflowScriptEntries.map(([name, command]) => ( ))} {onOpenScripts && ( )} ) : ( onOpenScripts && ( ) )}
)}
{/* Activity Log - in overflow on mobile */} {onOpenActivityLog && ( )} {/* Mailbox - in overflow on mobile */} {onOpenMailbox && ( )} {/* Usage - in overflow on mobile */} {onOpenUsage && ( )} {/* Workflow Steps - in overflow on mobile */} {onOpenWorkflowSteps && ( )} {/* Settings - always last in overflow menu */}
)}
{/* Desktop/Tablet Search - floating below header, in board or list view */} {canShowNonMobileSearch && shouldShowNonMobileSearch && (
onSearchChange(e.target.value)} className="header-search-input" />
)} {/* Mobile Search Expanded - floating below header */} {onSearchChange && isMobile && shouldShowMobileSearch && (
onSearchChange(e.target.value)} className="header-search-input" />
)}
); }