feat(FN-1206): promote missions to an inline top-level view

- Extend dashboard task view state and nav controls to include a persistent missions view toggle
- Render MissionManager inline in App project content and route mission/session navigation into the missions view
- Remove modal-based mission wiring from AppModals and useModalManager now that missions are no longer a modal
- Add inline MissionManager styling and update App/Header/tablet tests for the new missions navigation behavior
This commit is contained in:
gsxdsm
2026-04-08 15:33:19 -07:00
parent e80fb448d4
commit a47db4fa6b
12 changed files with 179 additions and 163 deletions

View File

@@ -5,6 +5,7 @@ import { Board } from "./components/Board";
import { ListView } from "./components/ListView";
import { ProjectOverview } from "./components/ProjectOverview";
import { AgentsView } from "./components/AgentsView";
import { MissionManager } from "./components/MissionManager";
import { NodesView } from "./components/NodesView";
import { AppModals } from "./components/AppModals";
import { ExecutorStatusBar } from "./components/ExecutorStatusBar";
@@ -23,7 +24,7 @@ import { useAppSettings } from "./hooks/useAppSettings";
import { useDeepLink } from "./hooks/useDeepLink";
import { useFavorites } from "./hooks/useFavorites";
import { useAuthOnboarding } from "./hooks/useAuthOnboarding";
import { useViewState } from "./hooks/useViewState";
import { useViewState, type TaskView } from "./hooks/useViewState";
import { useProjectActions } from "./hooks/useProjectActions";
import { useTaskHandlers } from "./hooks/useTaskHandlers";
import type { AiSessionSummary } from "./api";
@@ -60,6 +61,8 @@ function AppInner() {
// Nodes management is an overlay view (not a modal), so it stays local to App.
const [nodesOpen, setNodesOpen] = useState(false);
const [missionResumeSessionId, setMissionResumeSessionId] = useState<string | undefined>(undefined);
const [missionTargetId, setMissionTargetId] = useState<string | undefined>(undefined);
// Settings state
const {
@@ -94,6 +97,14 @@ function AppInner() {
setThemeMode,
});
const handleTaskViewChange = useCallback((newView: TaskView) => {
if (newView === "missions") {
setMissionResumeSessionId(undefined);
setMissionTargetId(undefined);
}
handleChangeTaskView(newView);
}, [handleChangeTaskView]);
// Auth and onboarding bootstrap logic extracted to a dedicated hook.
useAuthOnboarding({
projectId: currentProject?.id,
@@ -167,15 +178,29 @@ function AppInner() {
setNodesOpen((prev) => !prev);
}, []);
const handleOpenMissionsView = useCallback(() => {
setMissionTargetId(undefined);
setMissionResumeSessionId(undefined);
handleChangeTaskView("missions");
}, [handleChangeTaskView]);
const handleOpenMission = useCallback((missionId: string) => {
setMissionTargetId(missionId);
setMissionResumeSessionId(undefined);
handleChangeTaskView("missions");
}, [handleChangeTaskView]);
const handleOpenBackgroundSession = useCallback((session: AiSessionSummary) => {
if (session.type === "planning") {
modalManager.openPlanningWithSession(session.id);
} else if (session.type === "subtask") {
modalManager.openSubtaskWithSession(session.id);
} else if (session.type === "mission_interview") {
modalManager.openMissionWithSession(session.id);
setMissionTargetId(undefined);
setMissionResumeSessionId(session.id);
handleChangeTaskView("missions");
}
}, [modalManager]);
}, [handleChangeTaskView, modalManager]);
const handleDismissAllNeedingInputSessions = useCallback(() => {
for (const session of sessionsNeedingInput) {
@@ -212,6 +237,29 @@ function AppInner() {
}
// Project view
if (taskView === "missions") {
return (
<MissionManager
isInline={true}
isOpen={true}
onClose={() => {
setMissionTargetId(undefined);
setMissionResumeSessionId(undefined);
handleChangeTaskView("board");
}}
addToast={addToast}
projectId={currentProject?.id}
onSelectTask={(taskId) => {
const task = tasks.find((t) => t.id === taskId);
if (task) modalManager.openDetailTask(task as TaskDetail);
}}
availableTasks={tasks.map((t) => ({ id: t.id, title: t.title }))}
resumeSessionId={missionResumeSessionId}
targetMissionId={missionTargetId}
/>
);
}
if (taskView === "agents") {
return <AgentsView addToast={addToast} projectId={currentProject?.id} />;
}
@@ -244,7 +292,7 @@ function AppInner() {
onToggleFavorite={handleToggleFavorite}
onToggleModelFavorite={handleToggleModelFavorite}
taskStuckTimeoutMs={taskStuckTimeoutMs}
onOpenMission={modalManager.openMissionById}
onOpenMission={handleOpenMission}
/>
);
}
@@ -289,7 +337,6 @@ function AppInner() {
onOpenGitManager={modalManager.openGitManager}
onOpenNodes={handleOpenNodes}
onOpenWorkflowSteps={modalManager.openWorkflowSteps}
onOpenMissions={viewMode === "project" && currentProject ? modalManager.openMissions : undefined}
onOpenScripts={modalManager.openScripts}
onRunScript={modalManager.runScript}
onToggleTerminal={modalManager.toggleTerminal}
@@ -300,7 +347,7 @@ function AppInner() {
onToggleGlobalPause={toggleGlobalPause}
onToggleEnginePause={toggleEnginePause}
view={taskView}
onChangeView={viewMode === "project" && currentProject ? handleChangeTaskView : undefined}
onChangeView={viewMode === "project" && currentProject ? handleTaskViewChange : undefined}
searchQuery={searchQuery}
onSearchChange={setSearchQuery}
projects={projects}
@@ -337,7 +384,7 @@ function AppInner() {
)}
<MobileNavBar
view={taskView}
onChangeView={viewMode === "project" && currentProject ? handleChangeTaskView : () => {}}
onChangeView={viewMode === "project" && currentProject ? handleTaskViewChange : () => {}}
footerVisible={viewMode === "project" && !!currentProject}
modalOpen={modalManager.anyModalOpen}
onOpenSettings={handleOpenSettings}
@@ -346,7 +393,7 @@ function AppInner() {
mailboxUnreadCount={modalManager.mailboxUnreadCount}
onOpenGitManager={modalManager.openGitManager}
onOpenWorkflowSteps={modalManager.openWorkflowSteps}
onOpenMissions={viewMode === "project" && currentProject ? modalManager.openMissions : undefined}
onOpenMissions={viewMode === "project" && currentProject ? handleOpenMissionsView : undefined}
onOpenSchedules={modalManager.openSchedules}
onOpenScripts={modalManager.openScripts}
onToggleTerminal={modalManager.toggleTerminal}

View File

@@ -153,11 +153,6 @@ describe("tablet header controls", () => {
expect(screen.queryByTitle("Workflow Steps")).toBeNull();
});
it("does not render missions button inline on tablet", () => {
renderTabletHeader({ onOpenMissions: noop });
expect(screen.queryByTitle("Mission Manager")).toBeNull();
});
// ── Overflow menu on tablet ────────────────────────────────────
it("renders overflow menu trigger on tablet", () => {
@@ -235,12 +230,6 @@ describe("tablet header controls", () => {
expect(screen.getByTestId("overflow-workflow-steps-btn")).toBeDefined();
});
it("overflow menu contains missions on tablet when provided", () => {
renderTabletHeader({ onOpenMissions: noop });
fireEvent.click(screen.getByTitle("More header actions"));
expect(screen.getByTestId("overflow-missions-btn")).toBeDefined();
});
// ── Overflow menu callbacks work on tablet ─────────────────────
it("calls onOpenSettings from overflow menu on tablet", () => {
@@ -464,7 +453,6 @@ describe("tablet header controls", () => {
onOpenUsage: noop,
onOpenActivityLog: noop,
onOpenWorkflowSteps: noop,
onOpenMissions: noop,
onOpenFiles: noop,
onOpenGitManager: noop,
});

View File

@@ -18,7 +18,6 @@ import { NewTaskModal } from "./NewTaskModal";
import { ActivityLogModal } from "./ActivityLogModal";
import { GitManagerModal } from "./GitManagerModal";
import { WorkflowStepManager } from "./WorkflowStepManager";
import { MissionManager } from "./MissionManager";
import { AgentListModal } from "./AgentListModal";
import { MailboxModal } from "./MailboxModal";
import { SetupWizardModal } from "./SetupWizardModal";
@@ -208,22 +207,6 @@ export function AppModals({
projectId={projectId}
/>
<MissionManager
isOpen={modalManager.missionsOpen}
onClose={modalManager.closeMissions}
addToast={addToast}
projectId={projectId}
resumeSessionId={modalManager.missionResumeSessionId}
targetMissionId={modalManager.missionTargetId}
availableTasks={tasks.map((task) => ({ id: task.id, title: task.title }))}
onSelectTask={(taskId) => {
const task = tasks.find((candidate) => candidate.id === taskId);
if (task) {
modalManager.openDetailTask(task as TaskDetail);
}
}}
/>
<AgentListModal
isOpen={modalManager.agentsOpen}
onClose={modalManager.closeAgents}

View File

@@ -888,7 +888,6 @@ describe("Header", () => {
onOpenUsage: noop,
onOpenActivityLog: noop,
onOpenWorkflowSteps: noop,
onOpenMissions: noop,
onOpenFiles: noop,
onOpenGitManager: noop,
onOpenScripts: noop,
@@ -931,7 +930,6 @@ describe("Header", () => {
onOpenUsage: noop,
onOpenActivityLog: noop,
onOpenWorkflowSteps: noop,
onOpenMissions: noop,
onOpenFiles: noop,
onOpenGitManager: noop,
}, "mobile");

View File

@@ -41,7 +41,6 @@ export interface HeaderProps {
onOpenGitManager?: () => void;
onOpenNodes?: () => void;
onOpenWorkflowSteps?: () => void;
onOpenMissions?: () => void;
onOpenScripts?: () => void;
onRunScript?: (name: string, command: string) => void;
onToggleTerminal?: () => void;
@@ -52,8 +51,8 @@ export interface HeaderProps {
enginePaused?: boolean;
onToggleGlobalPause?: () => void;
onToggleEnginePause?: () => void;
view?: "board" | "list" | "agents";
onChangeView?: (view: "board" | "list" | "agents") => void;
view?: "board" | "list" | "agents" | "missions";
onChangeView?: (view: "board" | "list" | "agents" | "missions") => void;
searchQuery?: string;
onSearchChange?: (query: string) => void;
/** Multi-project props */
@@ -81,7 +80,6 @@ export function Header({
onOpenGitManager,
onOpenNodes,
onOpenWorkflowSteps,
onOpenMissions,
onOpenScripts,
onRunScript,
onToggleTerminal,
@@ -385,6 +383,15 @@ export function Header({
>
<Bot size={16} />
</button>
<button
className={`view-toggle-btn${view === "missions" ? " active" : ""}`}
onClick={() => onChangeView("missions")}
title="Missions view"
aria-label="Missions view"
aria-pressed={view === "missions"}
>
<Target size={16} />
</button>
</div>
)}
@@ -519,18 +526,6 @@ export function Header({
</button>
)}
{/* Missions - desktop only (moved to overflow on mobile/tablet) */}
{!isCompact && onOpenMissions && (
<button
className="btn-icon"
onClick={onOpenMissions}
title="Mission Manager"
data-testid="missions-btn"
>
<Target size={16} />
</button>
)}
{/* Scripts - desktop only (moved to overflow on mobile/tablet) */}
{!isCompact && onOpenScripts && onRunScript && (
<QuickScriptsDropdown
@@ -802,18 +797,6 @@ export function Header({
<span>Workflow Steps</span>
</button>
)}
{/* Missions - in overflow on mobile */}
{onOpenMissions && (
<button
className="mobile-overflow-item"
onClick={() => handleOverflowAction(onOpenMissions)}
role="menuitem"
data-testid="overflow-missions-btn"
>
<Target size={16} />
<span>Missions</span>
</button>
)}
{/* Settings - always last in overflow menu */}
<button
className="mobile-overflow-item"

View File

@@ -71,6 +71,7 @@ import type { AutopilotStatus as AutopilotStatusType, AutopilotState } from "./m
interface MissionManagerProps {
isOpen: boolean;
isInline?: boolean;
onClose: () => void;
addToast: (message: string, type?: ToastType) => void;
projectId?: string;
@@ -174,7 +175,8 @@ const EMPTY_FEATURE_FORM: FeatureFormData = {
status: "defined",
};
export function MissionManager({ isOpen, onClose, addToast, projectId, onSelectTask, availableTasks = [], resumeSessionId, targetMissionId }: MissionManagerProps) {
export function MissionManager({ isOpen, isInline = false, onClose, addToast, projectId, onSelectTask, availableTasks = [], resumeSessionId, targetMissionId }: MissionManagerProps) {
const isActive = isInline || isOpen;
const [missions, setMissions] = useState<MissionWithSummary[]>([]);
const [selectedMission, setSelectedMission] = useState<MissionWithHierarchy | null>(null);
const [loading, setLoading] = useState(true);
@@ -213,10 +215,10 @@ export function MissionManager({ isOpen, onClose, addToast, projectId, onSelectT
// Auto-open interview modal when resuming a session
useEffect(() => {
if (isOpen && resumeSessionId) {
if (isActive && resumeSessionId) {
setShowInterviewModal(true);
}
}, [isOpen, resumeSessionId]);
}, [isActive, resumeSessionId]);
// Delete confirmation
const [deleteConfirmId, setDeleteConfirmId] = useState<{ type: string; id: string } | null>(null);
@@ -257,27 +259,27 @@ export function MissionManager({ isOpen, onClose, addToast, projectId, onSelectT
}, [addToast, projectId]);
useEffect(() => {
if (isOpen) {
if (isActive) {
loadMissions();
setSelectedMission(null);
}
}, [isOpen, loadMissions]);
}, [isActive, loadMissions]);
// Auto-load target mission when specified
const targetLoadedRef = useRef<string | null>(null);
useEffect(() => {
if (isOpen && targetMissionId && targetLoadedRef.current !== targetMissionId && missions.length > 0) {
if (isActive && targetMissionId && targetLoadedRef.current !== targetMissionId && missions.length > 0) {
targetLoadedRef.current = targetMissionId;
loadMissionDetail(targetMissionId);
}
}, [isOpen, targetMissionId, missions, loadMissionDetail]);
}, [isActive, targetMissionId, missions, loadMissionDetail]);
// Reset target tracking when modal closes
useEffect(() => {
if (!isOpen) {
if (!isActive) {
targetLoadedRef.current = null;
}
}, [isOpen]);
}, [isActive]);
// Mission handlers
const handleCreateMission = useCallback(() => {
@@ -801,7 +803,7 @@ export function MissionManager({ isOpen, onClose, addToast, projectId, onSelectT
// Escape key handling
useEffect(() => {
if (!isOpen) return;
if (!isActive) return;
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") {
onClose();
@@ -809,24 +811,19 @@ export function MissionManager({ isOpen, onClose, addToast, projectId, onSelectT
};
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [isOpen, onClose]);
}, [isActive, onClose]);
if (!isOpen) return null;
if (!isActive) return null;
return (
const manager = (
<div
className="mission-manager-overlay open"
onClick={(e) => e.target === e.currentTarget && onClose()}
data-testid="mission-manager-overlay"
ref={modalRef}
className={`mission-manager${isInline ? " mission-manager--inline" : ""}`}
role={isInline ? undefined : "dialog"}
aria-modal={isInline ? undefined : true}
aria-label={isInline ? undefined : "Mission Manager"}
data-testid="mission-manager-dialog"
>
<div
ref={modalRef}
className="mission-manager"
role="dialog"
aria-modal="true"
aria-label="Mission Manager"
data-testid="mission-manager-dialog"
>
{/* ── Header ── */}
<div className="mission-manager__header">
<div className="mission-manager__header-title">
@@ -1800,17 +1797,40 @@ export function MissionManager({ isOpen, onClose, addToast, projectId, onSelectT
</div>
)}
</div>
);
<MissionInterviewModal
isOpen={showInterviewModal}
onClose={() => setShowInterviewModal(false)}
onMissionCreated={() => {
loadMissions();
addToast("Mission created from AI interview", "success");
}}
projectId={projectId}
resumeSessionId={resumeSessionId}
/>
</div>
const interviewModal = (
<MissionInterviewModal
isOpen={showInterviewModal}
onClose={() => setShowInterviewModal(false)}
onMissionCreated={() => {
loadMissions();
addToast("Mission created from AI interview", "success");
}}
projectId={projectId}
resumeSessionId={resumeSessionId}
/>
);
if (isInline) {
return (
<>
{manager}
{interviewModal}
</>
);
}
return (
<>
<div
className="mission-manager-overlay open"
onClick={(e) => e.target === e.currentTarget && onClose()}
data-testid="mission-manager-overlay"
>
{manager}
</div>
{interviewModal}
</>
);
}

View File

@@ -19,9 +19,9 @@ import { useViewportMode } from "./Header";
export interface MobileNavBarProps {
/** Current task view mode */
view: "board" | "list" | "agents";
view: "board" | "list" | "agents" | "missions";
/** Change task view handler */
onChangeView: (view: "board" | "list" | "agents") => void;
onChangeView: (view: "board" | "list" | "agents" | "missions") => void;
/** Whether the ExecutorStatusBar footer is visible */
footerVisible: boolean;
/** Whether any full-screen modal is currently open (hides the tab bar) */

View File

@@ -486,7 +486,7 @@ describe("App mission wiring", () => {
localStorage.removeItem("kb-dashboard-view-mode");
});
it("hides mission controls when no project is selected", async () => {
it("hides missions view toggle when no project is selected", async () => {
mockCurrentProjectState.currentProject = null;
mockProjectsState.projects = [];
@@ -496,10 +496,10 @@ describe("App mission wiring", () => {
expect(fetchSettings).toHaveBeenCalled();
});
expect(screen.queryByTestId("missions-btn")).toBeNull();
expect(screen.queryByTitle("Missions view")).toBeNull();
});
it("shows mission controls in project view when a project is selected", async () => {
it("shows missions view toggle in project view when a project is selected", async () => {
localStorage.setItem("kb-dashboard-view-mode", "project");
mockCurrentProjectState.currentProject = {
id: "proj_123",
@@ -514,7 +514,7 @@ describe("App mission wiring", () => {
render(<App />);
await waitFor(() => {
expect(screen.getByTestId("missions-btn")).toBeTruthy();
expect(screen.getByTitle("Missions view")).toBeTruthy();
});
});
});

View File

@@ -98,16 +98,6 @@ describe("Header", () => {
expect(btn).toBeDefined();
});
it("renders the missions button when mission management is available", () => {
const onOpen = vi.fn();
render(<Header onOpenMissions={onOpen} />);
expect(screen.getByTestId("missions-btn")).toBeDefined();
});
it("does not render the missions button when mission management is unavailable", () => {
render(<Header />);
expect(screen.queryByTestId("missions-btn")).toBeNull();
});
it("calls onOpenGitHubImport when import button is clicked", () => {
const onOpen = vi.fn();
@@ -305,6 +295,39 @@ describe("Header", () => {
expect(boardBtn.getAttribute("aria-pressed")).toBe("false");
});
// ── Missions View Toggle ────────────────────────────────────────
it("renders missions view button in view toggle when onChangeView is provided", () => {
const onChangeView = vi.fn();
render(<Header view="board" onChangeView={onChangeView} />);
const missionsBtn = screen.getByTitle("Missions view");
expect(missionsBtn).toBeDefined();
});
it("calls onChangeView with 'missions' when missions view button is clicked", () => {
const onChangeView = vi.fn();
render(<Header view="board" onChangeView={onChangeView} />);
const missionsBtn = screen.getByTitle("Missions view");
fireEvent.click(missionsBtn);
expect(onChangeView).toHaveBeenCalledWith("missions");
});
it("marks missions view button as active when view is 'missions'", () => {
const onChangeView = vi.fn();
render(<Header view="missions" onChangeView={onChangeView} />);
const missionsBtn = screen.getByTitle("Missions view");
expect(missionsBtn.className).toContain("active");
expect(missionsBtn.getAttribute("aria-pressed")).toBe("true");
});
it("does not mark missions view button as active when view is 'board'", () => {
const onChangeView = vi.fn();
render(<Header view="board" onChangeView={onChangeView} />);
const missionsBtn = screen.getByTitle("Missions view");
expect(missionsBtn.className).not.toContain("active");
expect(missionsBtn.getAttribute("aria-pressed")).toBe("false");
});
// ── Search Visibility by View ─────────────────────────────────────
it("shows search input when view is 'board'", () => {
@@ -770,14 +793,6 @@ describe("Header", () => {
expect(screen.queryByTestId("overflow-project-selector-btn")).not.toBeNull();
});
it("missions overflow menu item calls onOpenMissions when clicked", () => {
const onOpenMissions = vi.fn();
render(<Header onOpenSettings={vi.fn()} onOpenMissions={onOpenMissions} />);
fireEvent.click(screen.getByTitle("More header actions"));
fireEvent.click(screen.getByText("Missions"));
expect(onOpenMissions).toHaveBeenCalledOnce();
});
it("workflow steps overflow menu item calls onOpenWorkflowSteps when clicked", () => {
const onOpenWorkflowSteps = vi.fn();
render(<Header onOpenSettings={vi.fn()} onOpenWorkflowSteps={onOpenWorkflowSteps} />);

View File

@@ -47,9 +47,6 @@ export interface ModalManager {
mailboxAgents: Agent[];
gitManagerOpen: boolean;
workflowStepsOpen: boolean;
missionsOpen: boolean;
missionResumeSessionId: string | undefined;
missionTargetId: string | undefined;
agentsOpen: boolean;
scriptsOpen: boolean;
setupWizardOpen: boolean;
@@ -106,11 +103,6 @@ export interface ModalManager {
openWorkflowSteps: () => void;
closeWorkflowSteps: () => void;
openMissions: () => void;
openMissionById: (missionId: string) => void;
openMissionWithSession: (sessionId: string) => void;
closeMissions: () => void;
openAgents: () => void;
closeAgents: () => void;
@@ -162,9 +154,6 @@ export function useModalManager(options: UseModalManagerOptions): ModalManager {
const [mailboxAgents, setMailboxAgents] = useState<Agent[]>([]);
const [gitManagerOpen, setGitManagerOpen] = useState(false);
const [workflowStepsOpen, setWorkflowStepsOpen] = useState(false);
const [missionsOpen, setMissionsOpen] = useState(false);
const [missionResumeSessionId, setMissionResumeSessionId] = useState<string | undefined>(undefined);
const [missionTargetId, setMissionTargetId] = useState<string | undefined>(undefined);
const [agentsOpen, setAgentsOpen] = useState(false);
const [scriptsOpen, setScriptsOpen] = useState(false);
const [setupWizardOpen, setSetupWizardOpen] = useState(false);
@@ -182,7 +171,6 @@ export function useModalManager(options: UseModalManagerOptions): ModalManager {
mailboxOpen ||
gitManagerOpen ||
workflowStepsOpen ||
missionsOpen ||
scriptsOpen ||
agentsOpen ||
usageOpen ||
@@ -303,21 +291,6 @@ export function useModalManager(options: UseModalManagerOptions): ModalManager {
const openWorkflowSteps = useCallback(() => setWorkflowStepsOpen(true), []);
const closeWorkflowSteps = useCallback(() => setWorkflowStepsOpen(false), []);
const openMissions = useCallback(() => setMissionsOpen(true), []);
const openMissionById = useCallback((missionId: string) => {
setMissionTargetId(missionId);
setMissionsOpen(true);
}, []);
const openMissionWithSession = useCallback((sessionId: string) => {
setMissionResumeSessionId(sessionId);
setMissionsOpen(true);
}, []);
const closeMissions = useCallback(() => {
setMissionsOpen(false);
setMissionResumeSessionId(undefined);
setMissionTargetId(undefined);
}, []);
const openAgents = useCallback(() => setAgentsOpen(true), []);
const closeAgents = useCallback(() => setAgentsOpen(false), []);
@@ -380,9 +353,6 @@ export function useModalManager(options: UseModalManagerOptions): ModalManager {
mailboxAgents,
gitManagerOpen,
workflowStepsOpen,
missionsOpen,
missionResumeSessionId,
missionTargetId,
agentsOpen,
scriptsOpen,
setupWizardOpen,
@@ -423,10 +393,6 @@ export function useModalManager(options: UseModalManagerOptions): ModalManager {
closeGitManager,
openWorkflowSteps,
closeWorkflowSteps,
openMissions,
openMissionById,
openMissionWithSession,
closeMissions,
openAgents,
closeAgents,
openScripts,

View File

@@ -4,7 +4,7 @@ import type { ProjectInfo } from "../api";
import { getScopedItem, setScopedItem } from "../utils/projectStorage";
export type ViewMode = "overview" | "project";
export type TaskView = "board" | "list" | "agents";
export type TaskView = "board" | "list" | "agents" | "missions";
interface UseViewStateOptions {
projectsLoading: boolean;
@@ -48,7 +48,7 @@ export function useViewState(options: UseViewStateOptions): UseViewStateResult {
const [taskView, setTaskView] = useState<TaskView>(() => {
const saved = getScopedItem("kb-dashboard-task-view");
if (saved === "board" || saved === "list" || saved === "agents") return saved;
if (saved === "board" || saved === "list" || saved === "agents" || saved === "missions") return saved;
return "board";
});
@@ -58,7 +58,7 @@ export function useViewState(options: UseViewStateOptions): UseViewStateResult {
useEffect(() => {
const saved = getScopedItem("kb-dashboard-task-view", currentProject?.id);
if (saved === "board" || saved === "list" || saved === "agents") {
if (saved === "board" || saved === "list" || saved === "agents" || saved === "missions") {
setTaskView(saved);
return;
}

View File

@@ -20357,6 +20357,18 @@ body[data-color-theme="terminal"][data-theme="light"]::before {
box-shadow: var(--shadow-lg);
}
.mission-manager--inline {
position: relative;
width: 100%;
max-width: 100%;
height: 100%;
max-height: 100%;
overflow-y: auto;
border-radius: 0;
box-shadow: none;
border: none;
}
/* ── Header ── */
.mission-manager__header {
flex-shrink: 0;
@@ -21268,6 +21280,10 @@ body[data-color-theme="terminal"][data-theme="light"]::before {
border: none;
}
.mission-manager--inline {
max-height: 100%;
}
.mission-manager__header {
padding: var(--space-sm) var(--space-md);
}