@@ -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",