FN-8352: promote ideation to top-level navigation

Make ideation independently accessible from desktop and mobile navigation.

- Add an Ideation view backed by the Command Center panel and view-state routing.
- Surface Ideation in desktop/sidebar and mobile primary navigation, with coverage for navigation and rendering.
- Document the experimental setting and add a minor changeset.

Files changed:
 .changeset/fn-8352-ideation-top-level-view.md      |  7 ++++
 docs/dashboard-guide.md                            | 14 +++++++
 docs/settings-reference.md                         |  3 +-
 .../src/__tests__/mobile-nav-primary-items.test.ts | 10 ++++-
 packages/core/src/mobile-nav-primary-items.ts      | 17 +++++++-
 packages/dashboard/app/App.tsx                     | 16 ++++++-
 packages/dashboard/app/components/Header.tsx       | 21 +++++++--
 .../dashboard/app/components/LeftSidebarNav.tsx    |  8 +++-
 packages/dashboard/app/components/MobileNavBar.tsx |  2 +
 .../dashboard/app/components/SettingsModal.tsx     |  6 +++
 .../app/components/__tests__/App.test.tsx          | 20 +++++++++
 .../app/components/__tests__/Header.test.tsx       | 15 +++++++
 .../components/__tests__/LeftSidebarNav.test.tsx   |  5 +++
 .../app/components/__tests__/MobileNavBar.test.tsx | 43 +++++++++++++++---
 .../components/command-center/CommandCenter.tsx    | 12 +++---
 .../__tests__/CommandCenter.test.tsx               |  7 ++--
 .../__tests__/IdeationPanel.test.tsx               | 49 ++++++++++++++++++++++
 .../app/components/dashboard/MainContent.tsx       | 13 ++++++
 .../dashboard/app/components/dashboard/types.ts    |  1 +
 .../settings/sections/GeneralSection.tsx           | 10 ++---
 packages/dashboard/app/hooks/useViewState.ts       |  8 +++-
 21 files changed, 256 insertions(+), 31 deletions(-)

Fusion-Task-Id: FN-8352
Fusion-Task-Lineage: e7791119-2fe5-4e68-b669-0e5bd8df9b49
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-18 19:18:57 -07:00
parent 482dbe77a4
commit 12aee016ee
21 changed files with 256 additions and 31 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": minor
---
summary: Ideation is now a top-level experimental sidebar/mobile view instead of a Command Center tab.
category: feature
dev: `experimentalFeatures.ideationView` is default-off; mobile keeps Ideation More-only, redirects disabled deep links to Board, and removes the Command Center tab.

View File

@@ -1024,6 +1024,20 @@ Navigation:
For the full research workflow, provider setup, CLI commands, API reference, and agent integration, see the canonical [Research guide](./research.md).
## Ideation View
<!-- FNXC:Navigation 2026-08-01-00:00: FN-8352 promotes Ideation from Command Center into one default-off experimental top-level destination. Desktop uses the sidebar (or Header More fallback when the sidebar is opted out), while mobile always keeps it in More rather than a configurable footer tab. -->
Ideation is a standalone dashboard surface for capturing divergent candidates and converging one into the Mission hierarchy.
> Available only when `experimentalFeatures.ideationView` is enabled. The flag is off by default and is managed in **Settings → Experimental Features**.
Navigation:
- Desktop/tablet: **Left sidebar → Ideation**, or the Header **More views** menu when the desktop sidebar is opted out
- Mobile: **More** sheet → **Ideation**
- Mobile footer customization never promotes Ideation to a primary tab, including when older persisted settings list it
The view replaces the former **Command Center → Ideation** tab. It preserves the same empty, populated, converged, and request-error states, so sessions and Mission handoff evidence remain in one canonical surface.
## Files Modal
The Files modal provides a workspace-aware file browser and editor.

View File

