From e67dd11b5079a5efac88651f174648ed1de5498a Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sun, 21 Jun 2026 10:32:23 -0700 Subject: [PATCH] FN-6829: dock todos as a project view Convert Todos into the standard project right-content navigation surface. - Route Todos through taskView state instead of modal manager state. - Remove TodoModal wiring, styles, and tests while updating header, sidebar, mobile nav, and history coverage. - Polish TodoView as a standalone content page with refreshed header and layout styling. - Update dashboard and todo documentation for the full-view behavior. Files changed: docs/dashboard-guide.md | 5 +- docs/todo-view.md | 5 +- packages/dashboard/app/App.tsx | 30 ++--- .../app/__tests__/lazy-loaded-views-docs.test.ts | 10 +- packages/dashboard/app/components/AppModals.tsx | 16 --- packages/dashboard/app/components/Header.tsx | 12 +- .../dashboard/app/components/LeftSidebarNav.tsx | 8 +- packages/dashboard/app/components/MobileNavBar.tsx | 8 +- packages/dashboard/app/components/TodoModal.css | 76 ------------ packages/dashboard/app/components/TodoModal.tsx | 79 ------------ packages/dashboard/app/components/TodoView.css | 55 ++++++--- packages/dashboard/app/components/TodoView.tsx | 24 ++-- .../app/components/__tests__/AppModals.test.tsx | 56 --------- .../app/components/__tests__/Header.test.tsx | 26 ++-- .../components/__tests__/LeftSidebarNav.test.tsx | 7 +- .../app/components/__tests__/MobileNavBar.test.tsx | 22 +++- .../app/components/__tests__/TodoModal.test.tsx | 136 --------------------- .../__tests__/TodoView.mobile-css.test.ts | 7 -- .../app/components/__tests__/TodoView.test.tsx | 11 +- ...-merge-toggle-blank.mobile-integration.test.tsx | 1 - .../__tests__/navigation-history.test.tsx | 29 ++++- .../dashboard/app/hooks/useMobileScrollLock.ts | 2 +- packages/dashboard/app/hooks/useModalManager.ts | 10 -- packages/dashboard/app/hooks/useViewState.ts | 7 +- 24 files changed, 161 insertions(+), 481 deletions(-) Fusion-Task-Id: FN-6829 Fusion-Task-Lineage: b4034bf2-b8e2-47ec-be7c-11c320d2fc44 --- docs/dashboard-guide.md | 5 +- docs/todo-view.md | 5 +- packages/dashboard/app/App.tsx | 30 ++-- .../__tests__/lazy-loaded-views-docs.test.ts | 10 +- .../dashboard/app/components/AppModals.tsx | 16 --- packages/dashboard/app/components/Header.tsx | 12 +- .../app/components/LeftSidebarNav.tsx | 8 +- .../dashboard/app/components/MobileNavBar.tsx | 8 +- .../dashboard/app/components/TodoModal.css | 76 ---------- .../dashboard/app/components/TodoModal.tsx | 79 ---------- .../dashboard/app/components/TodoView.css | 55 ++++--- .../dashboard/app/components/TodoView.tsx | 24 ++-- .../components/__tests__/AppModals.test.tsx | 56 -------- .../app/components/__tests__/Header.test.tsx | 26 ++-- .../__tests__/LeftSidebarNav.test.tsx | 7 +- .../__tests__/MobileNavBar.test.tsx | 22 ++- .../components/__tests__/TodoModal.test.tsx | 136 ------------------ .../__tests__/TodoView.mobile-css.test.ts | 7 - .../components/__tests__/TodoView.test.tsx | 11 +- ...e-toggle-blank.mobile-integration.test.tsx | 1 - .../__tests__/navigation-history.test.tsx | 29 +++- .../app/hooks/useMobileScrollLock.ts | 2 +- .../dashboard/app/hooks/useModalManager.ts | 10 -- packages/dashboard/app/hooks/useViewState.ts | 7 +- 24 files changed, 161 insertions(+), 481 deletions(-) delete mode 100644 packages/dashboard/app/components/TodoModal.css delete mode 100644 packages/dashboard/app/components/TodoModal.tsx delete mode 100644 packages/dashboard/app/components/__tests__/TodoModal.test.tsx diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index 5975e4773d..0ce70bd301 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -505,12 +505,13 @@ Project-file previews also support selection comments in both raw and rendered m ## Todo View -Todo View is an experimental dashboard surface for managing per-project todo lists and turning items into planning or task workflows. +Todo View is an experimental full-height dashboard surface for managing per-project todo lists and turning items into planning or task workflows. It renders in the right content area like other project views rather than as a modal overlay. > Available when `experimentalFeatures.todoView` is enabled. Navigation: -- Desktop: **Header → More views → Todos** (single canonical desktop entry) +- Desktop/tablet with Left Sidebar Navigation enabled: **Left sidebar → Todos** +- Desktop/tablet without the left sidebar: **Header → More views → Todos** - Mobile: **More** sheet → **Todos** For full behavior, API contracts, and storage details, use the canonical [Todo View guide](./todo-view.md). diff --git a/docs/todo-view.md b/docs/todo-view.md index 6b7b8237e8..98bff237ad 100644 --- a/docs/todo-view.md +++ b/docs/todo-view.md @@ -2,7 +2,7 @@ [← Docs index](./README.md) -Todo View is an experimental dashboard surface for personal/project todo lists that can feed directly into Fusion planning and task workflows. +Todo View is an experimental full-height dashboard surface for personal/project todo lists that can feed directly into Fusion planning and task workflows. It renders in the project right-content area like other views rather than opening a modal overlay. ## Overview @@ -37,7 +37,8 @@ Behavior when disabled: When enabled: -- Desktop: header overflow menu (**More views**) → **Todos** (the only desktop Todos navigation entry) +- Desktop/tablet with Left Sidebar Navigation enabled: left sidebar → **Todos** +- Desktop/tablet without the left sidebar: header overflow menu (**More views**) → **Todos** - Mobile: **More** sheet in the mobile nav bar → **Todos** ## List management diff --git a/packages/dashboard/app/App.tsx b/packages/dashboard/app/App.tsx index 27404a7432..fac0607799 100644 --- a/packages/dashboard/app/App.tsx +++ b/packages/dashboard/app/App.tsx @@ -118,7 +118,7 @@ const MemoryView = lazy(() => import("./components/MemoryView").then((m) => ({ d const SecretsView = lazy(() => import("./components/SecretsView").then((m) => ({ default: m.SecretsView }))); const CommandCenter = lazy(() => import("./components/command-center/CommandCenter").then((m) => ({ default: m.CommandCenter }))); const DevServerView = lazy(() => import("./components/DevServerView").then((m) => ({ default: m.DevServerView }))); -const _TodoView = lazy(() => import("./components/TodoView").then((m) => ({ default: m.TodoView }))); +const TodoView = lazy(() => import("./components/TodoView").then((m) => ({ default: m.TodoView }))); const GoalsView = lazy(() => import("./components/GoalsView").then((m) => ({ default: m.GoalsView }))); const StashRecoveryView = lazy(() => import("./components/StashRecoveryView").then((m) => ({ default: m.StashRecoveryView }))); const PullRequestView = lazy(() => import("./components/PullRequestView").then((m) => ({ default: m.PullRequestView }))); @@ -1086,7 +1086,10 @@ function AppInner() { if (taskView === "goalsView" && !goalsEnabled) { handleChangeTaskView("board"); } - }, [taskView, settingsLoaded, skillsEnabled, insightsEnabled, handleChangeTaskView, agentsEnabled, memoryEnabled, devServerEnabled, researchEnabled, evalsEnabled, goalsEnabled, graphPluginTaskView]); + if (taskView === "todos" && !todosEnabled) { + handleChangeTaskView("board"); + } + }, [taskView, settingsLoaded, skillsEnabled, insightsEnabled, handleChangeTaskView, agentsEnabled, memoryEnabled, devServerEnabled, researchEnabled, evalsEnabled, goalsEnabled, todosEnabled, graphPluginTaskView]); const { availableModels, @@ -1289,11 +1292,6 @@ function AppInner() { pushNav({ type: "modal", close: modalManager.closeFiles }); }, [modalManager, pushNav]); - const openTodosWithNav = useCallback(() => { - modalManager.openTodos(); - pushNav({ type: "modal", close: modalManager.closeTodos }); - }, [modalManager, pushNav]); - const openActivityLogWithNav = useCallback(() => { modalManager.openActivityLog(); pushNav({ type: "modal", close: modalManager.closeActivityLog }); @@ -1773,7 +1771,17 @@ function AppInner() { ); } - + if (taskView === "todos") { + // FNXC:Todos 2026-06-21-09:21: Todos render as a docked right-content view, not a modal overlay, per FN-6829 so all dashboard navigation surfaces share the same taskView routing model. + if (!settingsLoaded || !todosEnabled) return null; + return ( + + + ingestCreatedTasks([task])} /> + + + ); + } if (taskView === "command-center") { return ( @@ -1943,8 +1951,6 @@ function AppInner() { onToggleTerminal={toggleTerminalWithNav} onOpenFiles={openFilesWithNav} filesOpen={modalManager.filesOpen} - onOpenTodos={openTodosWithNav} - todosOpen={modalManager.todosOpen} todosEnabled={todosEnabled} view={taskView} onChangeView={viewMode === "project" && currentProject ? handleTaskViewChange : undefined} @@ -2113,8 +2119,6 @@ function AppInner() { view={taskView} onChangeView={handleTaskViewChange} onOpenSettings={openSettingsWithNav} - onOpenTodos={openTodosWithNav} - todosOpen={modalManager.todosOpen} todosEnabled={todosEnabled} mailboxUnreadCount={mailboxUnreadCount} mailboxPendingApprovalCount={mailboxPendingApprovalCount} @@ -2181,8 +2185,6 @@ function AppInner() { onOpenScripts={openScriptsWithNav} onToggleTerminal={toggleTerminalWithNav} onOpenFiles={openFilesWithNav} - onOpenTodos={openTodosWithNav} - todosOpen={modalManager.todosOpen} onOpenGitHubImport={openGitHubImportWithNav} onOpenPlanning={openPlanningWithNav} onResumePlanning={resumePlanningWithNav} diff --git a/packages/dashboard/app/__tests__/lazy-loaded-views-docs.test.ts b/packages/dashboard/app/__tests__/lazy-loaded-views-docs.test.ts index 7e814e2906..2addfd5a91 100644 --- a/packages/dashboard/app/__tests__/lazy-loaded-views-docs.test.ts +++ b/packages/dashboard/app/__tests__/lazy-loaded-views-docs.test.ts @@ -89,15 +89,7 @@ function extractConstLazyViews(source: string): string[] { function extractAppLazyViews(appSource: string): Set { const normalized = extractConstLazyViews(appSource) - .map((name) => { - if (name === "_TodoView") { - return "TodoView"; - } - if (name.startsWith("_")) { - return null; - } - return name; - }) + .map((name) => (name.startsWith("_") ? null : name)) .filter((name): name is string => Boolean(name)); return new Set(normalized); } diff --git a/packages/dashboard/app/components/AppModals.tsx b/packages/dashboard/app/components/AppModals.tsx index 97b5ce9050..62fea512f4 100644 --- a/packages/dashboard/app/components/AppModals.tsx +++ b/packages/dashboard/app/components/AppModals.tsx @@ -13,7 +13,6 @@ import { SubtaskBreakdownModal } from "./SubtaskBreakdownModal"; import { TerminalModal } from "./TerminalModal"; import { ScriptsModal } from "./ScriptsModal"; import { FileBrowserModal } from "./FileBrowserModal"; -import { TodoModal } from "./TodoModal"; import { UsageIndicator } from "./UsageIndicator"; import { ScheduledTasksModal } from "./ScheduledTasksModal"; import { NewTaskModal } from "./NewTaskModal"; @@ -176,11 +175,6 @@ export function AppModals({ modalManager.closeFiles(); }, [modalManager.closeFiles, removeNav]); - const closeTodosWithNav = useCallback(() => { - removeNav(modalManager.closeTodos); - modalManager.closeTodos(); - }, [modalManager.closeTodos, removeNav]); - const closeUsageWithNav = useCallback(() => { removeNav(modalManager.closeUsage); modalManager.closeUsage(); @@ -405,16 +399,6 @@ export function AppModals({ /> )} - {modalManager.todosOpen && ( - - )} - void; filesOpen?: boolean; - onOpenTodos?: () => void; - todosOpen?: boolean; todosEnabled?: boolean; view?: TaskView; onChangeView?: (view: TaskView) => void; @@ -147,8 +145,6 @@ export function Header({ onToggleTerminal, onOpenFiles, filesOpen, - onOpenTodos, - todosOpen, todosEnabled, view = "board", onChangeView, @@ -1086,7 +1082,7 @@ export function Header({ <> )} - {todosEnabled && onOpenTodos && ( + {todosEnabled && onChangeView && ( - - -
- - - -
- - - ); -} diff --git a/packages/dashboard/app/components/TodoView.css b/packages/dashboard/app/components/TodoView.css index a70ead3d8a..7534ed5444 100644 --- a/packages/dashboard/app/components/TodoView.css +++ b/packages/dashboard/app/components/TodoView.css @@ -1,18 +1,53 @@ /* === TodoView === */ +/* +FNXC:TodosStyling 2026-06-21-09:26: +FN-6829 mounts Todos as a flex child of .project-content like GoalsView; grow, zero min-width, and fill the viewport so the docked view never collapses to modal-era intrinsic sizing. The split-pane layout keeps overflow inside the list and item panes rather than scrolling the whole view. +*/ .todo-view { display: flex; + flex: 1 1 auto; flex-direction: column; + gap: var(--space-lg); height: 100%; + min-height: 0; + min-width: 0; + width: 100%; overflow: hidden; + padding: var(--space-lg); +} + +.todo-view-header { + display: flex; + justify-content: space-between; + align-items: center; + gap: var(--space-md); +} + +.todo-view-title-group { + display: flex; + align-items: center; + gap: var(--space-sm); +} + +.todo-view-title-group h2 { + margin: 0; + color: var(--text); + font-size: calc(var(--space-lg) + var(--space-xs)); +} + +.todo-view-title-group p { + margin: 0; + color: var(--text-muted); } .todo-view-layout { display: flex; flex-direction: row; - flex: 1; + flex: 1 1 auto; + min-height: 0; + min-width: 0; overflow: hidden; gap: var(--space-lg); - padding: var(--space-lg); } .todo-view-sidebar { @@ -444,20 +479,4 @@ .todo-agent-picker-item { min-height: calc(var(--space-2xl) + var(--space-xs)); } - - .todo-view--mobile-keyboard-active { - padding-bottom: 0; - } - - .todo-view--mobile-keyboard-active .todo-view-layout { - height: 100%; - } - - .todo-view--mobile-keyboard-active .todo-view-sidebar { - max-height: calc(var(--space-2xl) * 4); - } - - .todo-view--mobile-keyboard-active .todo-view-main { - overscroll-behavior: contain; - } } diff --git a/packages/dashboard/app/components/TodoView.tsx b/packages/dashboard/app/components/TodoView.tsx index 4ed71c3e5a..47e7f6acd7 100644 --- a/packages/dashboard/app/components/TodoView.tsx +++ b/packages/dashboard/app/components/TodoView.tsx @@ -27,8 +27,6 @@ interface TodoViewProps { addToast: (message: string, type?: "success" | "error" | "info") => void; onPlanningMode?: (initialPlan: string) => void; onTaskCreated?: (task: Task) => void; - onClose?: () => void; - mobileKeyboardActive?: boolean; } function sortItems(items: TodoItem[]): TodoItem[] { @@ -40,7 +38,6 @@ export function TodoView({ addToast, onPlanningMode, onTaskCreated, - mobileKeyboardActive = false, }: TodoViewProps) { const { t } = useTranslation("app"); const { @@ -305,9 +302,22 @@ export function TodoView({ } }, [projectId, addToast, agents, onTaskCreated, t]); + const header = ( +
+
+
+
+ ); + if (loading) { return ( -
+
+ {header}