diff --git a/AGENTS.md b/AGENTS.md index 6d2a34036b..a90db432a6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -218,11 +218,10 @@ Scoped exception (FN-5819): shared-branch-group members (`branchContext.assignme ### Lazy-Loaded Heavy Views -These 22 views are lazy-loaded via `React.lazy()` with ``. +These 21 views are lazy-loaded via `React.lazy()` with ``. Keep this AGENTS inventory in sync with App lazy imports, AppModals lazy modal imports (`SettingsModal`, `WorkflowNodeEditor`, `SetupWizardModal`), and `packages/dashboard/app/__tests__/lazy-loaded-views-docs.test.ts`. - `AgentsView` -- `NodesView` - `ChatView` - `MemoryView` - `DevServerView` diff --git a/packages/dashboard/app/App.tsx b/packages/dashboard/app/App.tsx index 9bd9d80d6b..d8da78277f 100644 --- a/packages/dashboard/app/App.tsx +++ b/packages/dashboard/app/App.tsx @@ -109,7 +109,6 @@ const DocumentsView = lazy(() => import("./components/DocumentsView").then((m) = const InsightsView = lazy(() => import("./components/InsightsView").then((m) => ({ default: m.InsightsView }))); const ResearchView = lazy(() => import("./components/ResearchView").then((m) => ({ default: m.ResearchView }))); const EvalsView = lazy(() => import("./components/EvalsView").then((m) => ({ default: m.EvalsView }))); -const NodesView = lazy(() => import("./components/NodesView").then((m) => ({ default: m.NodesView }))); const ChatView = lazy(() => import("./components/ChatView").then((m) => ({ default: m.ChatView }))); const SkillsView = lazy(() => import("./components/SkillsView").then((m) => ({ default: m.SkillsView }))); @@ -139,7 +138,6 @@ function prefetchLazyViews() { void import("./components/InsightsView"); void import("./components/ResearchView"); void import("./components/EvalsView"); - void import("./components/NodesView"); void import("./components/ChatView"); void import("./components/SkillsView"); @@ -848,11 +846,6 @@ function AppInner() { }); }, [boardSourceTasks, branchFilter, baseBranchFilter]); - // Nodes management is an overlay view (not a modal), so it stays local to App. - const [nodesOpen, setNodesOpen] = useState(false); - const closeNodes = useCallback(() => { - setNodesOpen(false); - }, []); const [retryingProjects, setRetryingProjects] = useState(false); const [missionResumeSessionId, setMissionResumeSessionId] = useState(undefined); const [missionTargetId, setMissionTargetId] = useState(undefined); @@ -1089,13 +1082,6 @@ function AppInner() { } }, [taskView, settingsLoaded, skillsEnabled, insightsEnabled, handleChangeTaskView, agentsEnabled, memoryEnabled, devServerEnabled, researchEnabled, evalsEnabled, goalsEnabled, graphPluginTaskView]); - // Auto-close nodes overlay if feature flag is toggled off while overlay is open - useEffect(() => { - if (nodesOpen && !nodesEnabled) { - removeNav(closeNodes); - closeNodes(); - } - }, [closeNodes, nodesOpen, nodesEnabled, removeNav]); const { availableModels, favoriteProviders, @@ -1231,27 +1217,6 @@ function AppInner() { [workflowSteps], ); - const _handleOpenNodes = useCallback(() => { - if (!nodesEnabled) return; - setNodesOpen((prev) => !prev); - }, [nodesEnabled]); - - const closeNodesWithNav = useCallback(() => { - removeNav(closeNodes); - closeNodes(); - }, [closeNodes, removeNav]); - - // History-aware nodes toggle — pushes nav entry only when opening - const handleOpenNodesWithNav = useCallback(() => { - if (!nodesEnabled) return; - if (!nodesOpen) { - setNodesOpen(true); - pushNav({ type: "view", revert: closeNodes }); - } else { - closeNodesWithNav(); - } - }, [closeNodes, closeNodesWithNav, nodesEnabled, nodesOpen, pushNav]); - // History-aware modal open handlers — push nav entries for back-navigation. const openDetailTask = useCallback((task: Task | TaskDetail, tab?: Parameters[1], opts?: { origin?: DetailTaskOrigin }) => { modalManager.openDetailTask(task, tab, opts); @@ -1525,18 +1490,6 @@ function AppInner() { ); } - if (nodesOpen) { - return ( -
- - - - - -
- ); - } - if (viewMode === "overview") { return ( @@ -1825,6 +1778,8 @@ function AppInner() { themeMode={themeMode} onColorThemeChange={setColorTheme} onThemeModeChange={setThemeMode} + addToast={addToast} + nodesEnabled={nodesEnabled} />
@@ -1969,8 +1924,6 @@ function AppInner() { stashOrphanCount={stashOrphanCount} onOpenSchedules={openSchedulesWithNav} onOpenGitManager={openGitManagerWithNav} - onOpenNodes={handleOpenNodesWithNav} - showNodesButton={nodesEnabled} onOpenWorkflowEditor={openWorkflowEditorWithNav} onOpenScripts={openScriptsWithNav} onRunScript={runScriptWithNav} @@ -2040,7 +1993,7 @@ function AppInner() { /> )} - {viewMode === "project" && currentProject && !nodesOpen && taskView !== "missions" && !modalManager.isPlanningOpen && !sessionBannersHidden && ( + {viewMode === "project" && currentProject && taskView !== "missions" && !modalManager.isPlanningOpen && !sessionBannersHidden && ( {renderMainContent()} - {viewMode === "project" && currentProject && !nodesOpen && ( + {viewMode === "project" && currentProject && ( 0 ? remoteData.tasks : tasks} projectId={currentProject.id} @@ -2170,7 +2123,6 @@ function AppInner() { onOpenSettings={openSettingsWithNav} onOpenActivityLog={openActivityLogWithNav} onOpenMailbox={() => handleTaskViewChange("mailbox")} - onOpenNodes={handleOpenNodesWithNav} mailboxUnreadCount={mailboxUnreadCount} mailboxPendingApprovalCount={mailboxPendingApprovalCount} chatHasUnreadResponse={chatHasUnreadResponse} @@ -2201,7 +2153,6 @@ function AppInner() { researchView: researchEnabled, evalsView: evalsEnabled, goalsView: goalsEnabled, - nodesView: nodesEnabled, }} pluginDashboardViews={pluginDashboardViews} shellConnectionControl={ diff --git a/packages/dashboard/app/__tests__/lazy-loaded-views-docs.test.ts b/packages/dashboard/app/__tests__/lazy-loaded-views-docs.test.ts index ed22338b2f..7e814e2906 100644 --- a/packages/dashboard/app/__tests__/lazy-loaded-views-docs.test.ts +++ b/packages/dashboard/app/__tests__/lazy-loaded-views-docs.test.ts @@ -8,6 +8,9 @@ Merging main reconciled the curated count to 23 (main's 22 lazy views/modals + C FNXC:CommandCenter 2026-06-19-00:00: FN-6702 removes ReliabilityView from the App-level lazy inventory because Reliability now mounts inside the lazy CommandCenter chunk. + +FNXC:CommandCenter 2026-06-19-00:00: +FN-6717 removes NodesView from the App-level lazy inventory because Nodes now mounts inside the lazy CommandCenter chunk. */ import { describe, expect, it } from "vitest"; import { readFileSync } from "node:fs"; @@ -15,7 +18,6 @@ import { resolve } from "node:path"; const EXPECTED_DOCUMENTED_VIEWS = new Set([ "AgentsView", - "NodesView", "ChatView", "MemoryView", "DevServerView", @@ -44,7 +46,6 @@ const EXPECTED_APP_LEVEL_VIEWS = new Set([ "InsightsView", "ResearchView", "EvalsView", - "NodesView", "ChatView", "SkillsView", "MemoryView", @@ -106,7 +107,7 @@ function extractAppModalsLazyViews(appModalsSource: string): Set { } describe("AGENTS lazy-loaded views inventory", () => { - it("documents the App-level and AppModals lazy views accurately and keeps the curated 22-view list in sync", () => { + it("documents the App-level and AppModals lazy views accurately and keeps the curated 21-view list in sync", () => { const agentsDoc = readFileSync(resolve(__dirname, "../../../../AGENTS.md"), "utf-8"); const appSource = readFileSync(resolve(__dirname, "../App.tsx"), "utf-8"); const appModalsSource = readFileSync(resolve(__dirname, "../components/AppModals.tsx"), "utf-8"); @@ -114,11 +115,11 @@ describe("AGENTS lazy-loaded views inventory", () => { const section = extractLazyLoadedSection(agentsDoc); const countMatch = section.match(/These\s+(\d+)\s+views\s+are lazy-loaded/); expect(countMatch).toBeTruthy(); - expect(Number(countMatch?.[1])).toBe(22); + expect(Number(countMatch?.[1])).toBe(21); const documentedViews = extractBacktickedNamesFromBullets(section); expect(new Set(documentedViews)).toEqual(EXPECTED_DOCUMENTED_VIEWS); - expect(documentedViews).toHaveLength(22); + expect(documentedViews).toHaveLength(21); expect(section).toContain("`ResearchView`"); expect(section).toContain("`TodoView`"); diff --git a/packages/dashboard/app/__tests__/mobile-feature-access-regression.test.tsx b/packages/dashboard/app/__tests__/mobile-feature-access-regression.test.tsx index c3f98cf858..d3286a0747 100644 --- a/packages/dashboard/app/__tests__/mobile-feature-access-regression.test.tsx +++ b/packages/dashboard/app/__tests__/mobile-feature-access-regression.test.tsx @@ -46,7 +46,6 @@ const createDefaultMobileNavProps = () => ({ onOpenSettings: vi.fn(), onOpenActivityLog: vi.fn(), onOpenMailbox: vi.fn(), - onOpenNodes: vi.fn(), mailboxUnreadCount: 0, onOpenGitManager: vi.fn(), onOpenWorkflowEditor: vi.fn(), @@ -176,16 +175,15 @@ describe("Mobile Feature Access Regression Guard", () => { expect(props.onChangeView).toHaveBeenCalledWith("command-center"); }); - it("nodes view is reachable from mobile More sheet when enabled", () => { + it("nodes is no longer a mobile More item and is reached via Command Center", () => { const props = createDefaultMobileNavProps(); - render(); + render(); fireEvent.click(screen.getByTestId("mobile-nav-tab-more")); + expect(screen.queryByTestId("mobile-more-item-nodes")).toBeNull(); - const nodesItem = screen.getByTestId("mobile-more-item-nodes"); - expect(nodesItem).toBeDefined(); - fireEvent.click(nodesItem); - expect(props.onOpenNodes).toHaveBeenCalledOnce(); + fireEvent.click(screen.getByTestId("mobile-nav-tab-command-center")); + expect(props.onChangeView).toHaveBeenCalledWith("command-center"); }); it("chat is accessible via the bottom nav while remaining absent from the More sheet", () => { diff --git a/packages/dashboard/app/components/Header.tsx b/packages/dashboard/app/components/Header.tsx index 875e7141d2..6496b63fe2 100644 --- a/packages/dashboard/app/components/Header.tsx +++ b/packages/dashboard/app/components/Header.tsx @@ -1,6 +1,6 @@ import { useState, useEffect, useRef, useCallback, useMemo, type KeyboardEvent as ReactKeyboardEvent, type ReactNode } from "react"; import { useTranslation } from "react-i18next"; -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, Lock, Gauge } from "lucide-react"; +import { Settings, Pause, Play, Square, LayoutGrid, List, Terminal, Lightbulb, Search, X, Activity, MoreHorizontal, Clock, Folder, History, GitBranch, Monitor, Workflow, Bot, Target, ChevronRight, FileCode, Loader2, Grid3X3, Mail, MessageSquare, ChevronDown, Check, Zap, Sparkles, FileText, Brain, CheckSquare, Lock, Gauge } from "lucide-react"; import "./Header.css"; // ProjectSelector styles used by the imported standalone component. import "./ProjectSelector.css"; @@ -77,9 +77,6 @@ export interface HeaderProps { stashOrphanCount?: number; onOpenSchedules?: () => void; onOpenGitManager?: () => void; - onOpenNodes?: () => void; - /** When false, hides the Nodes management button. Defaults to true for backward compat. */ - showNodesButton?: boolean; onOpenWorkflowEditor?: () => void; onOpenScripts?: () => void; onRunScript?: (name: string, command: string) => void; @@ -146,8 +143,6 @@ export function Header({ stashOrphanCount = 0, onOpenSchedules, onOpenGitManager, - onOpenNodes, - showNodesButton, onOpenWorkflowEditor, onOpenScripts, onRunScript, @@ -1524,20 +1519,6 @@ export function Header({ role="menu" aria-label={t("header.moreActions", "More actions")} > - {onOpenNodes && showNodesButton !== false && ( - - )} )} - {/* Nodes - in overflow on mobile */} - {onOpenNodes && showNodesButton !== false && ( - - )} {!isDesktopShell && ( )} - {experimentalFeatures?.nodesView && onOpenNodes && ( - - )} - {todoViewEnabled && ( + {onClose ? ( + + ) : null} + + ), +})); + function tokenFixture(totalTokens = 1_500) { return { from: "2026-06-08", @@ -716,6 +726,7 @@ describe("CommandCenter shell", () => { const tabs = within(tablist).getAllByRole("tab"); // Overview, Tokens, Tools, Activity, Productivity, Team, Ecosystem, GitHub, Signals, System, Reliability, Mission Control. expect(tabs.length).toBe(12); + expect(screen.queryByTestId("command-center-tab-nodes")).toBeNull(); // roving tabindex: exactly one tab is focusable. const focusable = tabs.filter((tab) => tab.getAttribute("tabindex") === "0"); expect(focusable.length).toBe(1); @@ -742,6 +753,25 @@ describe("CommandCenter shell", () => { expect(screen.getByTestId("cc-system-cpu-gauge")).toBeTruthy(); }); + it("renders and routes the Nodes tab when the nodes feature is enabled", () => { + const addToast = vi.fn(); + render(); + expect(screen.getAllByTestId("command-center-tab-nodes")).toHaveLength(1); + + fireEvent.click(screen.getByTestId("command-center-tab-nodes")); + expect(screen.getByTestId("command-center-tab-nodes").getAttribute("aria-selected")).toBe("true"); + expect(screen.getByTestId("command-center-panel-nodes")).toBeTruthy(); + expect(screen.getByTestId("nodes-view")).toBeTruthy(); + + fireEvent.click(screen.getByTestId("nodes-view-toast-probe")); + expect(addToast).toHaveBeenCalledWith("Nodes tab toast", "success"); + }); + + it("omits the Nodes tab when the nodes feature is disabled", () => { + render(); + expect(screen.queryByTestId("command-center-tab-nodes")).toBeNull(); + }); + it("renders and routes the GitHub tab exactly once", async () => { mockOverviewApi({ github: githubFixture(4, 2) }); render(); @@ -858,7 +888,7 @@ describe("CommandCenter shell", () => { }); it("keeps existing Command Center tab test ids after adding Team", () => { - render(); + render(); for (const id of [ "overview", "tokens", @@ -869,6 +899,7 @@ describe("CommandCenter shell", () => { "github", "signals", "system", + "nodes", "reliability", "mission-control", "team", @@ -878,7 +909,7 @@ describe("CommandCenter shell", () => { }); it("supports arrow-key navigation between tabs (roving tabindex)", () => { - render(); + render(); const overviewTab = screen.getByTestId("command-center-tab-overview"); overviewTab.focus(); fireEvent.keyDown(overviewTab, { key: "ArrowRight" }); @@ -889,6 +920,11 @@ describe("CommandCenter shell", () => { const systemTab = screen.getByTestId("command-center-tab-system"); systemTab.focus(); fireEvent.keyDown(systemTab, { key: "ArrowRight" }); + const nodesTab = screen.getByTestId("command-center-tab-nodes"); + expect(nodesTab.getAttribute("aria-selected")).toBe("true"); + expect(document.activeElement).toBe(nodesTab); + + fireEvent.keyDown(nodesTab, { key: "ArrowRight" }); const reliabilityTab = screen.getByTestId("command-center-tab-reliability"); expect(reliabilityTab.getAttribute("aria-selected")).toBe("true"); expect(document.activeElement).toBe(reliabilityTab); diff --git a/packages/dashboard/app/styles.css b/packages/dashboard/app/styles.css index f1bf0bb42f..dda23f1ca6 100644 --- a/packages/dashboard/app/styles.css +++ b/packages/dashboard/app/styles.css @@ -3720,7 +3720,7 @@ input[type="range"]:focus-visible { base rules (e.g. /\.mailbox-view\s*\{[^}]*\}/) don't pick this up as the "first" definition for any individual selector. */ :is(.insights-view, .memory-view, .dev-server-view, .skills-view, - .documents-view, .roadmaps-view, .nodes-management-overlay, + .documents-view, .roadmaps-view, .agents-view, .chat-view, .mission-manager, .mailbox-view, .reliability-view) { min-height: 0; diff --git a/packages/i18n/locales/en/app.json b/packages/i18n/locales/en/app.json index 7352c4bdb3..c7fb632fdf 100644 --- a/packages/i18n/locales/en/app.json +++ b/packages/i18n/locales/en/app.json @@ -7544,6 +7544,7 @@ "tools": "Tools", "activity": "Activity", "productivity": "Productivity", + "nodes": "Nodes", "reliability": "Reliability", "ecosystem": "Ecosystem", "missionControl": "Mission Control"