diff --git a/.changeset/fn-6886-planning-sidebar-view.md b/.changeset/fn-6886-planning-sidebar-view.md new file mode 100644 index 0000000000..8254f0f19d --- /dev/null +++ b/.changeset/fn-6886-planning-sidebar-view.md @@ -0,0 +1,5 @@ +--- +"@runfusion/fusion": patch +--- + +Move Planning Mode into the dashboard sidebar as a first-class embedded view while removing the desktop toolbar affordance. diff --git a/packages/dashboard/app/App.tsx b/packages/dashboard/app/App.tsx index 86d724ed37..5a663bcfd7 100644 --- a/packages/dashboard/app/App.tsx +++ b/packages/dashboard/app/App.tsx @@ -98,6 +98,7 @@ import { getScopedItem, removeScopedItem, setScopedItem } from "./utils/projectS import { subscribeSse } from "./sse-bus"; import { AUTH_TOKEN_RECOVERY_REQUIRED_EVENT } from "./auth"; import { AuthTokenRecoveryDialog } from "./components/AuthTokenRecoveryDialog"; +import { PlanningModeModal } from "./components/PlanningModeModal"; // ChatView's CSS is imported eagerly so the styles bundle into the main // CSS file. Without this, the lazy ChatView JS chunk loaded its own CSS @@ -1249,20 +1250,24 @@ function AppInner() { pushNav({ type: "modal", close: modalManager.closeNewTask }); }, [modalManager, pushNav]); + /* + FNXC:Navigation 2026-06-21-00:00: + FN-6886 keeps the existing planning payload setters but routes every programmatic Planning Mode entry point to the docked `planning` view instead of pushing a modal overlay history entry. + */ const openPlanningWithNav = useCallback(() => { modalManager.openPlanning(); - pushNav({ type: "modal", close: modalManager.closePlanning }); - }, [modalManager, pushNav]); + handleTaskViewChange("planning"); + }, [handleTaskViewChange, modalManager]); const openPlanningWithInitialPlanWithNav = useCallback((initialPlan: string, workflowId?: string | null) => { modalManager.openPlanningWithInitialPlan(initialPlan, workflowId); - pushNav({ type: "modal", close: modalManager.closePlanning }); - }, [modalManager, pushNav]); + handleTaskViewChange("planning"); + }, [handleTaskViewChange, modalManager]); const resumePlanningWithNav = useCallback(() => { modalManager.resumePlanning(); - pushNav({ type: "modal", close: modalManager.closePlanning }); - }, [modalManager, pushNav]); + handleTaskViewChange("planning"); + }, [handleTaskViewChange, modalManager]); const openSubtaskBreakdownWithNav = useCallback((description: string, workflowId?: string | null) => { modalManager.openSubtaskBreakdown(description, workflowId); @@ -1810,6 +1815,33 @@ function AppInner() { ); } + if (taskView === "planning") { + /* + FNXC:Navigation 2026-06-21-00:00: + FN-6886 renders Planning Mode as a top-level main-content destination. Sidebar navigation opens an empty planning view, while Board, Todos, inline create, and resume entry points carry their initial plan/workflow/session state through modalManager. + */ + const closePlanningView = () => { + modalManager.closePlanning(); + handleChangeTaskView("board"); + }; + return ( + + + + ); + } + if (taskView === "devserver" || taskView === "dev-server") { if (!settingsLoaded || !devServerEnabled) { return null; @@ -1944,9 +1976,6 @@ function AppInner() { shellHost={shellHost.host} onOpenSettings={openSettingsWithNav} onOpenGitHubImport={openGitHubImportWithNav} - onOpenPlanning={openPlanningWithNav} - onResumePlanning={resumePlanningWithNav} - activePlanningSessionCount={bgPlanningSessions.length} onOpenUsage={openUsageWithNav} onOpenActivityLog={openActivityLogWithNav} onOpenMailbox={() => handleTaskViewChange("mailbox")} diff --git a/packages/dashboard/app/__tests__/tablet-header-controls.test.tsx b/packages/dashboard/app/__tests__/tablet-header-controls.test.tsx index 8b247a3639..497298b9c2 100644 --- a/packages/dashboard/app/__tests__/tablet-header-controls.test.tsx +++ b/packages/dashboard/app/__tests__/tablet-header-controls.test.tsx @@ -162,7 +162,7 @@ describe("tablet header controls", () => { }); it("does not render planning button inline on tablet", () => { - renderTabletHeader({ onOpenPlanning: noop }); + renderTabletHeader(); expect(screen.queryByTitle("Create a task with AI planning")).toBeNull(); }); @@ -214,10 +214,10 @@ describe("tablet header controls", () => { expect(screen.getByText("Settings")).toBeDefined(); }); - it("overflow menu contains planning on tablet", () => { - renderTabletHeader({ onOpenPlanning: noop }); + it("overflow menu omits planning on tablet", () => { + renderTabletHeader(); fireEvent.click(screen.getByTitle("More header actions")); - expect(screen.getByTestId("overflow-planning-btn")).toBeDefined(); + expect(screen.queryByTestId("overflow-planning-btn")).toBeNull(); }); it("overflow menu contains GitHub import on tablet", () => { @@ -296,14 +296,6 @@ describe("tablet header controls", () => { expect(onToggleTerminal).toHaveBeenCalled(); }); - it("calls onOpenPlanning from overflow menu on tablet", () => { - const onOpenPlanning = vi.fn(); - renderTabletHeader({ onOpenPlanning }); - fireEvent.click(screen.getByTitle("More header actions")); - fireEvent.click(screen.getByTestId("overflow-planning-btn")); - expect(onOpenPlanning).toHaveBeenCalled(); - }); - it("calls onOpenUsage from overflow menu on tablet", () => { const onOpenUsage = vi.fn(); renderTabletHeader({ onOpenUsage }); diff --git a/packages/dashboard/app/components/AppModals.tsx b/packages/dashboard/app/components/AppModals.tsx index f2e6dca31f..a1fe30b984 100644 --- a/packages/dashboard/app/components/AppModals.tsx +++ b/packages/dashboard/app/components/AppModals.tsx @@ -8,7 +8,6 @@ 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"; @@ -157,11 +156,6 @@ export function AppModals({ modalManager.closeGitHubImport(); }, [modalManager.closeGitHubImport, removeNav]); - const closePlanningWithNav = useCallback(() => { - removeNav(modalManager.closePlanning); - modalManager.closePlanning(); - }, [modalManager.closePlanning, removeNav]); - const closeSubtaskWithNav = useCallback(() => { removeNav(modalManager.closeSubtask); modalManager.closeSubtask(); @@ -354,20 +348,6 @@ export function AppModals({ projectId={projectId} /> - - - - 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; /** Opens the mailbox view */ @@ -131,9 +126,6 @@ export interface HeaderProps { export function Header({ onOpenSettings, onOpenGitHubImport, - onOpenPlanning, - onResumePlanning, - activePlanningSessionCount = 0, onOpenUsage, onOpenActivityLog, onOpenMailbox, @@ -1354,26 +1346,10 @@ export function Header({ )} - {!isCompact && ( - - )} + {/* + FNXC:Navigation 2026-06-21-00:00: + FN-6886 removes the header Lightbulb affordances because Planning Mode is now a primary left-sidebar destination after Command Center and a single canonical MobileNavBar More item on compact breakpoints. + */} {/* Terminal split button - desktop only (moved to overflow on mobile/tablet) */} {!isCompact && ( @@ -1621,22 +1597,6 @@ export function Header({ {t("header.browseFiles", "Browse Files")} )} - {/* Git Manager - in overflow on mobile */} {onOpenGitManager && ( @@ -2326,15 +2326,10 @@ describe("App view switching", () => { }, }); + localStorage.setItem(taskViewStorageKey(), "todos"); + render(); - await waitFor(() => { - expect(screen.getByTestId("view-toggle-overflow-trigger")).toBeInTheDocument(); - }); - - fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger")); - fireEvent.click(screen.getByTestId("view-overflow-todos")); - await waitFor(() => { expect(screen.getByTestId("todo-view")).toBeInTheDocument(); }); @@ -2342,6 +2337,7 @@ describe("App view switching", () => { fireEvent.click(screen.getByTestId("todo-planning-button")); await waitFor(() => { + expect(screen.getByTestId("planning-view")).toBeInTheDocument(); expect(screen.getByText("Planning Mode")).toBeInTheDocument(); }); @@ -2775,61 +2771,48 @@ describe("App GitHub import", () => { }); describe("App Planning Mode", () => { - it("opens Planning Mode modal when plan button is clicked", async () => { + it("opens Planning Mode as an embedded view from the sidebar destination", async () => { render(); - // Wait for the header to render - await waitFor(() => { - expect(screen.getByTitle("Create a task with AI planning")).toBeTruthy(); - }); + const planningNavItem = await screen.findByTestId("sidebar-nav-planning"); + fireEvent.click(planningNavItem); - // Click the plan button - fireEvent.click(screen.getByTitle("Create a task with AI planning")); - - // Planning modal should be visible await waitFor(() => { + expect(screen.getByTestId("planning-view")).toBeTruthy(); expect(screen.getByText("Planning Mode")).toBeTruthy(); }); + expect(screen.queryByTestId("planning-btn")).toBeNull(); }); - it("closes Planning Mode modal on close button click", async () => { + it("closes Planning Mode embedded view back to the board", async () => { render(); + fireEvent.click(await screen.findByTestId("sidebar-nav-planning")); await waitFor(() => { - expect(screen.getByTitle("Create a task with AI planning")).toBeTruthy(); + expect(screen.getByTestId("planning-view")).toBeTruthy(); }); - // Open the modal - fireEvent.click(screen.getByTitle("Create a task with AI planning")); - await waitFor(() => { - expect(screen.getByText("Planning Mode")).toBeTruthy(); - }); - - // Close the modal using the close button fireEvent.click(screen.getByLabelText("Close")); - // Modal should be closed await waitFor(() => { expect(screen.queryByText("Transform your idea into a detailed task")).toBeNull(); + expect(screen.getByTestId("sidebar-nav-board").getAttribute("aria-current")).toBe("page"); }); }); - it("renders planning modal with correct initial state", async () => { + it("renders planning embedded view with correct initial state", async () => { + localStorage.setItem(taskViewStorageKey(), "planning"); + render(); await waitFor(() => { - expect(screen.getByTitle("Create a task with AI planning")).toBeTruthy(); - }); - - // Open the modal - fireEvent.click(screen.getByTitle("Create a task with AI planning")); - - // Initial view should show - await waitFor(() => { + expect(screen.getByTestId("planning-view")).toBeTruthy(); expect(screen.getByText("Transform your idea into a detailed task")).toBeTruthy(); expect(screen.getByPlaceholderText(/e.g., Build a user authentication system with login/)).toBeTruthy(); expect(screen.getByText("Start Planning")).toBeTruthy(); }); + + localStorage.removeItem(taskViewStorageKey()); }); }); diff --git a/packages/dashboard/app/components/__tests__/Header.test.tsx b/packages/dashboard/app/components/__tests__/Header.test.tsx index 6003103f07..ae5cc98ff3 100644 --- a/packages/dashboard/app/components/__tests__/Header.test.tsx +++ b/packages/dashboard/app/components/__tests__/Header.test.tsx @@ -794,95 +794,19 @@ describe("Header", () => { }); describe("planning button", () => { - it("renders planning button with correct title on desktop", () => { - renderHeader({ onOpenPlanning: vi.fn() }, "desktop"); - expect(screen.getByTitle("Create a task with AI planning")).toBeDefined(); - }); - - it("does not render planning button inline on mobile", () => { - renderHeader({ onOpenPlanning: vi.fn() }, "mobile"); + it("does not render legacy planning affordances in the header on desktop", () => { + renderHeader({}, "desktop"); expect(screen.queryByTitle("Create a task with AI planning")).toBeNull(); + expect(screen.queryByTitle("Resume planning session")).toBeNull(); + expect(screen.queryByTestId("planning-btn")).toBeNull(); + expect(screen.queryByTestId("planning-badge")).toBeNull(); }); - it("calls onOpenPlanning when planning button is clicked", () => { - const onOpenPlanning = vi.fn(); - renderHeader({ onOpenPlanning }, "desktop"); - fireEvent.click(screen.getByTitle("Create a task with AI planning")); - expect(onOpenPlanning).toHaveBeenCalled(); - }); - - it("has correct data-testid for testing on desktop", () => { - renderHeader({ onOpenPlanning: vi.fn() }, "desktop"); - expect(screen.getByTestId("planning-btn")).toBeDefined(); - }); - - describe("active session badge", () => { - it("does not render badge when activePlanningSessionCount is 0", () => { - renderHeader({ onOpenPlanning: vi.fn(), activePlanningSessionCount: 0 }, "desktop"); - expect(screen.queryByTestId("planning-badge")).toBeNull(); - }); - - it("does not render badge when activePlanningSessionCount is undefined", () => { - renderHeader({ onOpenPlanning: vi.fn() }, "desktop"); - expect(screen.queryByTestId("planning-badge")).toBeNull(); - }); - - it("renders badge when activePlanningSessionCount > 0", () => { - renderHeader({ onOpenPlanning: vi.fn(), activePlanningSessionCount: 1 }, "desktop"); - expect(screen.getByTestId("planning-badge")).toBeDefined(); - }); - - it("badge shows correct count", () => { - renderHeader({ onOpenPlanning: vi.fn(), activePlanningSessionCount: 3 }, "desktop"); - expect(screen.getByTestId("planning-badge").textContent).toBe("3"); - }); - - it("updates title to 'Resume planning session' when count > 0", () => { - renderHeader({ onOpenPlanning: vi.fn(), activePlanningSessionCount: 1 }, "desktop"); - expect(screen.getByTitle("Resume planning session")).toBeDefined(); - expect(screen.queryByTitle("Create a task with AI planning")).toBeNull(); - }); - - it("keeps original title when count is 0", () => { - renderHeader({ onOpenPlanning: vi.fn(), activePlanningSessionCount: 0 }, "desktop"); - expect(screen.getByTitle("Create a task with AI planning")).toBeDefined(); - }); - - it("calls onResumePlanning when clicked with active sessions", () => { - const onResumePlanning = vi.fn(); - const onOpenPlanning = vi.fn(); - renderHeader({ onOpenPlanning, onResumePlanning, activePlanningSessionCount: 2 }, "desktop"); - fireEvent.click(screen.getByTitle("Resume planning session")); - expect(onResumePlanning).toHaveBeenCalled(); - expect(onOpenPlanning).not.toHaveBeenCalled(); - }); - - it("calls onOpenPlanning when clicked with no active sessions", () => { - const onResumePlanning = vi.fn(); - const onOpenPlanning = vi.fn(); - renderHeader({ onOpenPlanning, onResumePlanning, activePlanningSessionCount: 0 }, "desktop"); - fireEvent.click(screen.getByTitle("Create a task with AI planning")); - expect(onOpenPlanning).toHaveBeenCalled(); - expect(onResumePlanning).not.toHaveBeenCalled(); - }); - - it("calls onOpenPlanning when clicked with active sessions but no onResumePlanning", () => { - const onOpenPlanning = vi.fn(); - renderHeader({ onOpenPlanning, activePlanningSessionCount: 1 }, "desktop"); - // Without onResumePlanning, falls back to onOpenPlanning even with active sessions - fireEvent.click(screen.getByTitle("Resume planning session")); - expect(onOpenPlanning).toHaveBeenCalled(); - }); - - it("badge has correct aria-label", () => { - renderHeader({ onOpenPlanning: vi.fn(), activePlanningSessionCount: 2 }, "desktop"); - expect(screen.getByTestId("planning-badge").getAttribute("aria-label")).toBe("2 active planning sessions"); - }); - - it("badge aria-label uses singular for count of 1", () => { - renderHeader({ onOpenPlanning: vi.fn(), activePlanningSessionCount: 1 }, "desktop"); - expect(screen.getByTestId("planning-badge").getAttribute("aria-label")).toBe("1 active planning session"); - }); + it("does not render legacy planning affordances in the header on mobile", () => { + renderHeader({}, "mobile"); + expect(screen.queryByTitle("Create a task with AI planning")).toBeNull(); + expect(screen.queryByTestId("overflow-planning-btn")).toBeNull(); + expect(screen.queryByTestId("overflow-planning-badge")).toBeNull(); }); }); @@ -1073,39 +997,13 @@ describe("Header", () => { expect(screen.getByText("Import from GitHub")).toBeDefined(); }); - it("shows planning in overflow menu on mobile", () => { - renderHeader({ onOpenPlanning: noop }, "mobile"); - fireEvent.click(screen.getByTitle("More header actions")); - expect(screen.getByTestId("overflow-planning-btn")).toBeDefined(); - }); - - it("shows planning badge in overflow menu when activePlanningSessionCount > 0", () => { - renderHeader({ onOpenPlanning: noop, activePlanningSessionCount: 1 }, "mobile"); - fireEvent.click(screen.getByTitle("More header actions")); - expect(screen.getByTestId("overflow-planning-badge")).toBeDefined(); - expect(screen.getByTestId("overflow-planning-badge").textContent).toBe("1"); - }); - - it("does not show planning badge in overflow menu when count is 0", () => { - renderHeader({ onOpenPlanning: noop, activePlanningSessionCount: 0 }, "mobile"); + it("omits planning from the header overflow menu on mobile", () => { + renderHeader({}, "mobile"); fireEvent.click(screen.getByTitle("More header actions")); + expect(screen.queryByTestId("overflow-planning-btn")).toBeNull(); expect(screen.queryByTestId("overflow-planning-badge")).toBeNull(); - }); - - it("calls onResumePlanning from overflow menu when active sessions exist", () => { - const onResumePlanning = vi.fn(); - const onOpenPlanning = vi.fn(); - renderHeader({ onOpenPlanning, onResumePlanning, activePlanningSessionCount: 2 }, "mobile"); - fireEvent.click(screen.getByTitle("More header actions")); - fireEvent.click(screen.getByTestId("overflow-planning-btn")); - expect(onResumePlanning).toHaveBeenCalled(); - expect(onOpenPlanning).not.toHaveBeenCalled(); - }); - - it("shows resume text in overflow menu when active sessions exist", () => { - renderHeader({ onOpenPlanning: noop, activePlanningSessionCount: 1 }, "mobile"); - fireEvent.click(screen.getByTitle("More header actions")); - expect(screen.getByText("Resume planning session (1)")).toBeDefined(); + expect(screen.queryByText("Create a task with AI planning")).toBeNull(); + expect(screen.queryByText("Resume planning session (1)")).toBeNull(); }); it("shows settings in overflow menu on mobile", () => { diff --git a/packages/dashboard/app/components/__tests__/LeftSidebarNav.test.tsx b/packages/dashboard/app/components/__tests__/LeftSidebarNav.test.tsx index 8e935f28a0..857d6bcfb2 100644 --- a/packages/dashboard/app/components/__tests__/LeftSidebarNav.test.tsx +++ b/packages/dashboard/app/components/__tests__/LeftSidebarNav.test.tsx @@ -211,6 +211,7 @@ describe("LeftSidebarNav", () => { "sidebar-nav-list", "sidebar-nav-agents", "sidebar-nav-command-center", + "sidebar-nav-planning", "sidebar-nav-missions", "sidebar-nav-chat", "sidebar-nav-documents", @@ -231,6 +232,11 @@ describe("LeftSidebarNav", () => { } expect(screen.getByTestId("sidebar-nav-documents")).toHaveTextContent("Artifacts"); + expect(screen.getByTestId("sidebar-nav-planning")).toHaveTextContent("Planning"); + + const primaryNav = screen.getByRole("navigation", { name: "Primary navigation" }); + const primaryButtons = within(primaryNav).getAllByRole("button"); + expect(primaryButtons.indexOf(screen.getByTestId("sidebar-nav-planning"))).toBe(primaryButtons.indexOf(screen.getByTestId("sidebar-nav-command-center")) + 1); const sidebar = screen.getByTestId("left-sidebar-nav"); const footer = screen.getByTestId("sidebar-nav-settings").closest(".left-sidebar-nav__footer"); @@ -358,6 +364,7 @@ describe("LeftSidebarNav", () => { it.each<[TaskView, string]>([ ["board", "sidebar-nav-board"], ["research", "sidebar-nav-research"], + ["planning", "sidebar-nav-planning"], ["plugin:fusion-plugin-primary:primary-view", "sidebar-nav-plugin-fusion-plugin-primary-primary-view"], ["plugin:fusion-plugin-overflow:overflow-view", "sidebar-nav-plugin-fusion-plugin-overflow-overflow-view"], ])("highlights active destination %s", (view, testId) => { @@ -528,6 +535,9 @@ describe("LeftSidebarNav", () => { fireEvent.click(screen.getByTestId("sidebar-nav-list")); expect(onChangeView).toHaveBeenCalledWith("list"); + fireEvent.click(screen.getByTestId("sidebar-nav-planning")); + expect(onChangeView).toHaveBeenCalledWith("planning"); + fireEvent.click(screen.getByTestId("sidebar-nav-plugin-fusion-plugin-overflow-overflow-view")); expect(onChangeView).toHaveBeenCalledWith("plugin:fusion-plugin-overflow:overflow-view"); diff --git a/packages/dashboard/app/components/__tests__/PlanningModeModal.planning-flow.test.tsx b/packages/dashboard/app/components/__tests__/PlanningModeModal.planning-flow.test.tsx index 51ed68d962..180228447b 100644 --- a/packages/dashboard/app/components/__tests__/PlanningModeModal.planning-flow.test.tsx +++ b/packages/dashboard/app/components/__tests__/PlanningModeModal.planning-flow.test.tsx @@ -200,6 +200,31 @@ describe("PlanningModeModal", () => { }); }); + describe("embedded presentation", () => { + it("renders as a main-content region without modal overlay or backdrop-close behavior", async () => { + const { container } = render( + + ); + + const region = await screen.findByTestId("planning-view"); + expect(region.getAttribute("role")).toBe("region"); + expect(region.getAttribute("aria-modal")).toBeNull(); + expect(container.querySelector(".modal-overlay")).toBeNull(); + expect(container.querySelector(".planning-modal--embedded")).toBeTruthy(); + + fireEvent.mouseDown(region); + fireEvent.click(region); + expect(mockOnClose).not.toHaveBeenCalled(); + }); + }); + describe("Planning flow", () => { it("starts planning and shows question view", async () => { render( diff --git a/packages/dashboard/app/hooks/useModalManager.ts b/packages/dashboard/app/hooks/useModalManager.ts index 2c3e7b5ec8..b21c0f0e64 100644 --- a/packages/dashboard/app/hooks/useModalManager.ts +++ b/packages/dashboard/app/hooks/useModalManager.ts @@ -199,7 +199,10 @@ export function useModalManager(options: UseModalManagerOptions): ModalManager { groupModalGroupId || settingsOpen || newTaskModalOpen || - isPlanningOpen || + /* + FNXC:Navigation 2026-06-21-00:00: + FN-6886 reuses Planning Mode state only as docked-view payload storage, so it must not make the app behave as though a blocking modal overlay is open. + */ isSubtaskOpen || terminalOpen || filesOpen || diff --git a/packages/dashboard/app/hooks/useViewState.ts b/packages/dashboard/app/hooks/useViewState.ts index 122574a47b..ae32fa031a 100644 --- a/packages/dashboard/app/hooks/useViewState.ts +++ b/packages/dashboard/app/hooks/useViewState.ts @@ -5,7 +5,7 @@ import { getScopedItem, setScopedItem } from "../utils/projectStorage"; import { getPluginViewId, isPluginViewId, isPluginViewRegistered } from "../plugins/pluginViewRegistry"; export type ViewMode = "overview" | "project"; -export type BuiltInTaskView = "board" | "list" | "graph" | "agents" | "missions" | "chat" | "documents" | "research" | "evals" | "goalsView" | "todos" | "skills" | "mailbox" | "insights" | "memory" | "command-center" | "secrets" | "devserver" | "dev-server" | "stash-recovery" | "pull-requests"; +export type BuiltInTaskView = "board" | "list" | "graph" | "agents" | "missions" | "chat" | "documents" | "research" | "evals" | "goalsView" | "todos" | "planning" | "skills" | "mailbox" | "insights" | "memory" | "command-center" | "secrets" | "devserver" | "dev-server" | "stash-recovery" | "pull-requests"; export type PluginTaskView = `plugin:${string}:${string}`; export type TaskView = BuiltInTaskView | PluginTaskView; @@ -25,6 +25,11 @@ const BUILT_IN_TASK_VIEWS: readonly BuiltInTaskView[] = [ FN-6829 promotes project Todos from modal-only state into the persisted built-in task-view registry so dashboard navigation can dock it in the right content area. */ "todos", + /* + FNXC:Navigation 2026-06-21-00:00: + FN-6886 promotes Planning Mode into a persisted top-level docked task view instead of treating it as a modal-only overlay. + */ + "planning", "skills", "mailbox",