feat(FN-3104): switch todos to modal-only navigation

- Add TodoModal component and styling, and mount it through AppModals
- Remove dedicated Todos view routing and drive todos access through modal state/actions
- Update Header and MobileNavBar interactions plus modal manager/view-state hooks for modal flow
- Expand dashboard tests to cover modal rendering, open/close behavior, and updated app/header expectations

Fusion-Task-Id: FN-3104
This commit is contained in:
Fusion
2026-05-02 07:58:56 -07:00
committed by gsxdsm
parent 906c244bf3
commit c643340fa7
15 changed files with 324 additions and 81 deletions

View File

@@ -78,7 +78,7 @@ const RoadmapsView = lazy(() => import("./components/RoadmapsView").then((m) =>
const SkillsView = lazy(() => import("./components/SkillsView").then((m) => ({ default: m.SkillsView })));
const MemoryView = lazy(() => import("./components/MemoryView").then((m) => ({ default: m.MemoryView })));
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 })));
// Warm lazy chunks during browser idle so first navigation to each view is
// instant. Each chunk is ~1080 kB; total prefetch finishes well under a
@@ -405,13 +405,10 @@ function AppInner() {
if ((taskView === "devserver" || taskView === "dev-server") && !devServerEnabled) {
handleChangeTaskView("board");
}
if (taskView === "todos" && !todosEnabled) {
handleChangeTaskView("board");
}
if (taskView === "research" && !researchEnabled) {
handleChangeTaskView("board");
}
}, [taskView, settingsLoaded, skillsEnabled, insightsEnabled, roadmapEnabled, handleChangeTaskView, agentsEnabled, memoryEnabled, devServerEnabled, todosEnabled, researchEnabled]);
}, [taskView, settingsLoaded, skillsEnabled, insightsEnabled, roadmapEnabled, handleChangeTaskView, agentsEnabled, memoryEnabled, devServerEnabled, researchEnabled]);
// Auto-close nodes overlay if feature flag is toggled off while overlay is open
useEffect(() => {
@@ -822,24 +819,6 @@ function AppInner() {
);
}
if (taskView === "todos") {
if (!settingsLoaded || !todosEnabled) {
return null;
}
return (
<PageErrorBoundary>
<Suspense fallback={null}>
<TodoView
addToast={addToast}
projectId={currentProject?.id}
onPlanningMode={modalManager.openPlanningWithInitialPlan}
onTaskCreated={(task) => ingestCreatedTasks([task])}
/>
</Suspense>
</PageErrorBoundary>
);
}
if (taskView === "devserver" || taskView === "dev-server") {
if (!settingsLoaded || !devServerEnabled) {
return null;
@@ -961,6 +940,9 @@ function AppInner() {
onToggleTerminal={modalManager.toggleTerminal}
onOpenFiles={modalManager.openFiles}
filesOpen={modalManager.filesOpen}
onOpenTodos={modalManager.openTodos}
todosOpen={modalManager.todosOpen}
todosEnabled={todosEnabled}
globalPaused={globalPaused}
enginePaused={enginePaused}
onToggleGlobalPause={toggleGlobalPause}
@@ -994,7 +976,6 @@ function AppInner() {
memoryView: memoryEnabled,
devServer: devServerEnabled,
devServerView: devServerEnabled,
todoView: todosEnabled,
researchView: researchEnabled,
}}
pluginDashboardViews={pluginDashboardViews}
@@ -1072,6 +1053,8 @@ function AppInner() {
onOpenScripts={modalManager.openScripts}
onToggleTerminal={modalManager.toggleTerminal}
onOpenFiles={modalManager.openFiles}
onOpenTodos={modalManager.openTodos}
todosOpen={modalManager.todosOpen}
onOpenGitHubImport={modalManager.openGitHubImport}
onOpenPlanning={modalManager.openPlanning}
onResumePlanning={modalManager.resumePlanning}

View File

@@ -13,6 +13,7 @@ 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";
@@ -254,6 +255,16 @@ export function AppModals({
/>
)}
{modalManager.todosOpen && (
<TodoModal
isOpen={true}
onClose={modalManager.closeTodos}
addToast={addToast}
projectId={projectId}
onPlanningMode={modalManager.openPlanningWithInitialPlan}
/>
)}
<UsageIndicator
isOpen={modalManager.usageOpen}
onClose={modalManager.closeUsage}

View File

@@ -192,6 +192,9 @@ export interface HeaderProps {
/** Opens the top-level workspace-aware file browser modal. */
onOpenFiles?: () => void;
filesOpen?: boolean;
onOpenTodos?: () => void;
todosOpen?: boolean;
todosEnabled?: boolean;
globalPaused?: boolean;
enginePaused?: boolean;
onToggleGlobalPause?: () => void;
@@ -222,7 +225,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; roadmap?: boolean; memoryView?: boolean; devServer?: boolean; devServerView?: boolean; todoView?: boolean; researchView?: boolean };
experimentalFeatures?: { insights?: boolean; roadmap?: boolean; memoryView?: boolean; devServer?: boolean; devServerView?: boolean; researchView?: boolean };
pluginDashboardViews?: PluginDashboardViewEntry[];
}
@@ -247,6 +250,9 @@ export function Header({
onToggleTerminal,
onOpenFiles,
filesOpen,
onOpenTodos,
todosOpen,
todosEnabled,
globalPaused,
enginePaused,
onToggleGlobalPause,
@@ -334,7 +340,7 @@ export function Header({
const hasViewOverflowItems = useMemo(() => {
return !!(
experimentalFeatures?.researchView ||
experimentalFeatures?.todoView ||
todosEnabled ||
experimentalFeatures?.insights ||
experimentalFeatures?.roadmap ||
showSkillsTab ||
@@ -343,7 +349,7 @@ export function Header({
!hideFullNav ||
pluginDashboardViews.some((entry) => entry.view.placement !== "primary")
);
}, [experimentalFeatures, showSkillsTab, hideFullNav, pluginDashboardViews]);
}, [experimentalFeatures, todosEnabled, showSkillsTab, hideFullNav, pluginDashboardViews]);
const getEffectiveViewport = useCallback(() => {
const vv = window.visualViewport;
@@ -1117,7 +1123,7 @@ export function Header({
<>
<button
ref={viewOverflowTriggerRef}
className={`view-toggle-btn${["research", "skills", "roadmaps", "insights", "memory", "dev-server", "devserver"].includes(view) || (experimentalFeatures?.todoView && view === "todos") || view.startsWith("plugin:") ? " active" : ""}`}
className={`view-toggle-btn${["research", "skills", "roadmaps", "insights", "memory", "dev-server", "devserver"].includes(view) || (todosEnabled && todosOpen) || view.startsWith("plugin:") ? " active" : ""}`}
onClick={() => setIsViewOverflowOpen((prev) => !prev)}
title="More views"
aria-label="More views"
@@ -1219,11 +1225,11 @@ export function Header({
<span className="visually-hidden" data-testid="view-toggle-dev-server" />
</button>
)}
{experimentalFeatures?.todoView && (
{todosEnabled && onOpenTodos && (
<button
className={`view-toggle-overflow-item${view === "todos" ? " active" : ""}`}
className={`view-toggle-overflow-item${todosOpen ? " active" : ""}`}
onClick={() => {
onChangeView("todos");
onOpenTodos();
setIsViewOverflowOpen(false);
}}
role="menuitem"
@@ -1438,6 +1444,17 @@ export function Header({
</button>
)}
{!isCompact && todosEnabled && onOpenTodos && (
<button
className={`btn-icon${todosOpen ? " btn-icon--active" : ""}`}
onClick={onOpenTodos}
title="Open todos"
data-testid="todos-toggle-btn"
>
<CheckSquare size={16} />
</button>
)}
{/* Git Manager button - desktop only (moved to overflow on mobile/tablet) */}
{!isCompact && onOpenGitManager && (
<button

View File

@@ -58,6 +58,8 @@ export interface MobileNavBarProps {
onOpenScripts?: () => void;
onToggleTerminal?: () => void;
onOpenFiles?: () => void;
onOpenTodos?: () => void;
todosOpen?: boolean;
onOpenGitHubImport?: () => void;
onOpenPlanning?: () => void;
onResumePlanning?: () => void;
@@ -108,6 +110,8 @@ export function MobileNavBar({
onOpenScripts,
onToggleTerminal,
onOpenFiles,
onOpenTodos,
todosOpen = false,
onOpenGitHubImport,
onOpenPlanning,
onResumePlanning,
@@ -200,7 +204,7 @@ export function MobileNavBar({
|| view === "memory"
|| view === "devserver"
|| view === "dev-server"
|| (view === "todos" && todoViewEnabled)
|| (todosOpen && todoViewEnabled)
|| (view === "roadmaps" && !showRoadmapsTopLevel)
|| (view === "skills" && !showSkillsTopLevel)
|| view.startsWith("plugin:");
@@ -625,7 +629,7 @@ export function MobileNavBar({
type="button"
className="mobile-more-item"
data-testid="mobile-more-item-todos"
onClick={() => handleMoreAction(() => onChangeView("todos"))}
onClick={() => handleMoreAction(() => onOpenTodos?.())}
>
<CheckSquare />
<span>Todos</span>

View File

@@ -0,0 +1,64 @@
.modal.todo-modal {
width: 80vw;
max-width: calc(var(--space-xl) * 37.5);
height: 75vh;
min-height: calc(var(--space-xs) * 100);
max-height: calc(100dvh - var(--overlay-padding-top, 10vh) - var(--space-lg));
overflow: hidden;
resize: both;
display: flex;
flex-direction: column;
}
.todo-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-lg);
}
.todo-modal-header-title {
display: flex;
align-items: center;
gap: var(--space-sm);
}
.todo-modal-header-title h2 {
margin: 0;
font-size: calc(var(--space-md) + var(--space-xs) * 0.75);
}
.todo-modal-header-title p {
margin: 0;
color: var(--text-muted);
font-size: calc(var(--space-md) - var(--space-xs) * 0.25);
}
.todo-modal-body {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
min-height: 0;
}
@media (max-width: 768px) {
.modal-overlay:has(.todo-modal) {
padding-top: 0;
align-items: stretch;
justify-content: stretch;
}
.modal.todo-modal {
width: 100vw;
min-width: 0;
height: 100dvh;
min-height: 0;
max-width: 100vw;
max-height: 100dvh;
margin: 0;
border: none;
border-radius: 0;
resize: none;
}
}

View File

@@ -0,0 +1,56 @@
import "./TodoModal.css";
import { useEffect } from "react";
import { ListChecks, X } from "lucide-react";
import { useOverlayDismiss } from "../hooks/useOverlayDismiss";
import { TodoView } from "./TodoView";
interface TodoModalProps {
isOpen?: boolean;
onClose: () => void;
projectId?: string;
addToast: (message: string, type?: "success" | "error" | "info") => void;
onPlanningMode?: (initialPlan: string) => void;
}
export function TodoModal({ onClose, projectId, addToast, onPlanningMode }: TodoModalProps) {
const overlayDismissProps = useOverlayDismiss(onClose);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
onClose();
}
};
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [onClose]);
return (
<div className="modal-overlay open" {...overlayDismissProps} role="dialog" aria-modal="true">
<div className="modal todo-modal">
<div className="modal-header todo-modal-header">
<div className="todo-modal-header-title">
<ListChecks size={18} />
<div>
<h2>Todos</h2>
<p>Manage reusable todo lists for your project.</p>
</div>
</div>
<button className="modal-close" onClick={onClose} aria-label="Close">
<X size={20} />
</button>
</div>
<div className="todo-modal-body">
<TodoView
projectId={projectId}
addToast={addToast}
onPlanningMode={onPlanningMode}
onClose={onClose}
/>
</div>
</div>
</div>
);
}

View File

@@ -6,34 +6,6 @@
overflow: hidden;
}
.todo-view-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: var(--space-lg) var(--space-xl);
border-bottom: 1px solid var(--border);
background: var(--surface);
gap: var(--space-md);
flex-shrink: 0;
}
.todo-view-header h2 {
font-size: 1rem;
font-weight: 600;
color: var(--text);
margin: 0;
display: flex;
align-items: center;
gap: var(--space-sm);
}
.todo-view-description {
color: var(--text-muted);
font-size: 0.8125rem;
margin: 2px 0 0 0;
}
.todo-view-layout {
display: flex;
flex-direction: row;

View File

@@ -25,6 +25,7 @@ interface TodoViewProps {
addToast: (message: string, type?: "success" | "error" | "info") => void;
onPlanningMode?: (initialPlan: string) => void;
onTaskCreated?: (task: Task) => void;
onClose?: () => void;
}
function sortItems(items: TodoItem[]): TodoItem[] {
@@ -307,13 +308,6 @@ export function TodoView({ projectId, addToast, onPlanningMode, onTaskCreated }:
return (
<div className="todo-view">
<div className="todo-view-header">
<div>
<h2>Todos</h2>
<p className="todo-view-description">Manage reusable todo lists for your project.</p>
</div>
</div>
<div className="todo-view-layout">
<aside className="todo-view-sidebar" aria-label="Todo lists sidebar">
<div className="todo-sidebar-header">

View File

@@ -1504,7 +1504,6 @@ describe("App view switching", () => {
it("opens planning mode when TodoView triggers planning from todo item", async () => {
localStorage.setItem("kb-dashboard-view-mode", "project");
localStorage.setItem(taskViewStorageKey(), "todos");
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
...defaultSettings,
experimentalFeatures: {
@@ -1515,6 +1514,12 @@ describe("App view switching", () => {
render(<App />);
await waitFor(() => {
expect(screen.getByTestId("todos-toggle-btn")).toBeInTheDocument();
});
fireEvent.click(screen.getByTestId("todos-toggle-btn"));
await waitFor(() => {
expect(screen.getByTestId("todo-view")).toBeInTheDocument();
});

View File

@@ -45,6 +45,14 @@ vi.mock("../FileBrowserModal", () => ({
FileBrowserModal: () => null,
}));
const mockTodoModalProps = vi.fn();
vi.mock("../TodoModal", () => ({
TodoModal: (props: any) => {
mockTodoModalProps(props);
return null;
},
}));
vi.mock("../UsageIndicator", () => ({
UsageIndicator: () => null,
}));
@@ -145,6 +153,7 @@ describe("AppModals", () => {
terminalInitialCommand: undefined,
scriptsOpen: false,
filesOpen: false,
todosOpen: false,
fileBrowserWorkspace: "project",
usageOpen: false,
usageAnchorRect: null,
@@ -182,6 +191,8 @@ describe("AppModals", () => {
runScript: vi.fn(),
openFiles: vi.fn(),
closeFiles: vi.fn(),
openTodos: vi.fn(),
closeTodos: vi.fn(),
setFileWorkspace: vi.fn(),
openUsage: vi.fn(),
closeUsage: vi.fn(),
@@ -224,6 +235,7 @@ describe("AppModals", () => {
mockModelOnboardingModalProps.mockClear();
mockSettingsModalProps.mockClear();
mockSystemStatsModalProps.mockClear();
mockTodoModalProps.mockClear();
});
it("renders without crashing", () => {
@@ -247,6 +259,50 @@ describe("AppModals", () => {
expect(document.body).toBeDefined();
});
it("renders TodoModal when todosOpen is true", () => {
render(
<AppModals
projectId="proj-1"
tasks={[]}
projects={[]}
currentProject={null}
addToast={vi.fn()}
toasts={mockToasts}
removeToast={vi.fn()}
modalManager={{ ...mockModalManager, todosOpen: true }}
projectActions={{ handleAddProject: vi.fn(), handleSetupComplete: vi.fn(), handleModelOnboardingComplete: vi.fn() }}
taskHandlers={{ handleModalCreate: vi.fn(), handlePlanningTaskCreated: vi.fn(), handlePlanningTasksCreated: vi.fn(), handleSubtaskTasksCreated: vi.fn(), handleGitHubImport: vi.fn() }}
taskOperations={{ moveTask: vi.fn(), deleteTask: vi.fn(), mergeTask: vi.fn(), retryTask: vi.fn(), duplicateTask: vi.fn() }}
deepLink={{ handleDetailClose: vi.fn() }}
settings={mockSettings}
/>
);
expect(mockTodoModalProps).toHaveBeenCalledTimes(1);
});
it("does not render TodoModal when todosOpen is false", () => {
render(
<AppModals
projectId="proj-1"
tasks={[]}
projects={[]}
currentProject={null}
addToast={vi.fn()}
toasts={mockToasts}
removeToast={vi.fn()}
modalManager={{ ...mockModalManager, todosOpen: false }}
projectActions={{ handleAddProject: vi.fn(), handleSetupComplete: vi.fn(), handleModelOnboardingComplete: vi.fn() }}
taskHandlers={{ handleModalCreate: vi.fn(), handlePlanningTaskCreated: vi.fn(), handlePlanningTasksCreated: vi.fn(), handleSubtaskTasksCreated: vi.fn(), handleGitHubImport: vi.fn() }}
taskOperations={{ moveTask: vi.fn(), deleteTask: vi.fn(), mergeTask: vi.fn(), retryTask: vi.fn(), duplicateTask: vi.fn() }}
deepLink={{ handleDetailClose: vi.fn() }}
settings={mockSettings}
/>
);
expect(mockTodoModalProps).not.toHaveBeenCalled();
});
it("passes the live board task snapshot into the open detail modal while preserving prompt data", async () => {
const manager = {
...mockModalManager,

View File

@@ -146,13 +146,13 @@ describe("Header", () => {
expect(listBtn.getAttribute("aria-pressed")).toBe("false");
});
it("renders view overflow trigger when todoView experimental flag is enabled", () => {
renderHeader({ onChangeView: noop, experimentalFeatures: { todoView: true } });
it("renders view overflow trigger when todos are enabled", () => {
renderHeader({ onChangeView: noop, todosEnabled: true });
expect(screen.getByTestId("view-toggle-overflow-trigger")).toBeDefined();
});
it("shows the Todos entry in view overflow when todoView is enabled", () => {
renderHeader({ onChangeView: noop, experimentalFeatures: { todoView: true } });
it("shows the Todos entry in view overflow when todos are enabled", () => {
renderHeader({ onChangeView: noop, onOpenTodos: vi.fn(), todosEnabled: true });
fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
expect(screen.getByTestId("view-overflow-todos")).toBeInTheDocument();
});
@@ -190,7 +190,7 @@ describe("Header", () => {
renderHeader({
onChangeView: noop,
showSkillsTab: false,
experimentalFeatures: { insights: false, roadmap: false, memoryView: false, devServerView: false, todoView: false },
experimentalFeatures: { insights: false, roadmap: false, memoryView: false, devServerView: false },
});
expect(screen.getByTestId("view-toggle-overflow-trigger")).toBeDefined();
});
@@ -340,6 +340,25 @@ describe("Header", () => {
});
});
describe("todos button", () => {
it("renders todos button on desktop when enabled", () => {
renderHeader({ onOpenTodos: vi.fn(), todosEnabled: true }, "desktop");
expect(screen.getByTitle("Open todos")).toBeDefined();
});
it("does not render todos button when disabled", () => {
renderHeader({ onOpenTodos: vi.fn(), todosEnabled: false }, "desktop");
expect(screen.queryByTitle("Open todos")).toBeNull();
});
it("calls onOpenTodos when clicked", () => {
const onOpenTodos = vi.fn();
renderHeader({ onOpenTodos, todosEnabled: true }, "desktop");
fireEvent.click(screen.getByTitle("Open todos"));
expect(onOpenTodos).toHaveBeenCalled();
});
});
describe("pause controls", () => {
it("renders engine control split-button", () => {
renderHeader();

View File

@@ -0,0 +1,58 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { fireEvent, render, screen } from "@testing-library/react";
import { TodoModal } from "../TodoModal";
const mockTodoView = vi.fn();
vi.mock("../TodoView", () => ({
TodoView: (props: unknown) => {
mockTodoView(props);
return <div data-testid="todo-view-content">Todo content</div>;
},
}));
describe("TodoModal", () => {
const onClose = vi.fn();
const addToast = vi.fn();
beforeEach(() => {
vi.clearAllMocks();
});
it("renders modal dialog semantics and header content", () => {
render(<TodoModal onClose={onClose} addToast={addToast} />);
expect(screen.getByRole("dialog")).toHaveAttribute("aria-modal", "true");
expect(screen.getByRole("heading", { name: "Todos" })).toBeInTheDocument();
expect(screen.getByText("Manage reusable todo lists for your project.")).toBeInTheDocument();
});
it("closes on Escape", () => {
render(<TodoModal onClose={onClose} addToast={addToast} />);
fireEvent.keyDown(document, { key: "Escape" });
expect(onClose).toHaveBeenCalledTimes(1);
});
it("closes on overlay backdrop click", () => {
render(<TodoModal onClose={onClose} addToast={addToast} />);
const overlay = screen.getByRole("dialog");
fireEvent.mouseDown(overlay);
fireEvent.mouseUp(overlay);
expect(onClose).toHaveBeenCalledTimes(1);
});
it("closes from close button", () => {
render(<TodoModal onClose={onClose} addToast={addToast} />);
fireEvent.click(screen.getByRole("button", { name: "Close" }));
expect(onClose).toHaveBeenCalledTimes(1);
});
it("passes projectId and addToast through to TodoView", () => {
render(<TodoModal onClose={onClose} addToast={addToast} projectId="proj-1" />);
expect(screen.getByTestId("todo-view-content")).toBeInTheDocument();
expect(mockTodoView).toHaveBeenCalledWith(
expect.objectContaining({ projectId: "proj-1", addToast }),
);
});
});

View File

@@ -78,11 +78,6 @@ describe("TodoView", () => {
mockUseTodoLists.mockReturnValue(createMockTodoLists());
});
it("renders header with Todos heading", () => {
render(<TodoView addToast={addToast} />);
expect(screen.getByRole("heading", { name: "Todos" })).toBeInTheDocument();
});
it("renders sidebar with list names", () => {
render(<TodoView addToast={addToast} />);
expect(screen.getByTestId("todo-list-list-1")).toHaveTextContent("My List");

View File

@@ -41,6 +41,7 @@ export interface ModalManager {
terminalOpen: boolean;
terminalInitialCommand: string | undefined;
filesOpen: boolean;
todosOpen: boolean;
fileBrowserWorkspace: string;
activityLogOpen: boolean;
gitManagerOpen: boolean;
@@ -90,6 +91,8 @@ export interface ModalManager {
openFiles: () => void;
closeFiles: () => void;
openTodos: () => void;
closeTodos: () => void;
setFileWorkspace: (workspace: string) => void;
openActivityLog: () => void;
@@ -148,6 +151,7 @@ export function useModalManager(options: UseModalManagerOptions): ModalManager {
const [terminalOpen, setTerminalOpen] = useState(false);
const [terminalInitialCommand, setTerminalInitialCommand] = useState<string | undefined>(undefined);
const [filesOpen, setFilesOpen] = useState(false);
const [todosOpen, setTodosOpen] = useState(false);
const [fileBrowserWorkspace, setFileBrowserWorkspace] = useState("project");
const [activityLogOpen, setActivityLogOpen] = useState(false);
const [gitManagerOpen, setGitManagerOpen] = useState(false);
@@ -165,6 +169,7 @@ export function useModalManager(options: UseModalManagerOptions): ModalManager {
isSubtaskOpen ||
terminalOpen ||
filesOpen ||
todosOpen ||
activityLogOpen ||
gitManagerOpen ||
workflowStepsOpen ||
@@ -268,6 +273,8 @@ export function useModalManager(options: UseModalManagerOptions): ModalManager {
const openFiles = useCallback(() => setFilesOpen(true), []);
const closeFiles = useCallback(() => setFilesOpen(false), []);
const openTodos = useCallback(() => setTodosOpen(true), []);
const closeTodos = useCallback(() => setTodosOpen(false), []);
const setFileWorkspace = useCallback((workspace: string) => {
setFileBrowserWorkspace(workspace);
}, []);
@@ -338,6 +345,7 @@ export function useModalManager(options: UseModalManagerOptions): ModalManager {
terminalOpen,
terminalInitialCommand,
filesOpen,
todosOpen,
fileBrowserWorkspace,
activityLogOpen,
gitManagerOpen,
@@ -375,6 +383,8 @@ export function useModalManager(options: UseModalManagerOptions): ModalManager {
closeTerminal,
openFiles,
closeFiles,
openTodos,
closeTodos,
setFileWorkspace,
openActivityLog,
closeActivityLog,

View File

@@ -4,7 +4,7 @@ import type { ProjectInfo } from "../api";
import { getScopedItem, setScopedItem } from "../utils/projectStorage";
export type ViewMode = "overview" | "project";
export type BuiltInTaskView = "board" | "list" | "agents" | "missions" | "chat" | "documents" | "research" | "roadmaps" | "skills" | "mailbox" | "insights" | "memory" | "devserver" | "dev-server" | "todos";
export type BuiltInTaskView = "board" | "list" | "agents" | "missions" | "chat" | "documents" | "research" | "roadmaps" | "skills" | "mailbox" | "insights" | "memory" | "devserver" | "dev-server";
export type PluginTaskView = `plugin:${string}:${string}`;
export type TaskView = BuiltInTaskView | PluginTaskView;
@@ -23,7 +23,6 @@ const BUILT_IN_TASK_VIEWS: readonly BuiltInTaskView[] = [
"memory",
"devserver",
"dev-server",
"todos",
];
function isBuiltInTaskView(value: string | null): value is BuiltInTaskView {