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:
7
.changeset/fn-8352-ideation-top-level-view.md
Normal file
7
.changeset/fn-8352-ideation-top-level-view.md
Normal 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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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" : ""}`}
|
||||
|
||||
@@ -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") }]
|
||||
: []),
|
||||
|
||||
@@ -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")) },
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 } });
|
||||
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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"));
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user