@@ -165,7 +165,7 @@ Fusion automatically falls back to ntfy's JSON publish format when a notificatio
| `researchGlobalMaxSearchResults` | `number` | `undefined` | Maximum search results per provider query. |
| `researchGlobalFetchTimeoutMs` | `number` | `30000` | Timeout for individual HTTP fetches in milliseconds. |
| `researchGlobalUserAgent` | `string` | `"FusionResearchBot/1.0"` | User-Agent header for HTTP requests made by research providers. |
| `experimentalFeatures` | `Record<string, boolean>` | `{}` | Global-scoped experimental feature flags. Includes `experimentalFeatures.researchView`, which gates all Research surfaces and tools (dashboard view, engine task-session tools, and CLI `fn_research_*` tools), and `experimentalFeatures.evalsView`, which gates Evals surfaces (dashboard view, Settings → Scheduled Evals, and scheduled-eval cron execution). |
| `experimentalFeatures` | `Record<string, boolean>` | `{}` | Global-scoped experimental feature flags. Includes `experimentalFeatures.researchView`, which gates all Research surfaces and tools (dashboard view, engine task-session tools, and CLI `fn_research_*` tools); `experimentalFeatures.evalsView`, which gates Evals surfaces (dashboard view, Settings → Scheduled Evals, and scheduled-eval cron execution); and default-off `experimentalFeatures.ideationView`, which gates the top-level Ideation view (desktop sidebar/Header fallback and mobile More only). |
| `remoteAccess` | `RemoteAccessSettings` | `{ activeProvider: null, providers: {...}, tokenStrategy: {...}, lifecycle: {...} }` | Global-scoped remote access provider + token strategy configuration used by Remote Access routes and tunnel lifecycle controls. |
| `mcpServers` | `McpServersSettings` | `{ enabled: false, servers: [] }` | Global MCP server declarations shared across projects. Project `mcpServers` can enable/disable the effective set, override a same-named global server, or disable a global server with a same-named `enabled:false` entry. Sensitive env/header/token values must be `{ secretRef, scope }` references to Fusion-managed secrets, never plaintext. |
| `worktrunk` | `WorktrunkSettings` | `{ enabled: false, binaryPath: undefined, installedBinaryPath: undefined, onFailure: "fail" }` | Global defaults for worktrunk integration. Merged field-by-field with project `worktrunk` values; project values override global values for matching fields. |
@@ -1567,6 +1567,7 @@ Common built-in dashboard/runtime flags include:
- `todoView` (enables dashboard Todo View; see [Todo View](./todo-view.md))
- `researchView`
- `evalsView` (gates Evals dashboard view, Settings → Scheduled Evals section, and scheduled-eval cron execution)
- `ideationView` (default off; gates the top-level Ideation view, which is mobile More-only and replaces the Command Center Ideation tab)
- `workflowGraphExecutor` (enables the workflow-IR interpreter path)
- `graphNativePostMerge` (**default-ON**; the graph is the sole owner of post-merge `optional-group` steps after a successful merge — the legacy merger-owned post-merge path was deleted. Post-merge failures are non-blocking. See [Workflow Steps → Execution Phases](./workflow-steps.md#execution-phases))
- `workflowInterpreterDualObserve` (retired/inert; stale persisted `true` values are forced OFF and must not reactivate hidden shadow observation)

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { DEFAULT_MOBILE_NAV_PRIMARY_ITEMS, resolveMobileNavPrimaryItems } from "../mobile-nav-primary-items.js";
import { DEFAULT_MOBILE_NAV_PRIMARY_ITEMS, MOBILE_NAV_SELECTABLE_ITEMS, resolveMobileNavPrimaryItems } from "../mobile-nav-primary-items.js";
describe("resolveMobileNavPrimaryItems", () => {
it("uses the existing six-tab order for unset or empty values", () => {
@@ -14,6 +14,14 @@ describe("resolveMobileNavPrimaryItems", () => {
expect(resolved.omittedItems).toContain("settings");
});
it("keeps Ideation in More when stale settings try to promote it", () => {
const resolved = resolveMobileNavPrimaryItems({ mobileNavPrimaryItems: ["ideation"] });
expect(MOBILE_NAV_SELECTABLE_ITEMS).toContain("ideation");
expect(resolved.primaryItems).not.toContain("ideation");
expect(resolved.omittedItems).toContain("ideation");
});
it("keeps newly eligible settings and documents, drops overflow-only ids, deduplicates, and clamps footer tabs", () => {
const resolved = resolveMobileNavPrimaryItems({
mobileNavPrimaryItems: ["settings", "tasks", "more", "documents", "tasks", "agents", "missions", "chat", "mailbox", "planning", "unknown"],

View File

@@ -1,6 +1,6 @@
import type { ProjectSettings } from "./types.js";
/** The only destinations that may be promoted into the mobile footer. */
/** Destinations represented by the mobile navigation registry. */
export const MOBILE_NAV_SELECTABLE_ITEMS = [
"command-center",
"tasks",
@@ -25,6 +25,13 @@ export const MOBILE_NAV_SELECTABLE_ITEMS = [
"memory",
"research",
"evals",
/*
FNXC:Navigation 2026-08-01-00:00:
FN-8352 makes Ideation a top-level experimental destination, but it remains
More-only on mobile. Keep it registered for labels and More rendering while
excluding it from the footer-promotion registry below.
*/
"ideation",
"goals",
"todos",
"dev-server",
@@ -57,11 +64,17 @@ export const MOBILE_NAV_SELECTABLE_ITEM_LABEL_KEYS: Record<MobileNavSelectableIt
memory: "nav.memory",
research: "nav.research",
evals: "nav.evals",
ideation: "nav.ideation",
goals: "nav.goals",
todos: "nav.todos",
"dev-server": "nav.devServer",
};
/** Destinations that Settings may promote into the mobile footer. */
export const MOBILE_NAV_PRIMARY_SELECTABLE_ITEMS = MOBILE_NAV_SELECTABLE_ITEMS.filter(
(item): item is Exclude<MobileNavSelectableItem, "ideation"> => item !== "ideation",
);
export const DEFAULT_MOBILE_NAV_PRIMARY_ITEMS: MobileNavSelectableItem[] = [
"command-center", "tasks", "agents", "missions", "chat", "mailbox",
];
@@ -81,7 +94,7 @@ the render layer suppresses disabled experimental destinations, and More remains
*/
export function resolveMobileNavPrimaryItems(settings?: Pick<ProjectSettings, "mobileNavPrimaryItems">): ResolvedMobileNavPrimaryItems {
const selected = Array.isArray(settings?.mobileNavPrimaryItems) ? settings.mobileNavPrimaryItems : [];
const valid = new Set<string>(MOBILE_NAV_SELECTABLE_ITEMS);
const valid = new Set<string>(MOBILE_NAV_PRIMARY_SELECTABLE_ITEMS);
const primaryItems = selected.reduce<MobileNavSelectableItem[]>((items, id) => {
if (valid.has(id) && !items.includes(id as MobileNavSelectableItem) && items.length < MAX_MOBILE_NAV_PRIMARY_ITEMS) {
items.push(id as MobileNavSelectableItem);

View File

@@ -801,6 +801,7 @@ function AppInner() {
const nodesEnabled = experimentalFeatures.nodesView === true;
const researchEnabled = experimentalFeatures.researchView === true;
const evalsEnabled = experimentalFeatures.evalsView === true;
const ideationEnabled = experimentalFeatures.ideationView === true;
/* FNXC:QuickAddSubtaskFlag 2026-06-21-00:00: Missing or false `subtaskBreakdown` settings must hide the AI Subtask quick-add handoff across List, Board, and New Task Modal surfaces; only an explicit true wires the callback. */
const subtaskBreakdownEnabled = experimentalFeatures.subtaskBreakdown === true;
/*
@@ -863,13 +864,22 @@ function AppInner() {
if (taskView === "evals" && !evalsEnabled) {
handleChangeTaskView("board");
}
/*
FNXC:Navigation 2026-08-01-00:00:
FN-8352 promotes Ideation to a default-off experimental top-level view.
Redirect persisted and deep-linked disabled views to Board so MainContent
never leaves users on a blank unavailable surface.
*/
if (taskView === "ideation" && !ideationEnabled) {
handleChangeTaskView("board");
}
if (taskView === "goalsView" && !goalsEnabled) {
handleChangeTaskView("board");
}
if (taskView === "todos" && !todosEnabled) {
handleChangeTaskView("board");
}
}, [taskView, settingsLoaded, skillsEnabled, insightsEnabled, handleChangeTaskView, agentsEnabled, memoryEnabled, devServerEnabled, researchEnabled, evalsEnabled, goalsEnabled, todosEnabled, graphPluginTaskView]);
}, [taskView, settingsLoaded, skillsEnabled, insightsEnabled, handleChangeTaskView, agentsEnabled, memoryEnabled, devServerEnabled, researchEnabled, evalsEnabled, ideationEnabled, goalsEnabled, todosEnabled, graphPluginTaskView]);
const {
availableModels,
@@ -1518,6 +1528,7 @@ function AppInner() {
openSettingsWithNav,
researchReadinessVersion,
evalsEnabled,
ideationEnabled,
memoryEnabled,
goalsEnabled,
handleOpenMission,
@@ -1729,6 +1740,7 @@ function AppInner() {
devServerView: devServerEnabled,
researchView: researchEnabled,
evalsView: evalsEnabled,
ideationView: ideationEnabled,
goalsView: goalsEnabled,
leftSidebarNav: leftSidebarNavEnabled,
rightDock: rightDockEnabled,
@@ -1763,6 +1775,7 @@ function AppInner() {
devServerView: devServerEnabled,
researchView: researchEnabled,
evalsView: evalsEnabled,
ideationView: ideationEnabled,
goalsView: goalsEnabled,
}}
pluginDashboardViews={pluginDashboardViews}
@@ -1850,6 +1863,7 @@ function AppInner() {
todoView: todosEnabled,
researchView: researchEnabled,
evalsView: evalsEnabled,
ideationView: ideationEnabled,
goalsView: goalsEnabled,
}}
pluginDashboardViews={pluginDashboardViews}

View File

@@ -1,6 +1,6 @@
import { useState, useEffect, useRef, useCallback, useMemo, type ReactNode } from "react";
import { useTranslation } from "react-i18next";
import { Settings, LayoutGrid, List, Search, X, Activity, MoreHorizontal, Clock, Folder, History, GitBranch, Monitor, Workflow, Bot, Target, Grid3X3, Mail, MessageSquare, Check, Zap, Sparkles, FileText, Brain, CheckSquare, Lock, Gauge, ChevronDown, ChevronRight, PanelRight } from "lucide-react";
import { Settings, LayoutGrid, List, Search, X, Activity, MoreHorizontal, Clock, Folder, History, GitBranch, Monitor, Workflow, Bot, Target, Grid3X3, Mail, MessageSquare, Check, Zap, Sparkles, FileText, Brain, CheckSquare, Lock, Gauge, Lightbulb, ChevronDown, ChevronRight, PanelRight } from "lucide-react";
import "./Header.css";
// ProjectSelector styles used by the imported standalone component.
import "./ProjectSelector.css";
@@ -132,7 +132,7 @@ export interface HeaderProps {
/** Whether the current view is a remote node */
isRemote?: boolean;
/** Experimental feature flags controlling visibility of nav items. */
experimentalFeatures?: { insights?: boolean; memoryView?: boolean; devServer?: boolean; devServerView?: boolean; researchView?: boolean; evalsView?: boolean; goalsView?: boolean; leftSidebarNav?: boolean; rightDock?: boolean };
experimentalFeatures?: { insights?: boolean; memoryView?: boolean; devServer?: boolean; devServerView?: boolean; researchView?: boolean; evalsView?: boolean; ideationView?: boolean; goalsView?: boolean; leftSidebarNav?: boolean; rightDock?: boolean };
pluginDashboardViews?: PluginDashboardViewEntry[];
shellConnectionControl?: ReactNode;
}
@@ -236,6 +236,7 @@ export function Header({
return !!(
onChangeView ||
experimentalFeatures?.researchView ||
experimentalFeatures?.ideationView ||
todosEnabled ||
experimentalFeatures?.insights ||
@@ -784,7 +785,7 @@ export function Header({
<>
<button
ref={viewOverflowTriggerRef}
className={`view-toggle-btn${(["research", "skills", "insights", "memory", "secrets", "dev-server", "devserver", "graph", "todos"].includes(view) || (isTablet && view === "documents") || (experimentalFeatures?.evalsView && view === "evals") || (experimentalFeatures?.goalsView && view === "goalsView") || isPluginViewId(view)) ? " active" : ""}`}
className={`view-toggle-btn${(["research", "ideation", "skills", "insights", "memory", "secrets", "dev-server", "devserver", "graph", "todos"].includes(view) || (isTablet && view === "documents") || (experimentalFeatures?.evalsView && view === "evals") || (experimentalFeatures?.goalsView && view === "goalsView") || isPluginViewId(view)) ? " active" : ""}`}
onClick={() => {
setIsViewOverflowOpen((prev) => !prev);
}}
@@ -845,6 +846,20 @@ export function Header({
<span>{t("header.researchView", "Research")}</span>
</button>
)}
{experimentalFeatures?.ideationView && (
<button
className={`view-toggle-overflow-item${view === "ideation" ? " active" : ""}`}
onClick={() => {
onChangeView("ideation");
setIsViewOverflowOpen(false);
}}
role="menuitem"
data-testid="view-overflow-ideation"
>
<Lightbulb size={14} />
<span>{t("nav.ideation", "Ideation")}</span>
</button>
)}
{experimentalFeatures?.insights && (
<button
className={`view-toggle-overflow-item${view === "insights" ? " active" : ""}`}

View File

@@ -40,6 +40,7 @@ export interface LeftSidebarExperimentalFeatures {
devServerView?: boolean;
researchView?: boolean;
evalsView?: boolean;
ideationView?: boolean;
goalsView?: boolean;
}
@@ -279,11 +280,11 @@ export function LeftSidebarNav({
/*
FNXC:Navigation 2026-06-22-12:00:
Single explicit sidebar order (top to bottom): board, list, graph, agents, chat, mailbox, planning, missions, goals, compound, automation, import, workflows, insight, research, command-center, documents (Artifacts), skills, memory, evals, then any remaining plugin views in their sorted order.
Single explicit sidebar order (top to bottom): board, list, graph, agents, chat, mailbox, planning, missions, goals, compound, automation, import, workflows, insight, research, ideation, command-center, documents (Artifacts), skills, memory, evals, then any remaining plugin views in their sorted order.
Dev Server is intentionally absent: it moved to the right dock. Secrets and Todos remain omitted (they live in the right dock / mobile More-sheet / Header overflow).
Flag gates preserved verbatim from the prior layout: agents (showAgentsTab), goals (goalsView), insight (insights), research (researchView), skills (showSkillsTab), memory (memoryView), evals (evalsView). graph and compound are skipped when their plugin view is absent.
Flag gates preserved verbatim from the prior layout: agents (showAgentsTab), goals (goalsView), insight (insights), research (researchView), ideation (ideationView), skills (showSkillsTab), memory (memoryView), evals (evalsView). graph and compound are skipped when their plugin view is absent.
FNXC:Navigation 2026-06-22-18:50:
Roadmaps is no longer a dashboard navigation destination. Keep filtering it out even if a persisted plugin dashboard-view row is present, while preserving other plugin views in their sorted fallback section.
@@ -443,6 +444,9 @@ export function LeftSidebarNav({
...(experimentalFeatures?.researchView
? [{ id: "research", label: t("header.researchView", "Research"), view: "research" as TaskView, isActive: view === "research", icon: Search, testId: "sidebar-nav-research", onSelect: () => onChangeView("research") }]
: []),
...(experimentalFeatures?.ideationView
? [{ id: "ideation", label: t("nav.ideation", "Ideation"), view: "ideation" as TaskView, isActive: view === "ideation", icon: Lightbulb, testId: "sidebar-nav-ideation", onSelect: () => onChangeView("ideation") }]
: []),
...(experimentalFeatures?.evalsView
? [{ id: "evals", label: t("header.evalsView", "Evals"), view: "evals" as TaskView, isActive: view === "evals", icon: Target, testId: "sidebar-nav-evals", onSelect: () => onChangeView("evals") }]
: []),

View File

@@ -113,6 +113,7 @@ export interface MobileNavBarProps {
todoView?: boolean;
researchView?: boolean;
evalsView?: boolean;
ideationView?: boolean;
goalsView?: boolean;
};
pluginDashboardViews?: PluginDashboardViewEntry[];
@@ -440,6 +441,7 @@ export function MobileNavBar({
memory: { icon: <Brain />, labelKey: "nav.memory", fallback: "Memory", moreTestId: "mobile-more-item-memory", isActive: view === "memory", isAvailable: Boolean(experimentalFeatures?.memoryView), navigate: (surface) => surface === "primary" ? onChangeView("memory") : handleMoreAction(() => onChangeView("memory")) },
research: { icon: <Search />, labelKey: "nav.research", fallback: "Research", moreTestId: "mobile-more-item-research", isActive: view === "research", isAvailable: Boolean(experimentalFeatures?.researchView), navigate: (surface) => surface === "primary" ? onChangeView("research") : handleMoreAction(() => onChangeView("research")) },
evals: { icon: <Target />, labelKey: "nav.evals", fallback: "Evals", moreTestId: "mobile-more-item-evals", isActive: view === "evals", isAvailable: Boolean(experimentalFeatures?.evalsView), navigate: (surface) => surface === "primary" ? onChangeView("evals") : handleMoreAction(() => onChangeView("evals")) },
ideation: { icon: <Lightbulb />, labelKey: "nav.ideation", fallback: "Ideation", moreTestId: "mobile-more-item-ideation", isActive: view === "ideation", isAvailable: Boolean(experimentalFeatures?.ideationView), navigate: (surface) => surface === "primary" ? onChangeView("ideation") : handleMoreAction(() => onChangeView("ideation")) },
goals: { icon: <Target />, labelKey: "nav.goals", fallback: "Goals", moreTestId: "mobile-more-item-goals", isActive: view === "goalsView", isAvailable: Boolean(experimentalFeatures?.goalsView), navigate: (surface) => surface === "primary" ? onChangeView("goalsView") : handleMoreAction(() => onChangeView("goalsView")) },
todos: { icon: <CheckSquare />, labelKey: "nav.todos", fallback: "Todos", moreTestId: "mobile-more-item-todos", isActive: view === "todos", isAvailable: todoViewEnabled, navigate: (surface) => surface === "primary" ? onChangeView("todos") : handleMoreAction(() => onChangeView("todos")) },
"dev-server": { icon: <Monitor />, labelKey: "nav.devServer", fallback: "Dev Server", moreTestId: "mobile-more-item-dev-server", isActive: view === "dev-server" || view === "devserver", isAvailable: Boolean(experimentalFeatures?.devServerView), navigate: (surface) => surface === "primary" ? onChangeView("dev-server") : handleMoreAction(() => onChangeView("dev-server")) },

View File

@@ -682,6 +682,12 @@ const KNOWN_EXPERIMENTAL_FEATURES: Record<string, string> = {
todoView: "Todo List",
researchView: "Research View",
evalsView: "Evals View",
/*
FNXC:SettingsExperimental 2026-08-01-00:00:
FN-8352 promotes Ideation to a default-off top-level view. Keep its toggle
visible so operators explicitly opt into the sidebar and mobile More surface.
*/
ideationView: "Ideation View",
qualityPlugin: "Quality Plugin",
goalsView: "Goals View",
/* FNXC:QuickAddSubtaskFlag 2026-06-21-00:00: The AI subtask-breakdown quick-add affordance is exposed only through this default-off experimental flag so missing settings keep every quick-add Subtask button hidden. */

View File

@@ -2349,6 +2349,26 @@ describe("App view switching", () => {
localStorage.removeItem(taskViewStorageKey());
});
it("falls back to Board when persisted Ideation view is feature-disabled", async () => {
localStorage.setItem("kb-dashboard-view-mode", "project");
localStorage.setItem(taskViewStorageKey(), "ideation");
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
...defaultSettings,
experimentalFeatures: {
...defaultSettings.experimentalFeatures,
ideationView: false,
},
});
render(<App />);
await waitFor(() => expect(document.querySelector(".board")).toBeTruthy());
expect(screen.queryByLabelText("Persisted ideation")).not.toBeInTheDocument();
localStorage.removeItem("kb-dashboard-view-mode");
localStorage.removeItem(taskViewStorageKey());
});
it("falls back to board when evals view is feature-disabled", async () => {
localStorage.setItem("kb-dashboard-view-mode", "project");
localStorage.setItem(taskViewStorageKey(), "evals");

View File

@@ -430,6 +430,21 @@ describe("Header", () => {
expect(screen.queryByTestId("view-overflow-research")).toBeNull();
});
it("gates Ideation in the desktop overflow and routes the enabled fallback", () => {
const hidden = renderHeader({ onChangeView: noop, experimentalFeatures: { ideationView: false } });
fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
expect(screen.queryByTestId("view-overflow-ideation")).toBeNull();
hidden.unmount();
const onChangeView = vi.fn();
renderHeader({ onChangeView, view: "ideation", experimentalFeatures: { ideationView: true } });
const trigger = screen.getByTestId("view-toggle-overflow-trigger");
expect(trigger).toHaveClass("active");
fireEvent.click(trigger);
fireEvent.click(screen.getByTestId("view-overflow-ideation"));
expect(onChangeView).toHaveBeenCalledWith("ideation");
});
it("hides evals in the desktop view overflow when evalsView is disabled", () => {
renderHeader({ onChangeView: noop, experimentalFeatures: { evalsView: false } });

View File

@@ -111,6 +111,7 @@ function renderSidebar(overrides: Partial<ComponentProps<typeof LeftSidebarNav>>
devServerView: true,
researchView: true,
evalsView: true,
ideationView: true,
goalsView: true,
},
pluginDashboardViews: pluginViews,
@@ -224,6 +225,7 @@ describe("LeftSidebarNav", () => {
"sidebar-nav-workflows",
"sidebar-nav-insights",
"sidebar-nav-research",
"sidebar-nav-ideation",
"sidebar-nav-skills",
"sidebar-nav-memory",
"sidebar-nav-evals",
@@ -287,6 +289,7 @@ describe("LeftSidebarNav", () => {
"sidebar-nav-workflows",
"sidebar-nav-insights",
"sidebar-nav-research",
"sidebar-nav-ideation",
"sidebar-nav-evals",
];
const orderedIndices = orderedTestIds.map((testId) => primaryButtons.indexOf(screen.getByTestId(testId)));
@@ -352,6 +355,7 @@ describe("LeftSidebarNav", () => {
expect(screen.queryByTestId("sidebar-nav-stash-recovery")).toBeNull();
expect(screen.queryByTestId("sidebar-nav-agents")).toBeNull();
expect(screen.queryByTestId("sidebar-nav-research")).toBeNull();
expect(screen.queryByTestId("sidebar-nav-ideation")).toBeNull();
expect(screen.queryByTestId("sidebar-nav-insights")).toBeNull();
expect(screen.queryByTestId("sidebar-nav-skills")).toBeNull();
expect(screen.queryByTestId("sidebar-nav-memory")).toBeNull();
@@ -458,6 +462,7 @@ describe("LeftSidebarNav", () => {
it.each<[TaskView, string]>([
["board", "sidebar-nav-board"],
["research", "sidebar-nav-research"],
["ideation", "sidebar-nav-ideation"],
["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"],

View File

@@ -170,16 +170,30 @@ describe("MobileNavBar", () => {
});
it("renders every available selectable destination exactly once across tab and More", () => {
const gated = new Set(["skills", "insights", "memory", "research", "evals", "goals", "todos", "dev-server"]);
const gated = new Set(["skills", "insights", "memory", "research", "evals", "ideation", "goals", "todos", "dev-server"]);
const moreTestIds: Record<string, string> = { automation: "schedules", "github-import": "github", workflows: "workflow" };
for (const item of MOBILE_NAV_SELECTABLE_ITEMS) {
const { unmount } = render(<MobileNavBar {...createDefaultProps()} mobileNavPrimaryItems={[item]} showSkillsTab experimentalFeatures={{ insights: true, memoryView: true, researchView: true, evalsView: true, goalsView: true, todoView: true, devServerView: true }} />);
expect(screen.getAllByTestId(`mobile-nav-tab-${item}`)).toHaveLength(1);
fireEvent.click(screen.getByTestId("mobile-nav-tab-more"));
expect(screen.queryByTestId(`mobile-more-item-${moreTestIds[item] ?? item}`)).toBeNull();
const { unmount } = render(<MobileNavBar {...createDefaultProps()} mobileNavPrimaryItems={[item]} showSkillsTab experimentalFeatures={{ insights: true, memoryView: true, researchView: true, evalsView: true, ideationView: true, goalsView: true, todoView: true, devServerView: true }} />);
if (item === "ideation") {
expect(screen.queryByTestId("mobile-nav-tab-ideation")).toBeNull();
fireEvent.click(screen.getByTestId("mobile-nav-tab-more"));
expect(screen.getAllByTestId("mobile-more-item-ideation")).toHaveLength(1);
} else {
expect(screen.getAllByTestId(`mobile-nav-tab-${item}`)).toHaveLength(1);
fireEvent.click(screen.getByTestId("mobile-nav-tab-more"));
expect(screen.queryByTestId(`mobile-more-item-${moreTestIds[item] ?? item}`)).toBeNull();
}
unmount();
}
expect(gated.size).toBe(8);
expect(gated.size).toBe(9);
});
it("keeps enabled Ideation in More when persisted customization lists it", () => {
render(<MobileNavBar {...createDefaultProps()} mobileNavPrimaryItems={["ideation"]} experimentalFeatures={{ ideationView: true }} />);
expect(screen.queryByTestId("mobile-nav-tab-ideation")).toBeNull();
fireEvent.click(screen.getByTestId("mobile-nav-tab-more"));
expect(screen.getAllByTestId("mobile-more-item-ideation")).toHaveLength(1);
});
it("does not render legacy roadmaps tab", () => {
@@ -799,6 +813,23 @@ describe("MobileNavBar", () => {
expect(screen.getByTestId("mobile-more-item-research")).toBeDefined();
});
it("shows Ideation in More only when ideationView is enabled and routes to it", () => {
const props = createDefaultProps();
const { container } = render(<MobileNavBar {...props} experimentalFeatures={{ ideationView: true }} />);
fireEvent.click(screen.getByTestId("mobile-nav-tab-more"));
fireEvent.click(screen.getByTestId("mobile-more-item-ideation"));
expect(container.querySelector(".mobile-more-sheet")).toBeNull();
expect(props.onChangeView).toHaveBeenCalledWith("ideation");
});
it("does not show Ideation in more sheet when ideationView is disabled", () => {
render(<MobileNavBar {...createDefaultProps()} experimentalFeatures={{ ideationView: false }} />);
fireEvent.click(screen.getByTestId("mobile-nav-tab-more"));
expect(screen.queryByTestId("mobile-more-item-ideation")).toBeNull();
});
it("does not show research in more sheet when experimentalFeatures.researchView is false", () => {
render(<MobileNavBar {...createDefaultProps()} experimentalFeatures={{ researchView: false }} />);
fireEvent.click(screen.getByTestId("mobile-nav-tab-more"));

View File

@@ -22,7 +22,6 @@ import { SystemStatsArea } from "./areas/SystemStatsArea";
import { SystemControlsArea } from "./areas/SystemControlsArea";
import { PluginManager } from "../PluginManager";
import { MissionControlPanel } from "./MissionControlPanel";
import { IdeationPanel } from "./IdeationPanel";
import { CommandCenterControls } from "./CommandCenterControls";
import { ReliabilityView } from "../ReliabilityView";
import { NodesView } from "../NodesView";
@@ -54,8 +53,7 @@ type SubViewId =
| "plugins"
| "nodes"
| "reliability"
| "mission-control"
| "ideation";
| "mission-control";
interface SubView {
id: SubViewId;
@@ -100,8 +98,12 @@ function useSubViews(nodesEnabled: boolean): SubView[] {
{ id: "plugins", label: t("commandCenter.tabs.plugins", "Plugins") },
...(nodesEnabled ? [{ id: "nodes" as const, label: t("commandCenter.tabs.nodes", "Nodes") }] : []),
{ id: "reliability", label: t("commandCenter.tabs.reliability", "Reliability") },
/*
FNXC:Navigation 2026-08-01-00:00:
FN-8352 removes Ideation from Command Center because its experimental
top-level navigation view is now the single canonical host.
*/
{ id: "mission-control", label: t("commandCenter.tabs.missionControl", "Mission Control") },
{ id: "ideation", label: t("commandCenter.tabs.ideation", "Ideation") },
];
}
@@ -707,8 +709,6 @@ export function CommandCenter({
return <ReliabilityView projectId={projectId} />;
case "mission-control":
return <MissionControlPanel projectId={projectId} />;
case "ideation":
return <IdeationPanel projectId={projectId} />;
default:
return <PlaceholderTab tabId={activeTab} />;
}

View File

@@ -1039,8 +1039,9 @@ describe("CommandCenter shell", () => {
render(<CommandCenter />);
const tablist = screen.getByRole("tablist");
const tabs = within(tablist).getAllByRole("tab");
// Overview, Tokens, Tools, Activity, Productivity, Team, Workflows, Ecosystem, GitHub, GitLab, Signals, System, Plugins, Reliability, Mission Control, Ideation.
expect(tabs.length).toBe(17);
// Overview, Tokens, Tools, Activity, Productivity, Team, Workflows, Ecosystem, GitHub, GitLab, Signals, System, Plugins, Reliability, Mission Control.
expect(tabs.length).toBe(16);
expect(screen.queryByTestId("command-center-tab-ideation")).toBeNull();
expect(screen.queryByTestId("command-center-tab-nodes")).toBeNull();
// roving tabindex: exactly one tab is focusable.
const focusable = tabs.filter((tab) => tab.getAttribute("tabindex") === "0");
@@ -1294,7 +1295,7 @@ describe("CommandCenter shell", () => {
const overviewTab = screen.getByTestId("command-center-tab-overview");
overviewTab.focus();
fireEvent.keyDown(overviewTab, { key: "ArrowLeft" });
const last = screen.getByTestId("command-center-tab-ideation");
const last = screen.getByTestId("command-center-tab-mission-control");
expect(last.getAttribute("aria-selected")).toBe("true");
expect(document.activeElement).toBe(last);
});

View File

@@ -0,0 +1,49 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
import { IdeationPanel } from "../IdeationPanel";
function jsonResponse(value: unknown): Response {
return { ok: true, json: async () => value } as Response;
}
describe("IdeationPanel", () => {
afterEach(() => {
vi.unstubAllGlobals();
});
it("renders the empty session state", async () => {
vi.stubGlobal("fetch", vi.fn().mockResolvedValue(jsonResponse([])));
render(<IdeationPanel projectId="project-1" />);
expect(await screen.findByText("No sessions yet.")).toBeInTheDocument();
});
it("renders populated and converged session details", async () => {
const session = {
id: "idea-1",
title: "Improve navigation",
status: "converged",
targetMissionId: "M-001",
candidates: [{ id: "candidate-1", content: "Use a More destination", origin: "human" }],
};
vi.stubGlobal("fetch", vi.fn()
.mockResolvedValueOnce(jsonResponse([{ id: session.id }]))
.mockResolvedValueOnce(jsonResponse(session)));
render(<IdeationPanel projectId="project-1" />);
expect(await screen.findByRole("heading", { name: session.title })).toBeInTheDocument();
expect(screen.getByText("Converged to Mission")).toBeInTheDocument();
expect(screen.getByText("M-001")).toBeInTheDocument();
expect(screen.getByText("Use a More destination")).toBeInTheDocument();
});
it("exposes request errors as an alert", async () => {
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: false, text: async () => "Ideation unavailable" }));
render(<IdeationPanel projectId="project-1" />);
await waitFor(() => expect(screen.getByRole("alert")).toHaveTextContent("Ideation unavailable"));
});
});

View File

@@ -11,6 +11,7 @@ import { TaskDetailContent } from "../TaskDetailModal";
import { ProjectOverview } from "../ProjectOverview";
import { MissionManager } from "../MissionManager";
import { MailboxView } from "../MailboxView";
import { IdeationPanel } from "../command-center/IdeationPanel";
import { PageErrorBoundary } from "../ErrorBoundary";
import { BackendConnectionErrorPage } from "../BackendConnectionErrorPage";
import { CapacityRiskBanner } from "../CapacityRiskBanner";
@@ -103,6 +104,7 @@ export function MainContent({
openSettingsWithNav,
researchReadinessVersion,
evalsEnabled,
ideationEnabled,
memoryEnabled,
goalsEnabled,
handleOpenMission,
@@ -526,6 +528,17 @@ export function MainContent({
);
}
if (taskView === "ideation") {
if (!settingsLoaded || !ideationEnabled) {
return null;
}
return (
<PageErrorBoundary>
<IdeationPanel projectId={currentProject?.id} />
</PageErrorBoundary>
);
}
if (taskView === "memory") {
if (!settingsLoaded || !memoryEnabled) {
return null;

View File

@@ -147,6 +147,7 @@ export interface MainContentProps {
openSettingsWithNav: (section?: SectionId) => void;
researchReadinessVersion: number;
evalsEnabled: boolean;
ideationEnabled: boolean;
memoryEnabled: boolean;
goalsEnabled: boolean;
handleOpenMission: (missionId: string) => void;

View File

@@ -1,6 +1,6 @@
import { useEffect, useMemo, useState } from "react";
import { DEPRECATED_BUILTIN_WORKFLOW_IDS, isLocale, SUPPORTED_LOCALES, type ReportActionType, type WorkflowDefinition } from "@fusion/core";
import { DEFAULT_MOBILE_NAV_PRIMARY_ITEMS, MAX_MOBILE_NAV_PRIMARY_ITEMS, MOBILE_NAV_SELECTABLE_ITEMS, MOBILE_NAV_SELECTABLE_ITEM_LABEL_KEYS } from "../../../../../core/src/mobile-nav-primary-items";
import { DEFAULT_MOBILE_NAV_PRIMARY_ITEMS, MAX_MOBILE_NAV_PRIMARY_ITEMS, MOBILE_NAV_PRIMARY_SELECTABLE_ITEMS, MOBILE_NAV_SELECTABLE_ITEM_LABEL_KEYS } from "../../../../../core/src/mobile-nav-primary-items";
import { SettingsFieldRow } from "../SettingsFieldRow";
import { SettingsToggleRow } from "../SettingsToggleRow";
import { SettingsSelectRow } from "../SettingsSelectRow";
@@ -374,8 +374,8 @@ export function GeneralSection({ form, setForm, projectId, addToast, prefixError
{/*
FNXC:Navigation 2026-07-17-00:00:
Render the selected quick actions in persisted order so move controls visibly reorder their rows.
The add picker exposes every eligible destination, while each mutation updates the live footer before
Settings is saved; More, Terminal/scripts, shell controls, and plugin views remain unavailable here.
The add picker exposes only footer-eligible destinations, while each mutation updates the live footer before
Settings is saved; More, Ideation, Terminal/scripts, shell controls, and plugin views remain unavailable here.
*/}
<SettingsFieldRow
htmlFor="mobileNavPrimaryItems"
@@ -386,13 +386,13 @@ export function GeneralSection({ form, setForm, projectId, addToast, prefixError
<div role="group" aria-label={t("settings.general.mobileNavPrimaryItems", "Mobile footer quick actions")}>
{(() => {
const selectedItems = Array.isArray(form.mobileNavPrimaryItems) && form.mobileNavPrimaryItems.length > 0
? form.mobileNavPrimaryItems.filter((item): item is typeof MOBILE_NAV_SELECTABLE_ITEMS[number] => MOBILE_NAV_SELECTABLE_ITEMS.includes(item as typeof MOBILE_NAV_SELECTABLE_ITEMS[number]))
? form.mobileNavPrimaryItems.filter((item): item is typeof MOBILE_NAV_PRIMARY_SELECTABLE_ITEMS[number] => MOBILE_NAV_PRIMARY_SELECTABLE_ITEMS.includes(item as typeof MOBILE_NAV_PRIMARY_SELECTABLE_ITEMS[number]))
: [...DEFAULT_MOBILE_NAV_PRIMARY_ITEMS];
const updateItems = (nextItems: string[]) => {
setForm((current) => ({ ...current, mobileNavPrimaryItems: nextItems }));
onMobileNavPrimaryItemsChange?.(nextItems);
};
const availableItems = MOBILE_NAV_SELECTABLE_ITEMS.filter((item) => !selectedItems.includes(item));
const availableItems = MOBILE_NAV_PRIMARY_SELECTABLE_ITEMS.filter((item) => !selectedItems.includes(item));
return <>
{selectedItems.map((item, selectedIndex) => {
const label = t(MOBILE_NAV_SELECTABLE_ITEM_LABEL_KEYS[item], item);

View File

@@ -10,7 +10,7 @@ export type ViewMode = "overview" | "project";
FNXC:ViewState 2026-06-22-00:00:
Workflows, Import Tasks, and Automations are promoted to top-level main-content task views (left-sidebar destinations) instead of modal-only overlays, so they render in the main panel like Command Center.
*/
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" | "pull-requests" | "workflows" | "import-tasks" | "automations" | "settings" | "task-detail";
export type BuiltInTaskView = "board" | "list" | "graph" | "agents" | "missions" | "chat" | "documents" | "research" | "evals" | "ideation" | "goalsView" | "todos" | "planning" | "skills" | "mailbox" | "insights" | "memory" | "command-center" | "secrets" | "devserver" | "dev-server" | "pull-requests" | "workflows" | "import-tasks" | "automations" | "settings" | "task-detail";
export type PluginTaskView = `plugin:${string}:${string}`;
export type TaskView = BuiltInTaskView | PluginTaskView;
@@ -24,6 +24,12 @@ const BUILT_IN_TASK_VIEWS: readonly BuiltInTaskView[] = [
"documents",
"research",
"evals",
/*
FNXC:Navigation 2026-08-01-00:00:
FN-8352 promotes Ideation from a Command Center tab to a persisted,
default-off experimental top-level view.
*/
"ideation",
"goalsView",
/*
FNXC:ViewState 2026-06-21-09:14: