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:
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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) */
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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} />);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user