- Add AgentsView as a third view option alongside Board and List - Add view toggle button with board/list/agents options in Header - Integrate agents monitoring directly into main dashboard navigation - Remove separate Agents modal button from header - Update App.tsx to handle agents view state - Add comprehensive tests for AgentsView component and Header view toggle
437 lines
15 KiB
TypeScript
437 lines
15 KiB
TypeScript
import { useState, useCallback, useEffect } from "react";
|
|
import type { TaskDetail, TaskCreateInput, Task, ThemeMode } from "@fusion/core";
|
|
import { fetchConfig, fetchSettings, fetchAuthStatus, updateSettings, fetchModels } from "./api";
|
|
import type { ModelInfo } from "./api";
|
|
import { Header } from "./components/Header";
|
|
import { Board } from "./components/Board";
|
|
import { ListView } from "./components/ListView";
|
|
import { TaskDetailModal } from "./components/TaskDetailModal";
|
|
import { TerminalModal } from "./components/TerminalModal";
|
|
import { FileBrowserModal } from "./components/FileBrowserModal";
|
|
import { SettingsModal } from "./components/SettingsModal";
|
|
import { PlanningModeModal } from "./components/PlanningModeModal";
|
|
import { SubtaskBreakdownModal } from "./components/SubtaskBreakdownModal";
|
|
import type { SectionId } from "./components/SettingsModal";
|
|
import { ToastContainer } from "./components/ToastContainer";
|
|
import { GitHubImportModal } from "./components/GitHubImportModal";
|
|
import { GitManagerModal } from "./components/GitManagerModal";
|
|
import { UsageIndicator } from "./components/UsageIndicator";
|
|
import { NewTaskModal } from "./components/NewTaskModal";
|
|
import { ScheduledTasksModal } from "./components/ScheduledTasksModal";
|
|
import { ActivityLogModal } from "./components/ActivityLogModal";
|
|
import { WorkflowStepManager } from "./components/WorkflowStepManager";
|
|
import { AgentListModal } from "./components/AgentListModal";
|
|
import { AgentsView } from "./components/AgentsView";
|
|
import { useTasks } from "./hooks/useTasks";
|
|
import { ToastProvider, useToast } from "./hooks/useToast";
|
|
import { useTheme } from "./hooks/useTheme";
|
|
|
|
function AppInner() {
|
|
const [newTaskModalOpen, setNewTaskModalOpen] = useState(false);
|
|
const [isPlanningOpen, setIsPlanningOpen] = useState(false);
|
|
const [planningInitialPlan, setPlanningInitialPlan] = useState<string | null>(null);
|
|
const [isSubtaskOpen, setIsSubtaskOpen] = useState(false);
|
|
const [subtaskInitialDescription, setSubtaskInitialDescription] = useState<string | null>(null);
|
|
const [detailTask, setDetailTask] = useState<TaskDetail | null>(null);
|
|
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
const [schedulesOpen, setSchedulesOpen] = useState(false);
|
|
const [githubImportOpen, setGitHubImportOpen] = useState(false);
|
|
const [usageOpen, setUsageOpen] = useState(false);
|
|
const [terminalOpen, setTerminalOpen] = useState(false);
|
|
const [filesOpen, setFilesOpen] = useState(false);
|
|
const [fileBrowserWorkspace, setFileBrowserWorkspace] = useState("project");
|
|
const [activityLogOpen, setActivityLogOpen] = useState(false);
|
|
const [gitManagerOpen, setGitManagerOpen] = useState(false);
|
|
const [workflowStepsOpen, setWorkflowStepsOpen] = useState(false);
|
|
const [agentsOpen, setAgentsOpen] = useState(false);
|
|
const [settingsInitialSection, setSettingsInitialSection] = useState<SectionId | undefined>(undefined);
|
|
const [maxConcurrent, setMaxConcurrent] = useState(2);
|
|
const [rootDir, setRootDir] = useState<string>(".");
|
|
const [autoMerge, setAutoMerge] = useState(true);
|
|
const [globalPaused, setGlobalPaused] = useState(false);
|
|
const [enginePaused, setEnginePaused] = useState(false);
|
|
const [view, setView] = useState<"board" | "list" | "agents">(() => {
|
|
// Initialize from localStorage if available
|
|
if (typeof window !== "undefined") {
|
|
const saved = localStorage.getItem("kb-dashboard-view");
|
|
if (saved === "list" || saved === "board" || saved === "agents") {
|
|
return saved;
|
|
}
|
|
}
|
|
return "board";
|
|
});
|
|
const [searchQuery, setSearchQuery] = useState("");
|
|
const [githubTokenConfigured, setGithubTokenConfigured] = useState(false);
|
|
const [availableModels, setAvailableModels] = useState<ModelInfo[]>([]);
|
|
const { tasks, createTask, moveTask, deleteTask, mergeTask, retryTask, updateTask, duplicateTask, archiveTask, unarchiveTask, archiveAllDone } = useTasks();
|
|
|
|
// Theme management
|
|
const { themeMode, colorTheme, setThemeMode, setColorTheme } = useTheme();
|
|
|
|
// Theme toggle handler: cycles Dark → Light → System → Dark
|
|
const handleToggleTheme = useCallback(() => {
|
|
const cycle: ThemeMode[] = ["dark", "light", "system"];
|
|
const currentIndex = cycle.indexOf(themeMode);
|
|
const nextMode = cycle[(currentIndex + 1) % cycle.length];
|
|
setThemeMode(nextMode);
|
|
}, [themeMode, setThemeMode]);
|
|
|
|
useEffect(() => {
|
|
fetchConfig()
|
|
.then((cfg) => {
|
|
setMaxConcurrent(cfg.maxConcurrent);
|
|
setRootDir(cfg.rootDir);
|
|
})
|
|
.catch(() => {/* keep default */});
|
|
fetchSettings()
|
|
.then((s) => {
|
|
setAutoMerge(!!s.autoMerge);
|
|
setGlobalPaused(!!s.globalPause);
|
|
setEnginePaused(!!s.enginePaused);
|
|
setGithubTokenConfigured(!!s.githubTokenConfigured);
|
|
})
|
|
.catch(() => {/* keep default */});
|
|
fetchAuthStatus()
|
|
.then(({ providers }) => {
|
|
if (providers.length > 0 && providers.every((p) => !p.authenticated)) {
|
|
setSettingsOpen(true);
|
|
setSettingsInitialSection("authentication");
|
|
}
|
|
})
|
|
.catch(() => {/* fail silently — do not auto-open */});
|
|
}, []);
|
|
|
|
// Fetch available models
|
|
useEffect(() => {
|
|
fetchModels()
|
|
.then((models) => setAvailableModels(models))
|
|
.catch(() => {/* keep empty array on failure */});
|
|
}, []);
|
|
const { toasts, addToast, removeToast } = useToast();
|
|
|
|
// Persist view preference to localStorage
|
|
useEffect(() => {
|
|
localStorage.setItem("kb-dashboard-view", view);
|
|
}, [view]);
|
|
|
|
const handleChangeView = useCallback((newView: "board" | "list" | "agents") => {
|
|
setView(newView);
|
|
}, []);
|
|
|
|
const handleNewTaskOpen = useCallback(() => setNewTaskModalOpen(true), []);
|
|
const handleNewTaskClose = useCallback(() => setNewTaskModalOpen(false), []);
|
|
|
|
const handleBoardQuickCreate = useCallback(
|
|
async (input: TaskCreateInput): Promise<void> => {
|
|
await createTask({ ...input, column: "triage" });
|
|
},
|
|
[createTask],
|
|
);
|
|
|
|
const handleModalCreate = useCallback(
|
|
async (input: TaskCreateInput): Promise<Task> => {
|
|
const task = await createTask({ ...input, column: "triage" });
|
|
return task;
|
|
},
|
|
[createTask],
|
|
);
|
|
|
|
// Planning mode handlers
|
|
const handlePlanningOpen = useCallback(() => setIsPlanningOpen(true), []);
|
|
const handlePlanningClose = useCallback(() => {
|
|
setIsPlanningOpen(false);
|
|
setPlanningInitialPlan(null);
|
|
}, []);
|
|
const handlePlanningTaskCreated = useCallback((task: Task) => {
|
|
addToast(`Created ${task.id} from planning mode`, "success");
|
|
setIsPlanningOpen(false);
|
|
setPlanningInitialPlan(null);
|
|
}, [addToast]);
|
|
|
|
// Handle planning mode from new task dialog
|
|
const handleNewTaskPlanningMode = useCallback((initialPlan: string) => {
|
|
setPlanningInitialPlan(initialPlan);
|
|
setIsPlanningOpen(true);
|
|
}, []);
|
|
|
|
// Handle subtask breakdown from inline/quick create
|
|
const handleSubtaskBreakdown = useCallback((description: string) => {
|
|
setSubtaskInitialDescription(description);
|
|
setIsSubtaskOpen(true);
|
|
}, []);
|
|
|
|
const handleSubtaskClose = useCallback(() => {
|
|
setIsSubtaskOpen(false);
|
|
setSubtaskInitialDescription(null);
|
|
}, []);
|
|
|
|
const handleSubtaskTasksCreated = useCallback((createdTasks: Task[]) => {
|
|
const ids = createdTasks.map((task) => task.id).join(", ");
|
|
addToast(`Created ${ids} from subtask breakdown`, "success");
|
|
setIsSubtaskOpen(false);
|
|
setSubtaskInitialDescription(null);
|
|
}, [addToast]);
|
|
|
|
// Usage indicator handlers
|
|
const handleOpenUsage = useCallback(() => setUsageOpen(true), []);
|
|
const handleCloseUsage = useCallback(() => setUsageOpen(false), []);
|
|
|
|
// Schedules modal handlers
|
|
const handleOpenSchedules = useCallback(() => setSchedulesOpen(true), []);
|
|
const handleCloseSchedules = useCallback(() => setSchedulesOpen(false), []);
|
|
|
|
const handleToggleAutoMerge = useCallback(async () => {
|
|
const next = !autoMerge;
|
|
setAutoMerge(next);
|
|
try {
|
|
await updateSettings({ autoMerge: next });
|
|
} catch {
|
|
setAutoMerge(!next); // revert on failure
|
|
}
|
|
}, [autoMerge]);
|
|
|
|
const handleToggleGlobalPause = useCallback(async () => {
|
|
const next = !globalPaused;
|
|
setGlobalPaused(next);
|
|
try {
|
|
await updateSettings({ globalPause: next });
|
|
} catch {
|
|
setGlobalPaused(!next); // revert on failure
|
|
}
|
|
}, [globalPaused]);
|
|
|
|
const handleToggleEnginePause = useCallback(async () => {
|
|
const next = !enginePaused;
|
|
setEnginePaused(next);
|
|
try {
|
|
await updateSettings({ enginePaused: next });
|
|
} catch {
|
|
setEnginePaused(!next); // revert on failure
|
|
}
|
|
}, [enginePaused]);
|
|
|
|
const handleDetailOpen = useCallback((task: TaskDetail) => {
|
|
setDetailTask(task);
|
|
}, []);
|
|
|
|
const handleDetailClose = useCallback(() => setDetailTask(null), []);
|
|
|
|
const handleGitHubImport = useCallback((task: Task) => {
|
|
addToast(`Imported ${task.id} from GitHub`, "success");
|
|
}, [addToast]);
|
|
|
|
const handleToggleTerminal = useCallback(() => {
|
|
setTerminalOpen((prev) => !prev);
|
|
}, []);
|
|
|
|
const handleTerminalClose = useCallback(() => {
|
|
setTerminalOpen(false);
|
|
}, []);
|
|
|
|
const handleOpenFiles = useCallback(() => {
|
|
setFilesOpen(true);
|
|
}, []);
|
|
|
|
const handleOpenFilesForTask = useCallback((taskId: string) => {
|
|
setFileBrowserWorkspace(taskId);
|
|
setFilesOpen(true);
|
|
}, []);
|
|
|
|
const handleWorkspaceChange = useCallback((workspace: string) => {
|
|
setFileBrowserWorkspace(workspace);
|
|
}, []);
|
|
|
|
// Activity log handlers
|
|
const handleOpenActivityLog = useCallback(() => setActivityLogOpen(true), []);
|
|
const handleCloseActivityLog = useCallback(() => setActivityLogOpen(false), []);
|
|
|
|
// Git Manager handlers
|
|
const handleOpenGitManager = useCallback(() => setGitManagerOpen(true), []);
|
|
const handleCloseGitManager = useCallback(() => setGitManagerOpen(false), []);
|
|
|
|
// Agent handlers
|
|
const handleOpenAgents = useCallback(() => setAgentsOpen(true), []);
|
|
const handleCloseAgents = useCallback(() => setAgentsOpen(false), []);
|
|
|
|
return (
|
|
<>
|
|
<Header
|
|
onOpenSettings={() => setSettingsOpen(true)}
|
|
onOpenGitHubImport={() => setGitHubImportOpen(true)}
|
|
onOpenPlanning={handlePlanningOpen}
|
|
onOpenUsage={handleOpenUsage}
|
|
onOpenActivityLog={handleOpenActivityLog}
|
|
onOpenSchedules={handleOpenSchedules}
|
|
onOpenGitManager={handleOpenGitManager}
|
|
onOpenWorkflowSteps={() => setWorkflowStepsOpen(true)}
|
|
onOpenAgents={handleOpenAgents}
|
|
onToggleTerminal={handleToggleTerminal}
|
|
onOpenFiles={handleOpenFiles}
|
|
filesOpen={filesOpen}
|
|
globalPaused={globalPaused}
|
|
enginePaused={enginePaused}
|
|
onToggleGlobalPause={handleToggleGlobalPause}
|
|
onToggleEnginePause={handleToggleEnginePause}
|
|
view={view}
|
|
onChangeView={handleChangeView}
|
|
searchQuery={searchQuery}
|
|
onSearchChange={setSearchQuery}
|
|
/>
|
|
{view === "agents" ? (
|
|
<AgentsView addToast={addToast} />
|
|
) : view === "board" ? (
|
|
<Board
|
|
tasks={tasks}
|
|
maxConcurrent={maxConcurrent}
|
|
onMoveTask={moveTask}
|
|
onOpenDetail={handleDetailOpen}
|
|
addToast={addToast}
|
|
onQuickCreate={handleBoardQuickCreate}
|
|
onNewTask={handleNewTaskOpen}
|
|
onPlanningMode={handleNewTaskPlanningMode}
|
|
onSubtaskBreakdown={handleSubtaskBreakdown}
|
|
autoMerge={autoMerge}
|
|
onToggleAutoMerge={handleToggleAutoMerge}
|
|
globalPaused={globalPaused}
|
|
onUpdateTask={updateTask}
|
|
onArchiveTask={archiveTask}
|
|
onUnarchiveTask={unarchiveTask}
|
|
onArchiveAllDone={archiveAllDone}
|
|
searchQuery={searchQuery}
|
|
availableModels={availableModels}
|
|
onOpenFilesForTask={handleOpenFilesForTask}
|
|
/>
|
|
) : (
|
|
// List view now uses the same modal-based create flow as board view.
|
|
<ListView
|
|
tasks={tasks}
|
|
onMoveTask={moveTask}
|
|
onOpenDetail={handleDetailOpen}
|
|
addToast={addToast}
|
|
globalPaused={globalPaused}
|
|
onNewTask={handleNewTaskOpen}
|
|
onQuickCreate={handleBoardQuickCreate}
|
|
onPlanningMode={handleNewTaskPlanningMode}
|
|
onSubtaskBreakdown={handleSubtaskBreakdown}
|
|
availableModels={availableModels}
|
|
/>
|
|
)}
|
|
{detailTask && (
|
|
<TaskDetailModal
|
|
task={detailTask}
|
|
tasks={tasks}
|
|
onClose={handleDetailClose}
|
|
onOpenDetail={handleDetailOpen}
|
|
onMoveTask={moveTask}
|
|
onDeleteTask={deleteTask}
|
|
onMergeTask={mergeTask}
|
|
onRetryTask={retryTask}
|
|
onDuplicateTask={duplicateTask}
|
|
addToast={addToast}
|
|
githubTokenConfigured={githubTokenConfigured}
|
|
/>
|
|
)}
|
|
{settingsOpen && (
|
|
<SettingsModal
|
|
onClose={() => {
|
|
setSettingsOpen(false);
|
|
setSettingsInitialSection(undefined);
|
|
}}
|
|
addToast={addToast}
|
|
initialSection={settingsInitialSection}
|
|
themeMode={themeMode}
|
|
colorTheme={colorTheme}
|
|
onThemeModeChange={setThemeMode}
|
|
onColorThemeChange={setColorTheme}
|
|
/>
|
|
)}
|
|
<GitHubImportModal
|
|
isOpen={githubImportOpen}
|
|
onClose={() => setGitHubImportOpen(false)}
|
|
onImport={handleGitHubImport}
|
|
tasks={tasks}
|
|
/>
|
|
<PlanningModeModal
|
|
isOpen={isPlanningOpen}
|
|
onClose={handlePlanningClose}
|
|
onTaskCreated={handlePlanningTaskCreated}
|
|
tasks={tasks}
|
|
initialPlan={planningInitialPlan ?? undefined}
|
|
/>
|
|
<SubtaskBreakdownModal
|
|
isOpen={isSubtaskOpen}
|
|
onClose={handleSubtaskClose}
|
|
initialDescription={subtaskInitialDescription ?? ""}
|
|
onTasksCreated={handleSubtaskTasksCreated}
|
|
/>
|
|
<TerminalModal
|
|
isOpen={terminalOpen}
|
|
onClose={handleTerminalClose}
|
|
/>
|
|
{filesOpen && (
|
|
<FileBrowserModal
|
|
initialWorkspace={fileBrowserWorkspace}
|
|
isOpen={true}
|
|
onClose={() => setFilesOpen(false)}
|
|
onWorkspaceChange={handleWorkspaceChange}
|
|
/>
|
|
)}
|
|
<UsageIndicator
|
|
isOpen={usageOpen}
|
|
onClose={handleCloseUsage}
|
|
/>
|
|
{schedulesOpen && (
|
|
<ScheduledTasksModal
|
|
onClose={handleCloseSchedules}
|
|
addToast={addToast}
|
|
/>
|
|
)}
|
|
<NewTaskModal
|
|
isOpen={newTaskModalOpen}
|
|
onClose={handleNewTaskClose}
|
|
tasks={tasks}
|
|
onCreateTask={handleModalCreate}
|
|
addToast={addToast}
|
|
onPlanningMode={handleNewTaskPlanningMode}
|
|
onSubtaskBreakdown={handleSubtaskBreakdown}
|
|
/>
|
|
<ActivityLogModal
|
|
isOpen={activityLogOpen}
|
|
onClose={handleCloseActivityLog}
|
|
tasks={tasks}
|
|
onOpenTaskDetail={(taskId) => {
|
|
const task = tasks.find((t) => t.id === taskId);
|
|
if (task) {
|
|
handleDetailOpen(task as TaskDetail);
|
|
}
|
|
}}
|
|
/>
|
|
<GitManagerModal
|
|
isOpen={gitManagerOpen}
|
|
onClose={handleCloseGitManager}
|
|
tasks={tasks}
|
|
addToast={addToast}
|
|
/>
|
|
<WorkflowStepManager
|
|
isOpen={workflowStepsOpen}
|
|
onClose={() => setWorkflowStepsOpen(false)}
|
|
addToast={addToast}
|
|
/>
|
|
<AgentListModal
|
|
isOpen={agentsOpen}
|
|
onClose={handleCloseAgents}
|
|
addToast={addToast}
|
|
/>
|
|
<ToastContainer toasts={toasts} onRemove={removeToast} />
|
|
</>
|
|
);
|
|
}
|
|
|
|
export function App() {
|
|
return (
|
|
<ToastProvider>
|
|
<AppInner />
|
|
</ToastProvider>
|
|
);
|
|
}
|