- Unify stuck-task derivation using project settings (taskStuckTimeoutMs) instead of hardcoded values - Thread taskStuckTimeoutMs through Board, Column, WorktreeGroup, and TaskCard components - Add visual stuck indicator with pulsing animation on in-progress task cards - Resolve merge conflicts with isSearchActive prop from main (both props coexist) - Add taskStuck utility unit tests and update README with stuck indicator docs
797 lines
29 KiB
TypeScript
797 lines
29 KiB
TypeScript
import { useState, useCallback, useEffect, useRef } from "react";
|
|
import type { TaskDetail, TaskCreateInput, Task, ThemeMode } from "@fusion/core";
|
|
import { fetchConfig, fetchSettings, fetchAuthStatus, updateSettings, updateGlobalSettings, fetchModels, fetchTaskDetail, updateProject, unregisterProject } from "./api";
|
|
import type { ModelInfo, ProjectInfo } from "./api";
|
|
import { Header } from "./components/Header";
|
|
import { Board } from "./components/Board";
|
|
import { ListView } from "./components/ListView";
|
|
import { ProjectOverview } from "./components/ProjectOverview";
|
|
import { SetupWizardModal } from "./components/SetupWizardModal";
|
|
import { TaskDetailModal } from "./components/TaskDetailModal";
|
|
import { TerminalModal } from "./components/TerminalModal";
|
|
import { FileBrowserModal } from "./components/FileBrowserModal";
|
|
import { ChangedFilesModal } from "./components/ChangedFilesModal";
|
|
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 { MissionManager } from "./components/MissionManager";
|
|
import { AgentListModal } from "./components/AgentListModal";
|
|
import { AgentsView } from "./components/AgentsView";
|
|
import { ScriptsModal } from "./components/ScriptsModal";
|
|
import { ExecutorStatusBar } from "./components/ExecutorStatusBar";
|
|
import { useTasks } from "./hooks/useTasks";
|
|
import { useProjects } from "./hooks/useProjects";
|
|
import { useCurrentProject } from "./hooks/useCurrentProject";
|
|
import { ToastProvider, useToast } from "./hooks/useToast";
|
|
import { useTheme } from "./hooks/useTheme";
|
|
|
|
type ViewMode = "overview" | "project";
|
|
type TaskView = "board" | "list" | "agents";
|
|
|
|
function AppInner() {
|
|
const { toasts, addToast, removeToast } = useToast();
|
|
|
|
// Project management hooks - MUST be called before any conditional logic
|
|
const { projects, loading: projectsLoading, error: projectsError, refresh: refreshProjects, register: registerProject, update: updateProjectHook, unregister: unregisterProjectHook } = useProjects();
|
|
const { currentProject, setCurrentProject, clearCurrentProject, loading: currentProjectLoading } = useCurrentProject(projects);
|
|
|
|
// Tasks hook with project context
|
|
const { tasks, createTask, moveTask, deleteTask, mergeTask, retryTask, updateTask, duplicateTask, archiveTask, unarchiveTask, archiveAllDone } = useTasks(
|
|
currentProject ? { projectId: currentProject.id } : undefined
|
|
);
|
|
|
|
// Theme management
|
|
const { themeMode, colorTheme, setThemeMode, setColorTheme } = useTheme();
|
|
|
|
// View state
|
|
const [viewMode, setViewMode] = useState<ViewMode>(() => {
|
|
if (typeof window !== "undefined") {
|
|
const saved = localStorage.getItem("kb-dashboard-view-mode");
|
|
if (saved === "overview" || saved === "project") return saved;
|
|
}
|
|
return "overview";
|
|
});
|
|
|
|
const [taskView, setTaskView] = useState<TaskView>(() => {
|
|
if (typeof window !== "undefined") {
|
|
const saved = localStorage.getItem("kb-dashboard-task-view");
|
|
if (saved === "board" || saved === "list" || saved === "agents") return saved;
|
|
}
|
|
return "board";
|
|
});
|
|
|
|
// Modal states
|
|
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 [changedFilesState, setChangedFilesState] = useState<{ taskId: string; worktree: string | undefined; column: string } | null>(null);
|
|
const [activityLogOpen, setActivityLogOpen] = useState(false);
|
|
const [gitManagerOpen, setGitManagerOpen] = useState(false);
|
|
const [workflowStepsOpen, setWorkflowStepsOpen] = useState(false);
|
|
const [missionsOpen, setMissionsOpen] = useState(false);
|
|
const [agentsOpen, setAgentsOpen] = useState(false);
|
|
const [scriptsOpen, setScriptsOpen] = useState(false);
|
|
const [terminalInitialCommand, setTerminalInitialCommand] = useState<string | undefined>(undefined);
|
|
const [settingsInitialSection, setSettingsInitialSection] = useState<SectionId | undefined>(undefined);
|
|
const [setupWizardOpen, setSetupWizardOpen] = useState(false);
|
|
|
|
// Settings state
|
|
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 [searchQuery, setSearchQuery] = useState("");
|
|
const [taskStuckTimeoutMs, setTaskStuckTimeoutMs] = useState<number | undefined>(undefined);
|
|
const [githubTokenConfigured, setGithubTokenConfigured] = useState(false);
|
|
const [availableModels, setAvailableModels] = useState<ModelInfo[]>([]);
|
|
const [favoriteProviders, setFavoriteProviders] = useState<string[]>([]);
|
|
const [favoriteModels, setFavoriteModels] = useState<string[]>([]);
|
|
|
|
// Persist view mode
|
|
useEffect(() => {
|
|
localStorage.setItem("kb-dashboard-view-mode", viewMode);
|
|
}, [viewMode]);
|
|
|
|
// Persist task view
|
|
useEffect(() => {
|
|
localStorage.setItem("kb-dashboard-task-view", taskView);
|
|
}, [taskView]);
|
|
|
|
// Sync view mode when current project is restored from localStorage
|
|
useEffect(() => {
|
|
// Wait for both loading states to complete before syncing
|
|
if (projectsLoading || currentProjectLoading) return;
|
|
|
|
// If we have a restored current project but viewMode is overview, sync to project view
|
|
if (currentProject && viewMode === "overview") {
|
|
setViewMode("project");
|
|
}
|
|
}, [projectsLoading, currentProjectLoading, currentProject, viewMode]);
|
|
|
|
// Auto-open setup wizard on first run (no projects)
|
|
useEffect(() => {
|
|
// Wait for both loading states to complete before making decision
|
|
if (projectsLoading || currentProjectLoading) return;
|
|
|
|
// Don't open if wizard is already open
|
|
if (setupWizardOpen) return;
|
|
|
|
// Don't open if we have projects OR a saved current project
|
|
if (projects.length > 0 || currentProject) return;
|
|
|
|
// Only open when truly no projects exist and no project is being restored
|
|
const timer = setTimeout(() => {
|
|
setSetupWizardOpen(true);
|
|
}, 500);
|
|
return () => clearTimeout(timer);
|
|
}, [projectsLoading, projects.length, currentProjectLoading, currentProject, setupWizardOpen]);
|
|
|
|
// 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]);
|
|
|
|
// Initial data fetch
|
|
useEffect(() => {
|
|
fetchConfig(currentProject?.id)
|
|
.then((cfg) => {
|
|
setMaxConcurrent(cfg.maxConcurrent);
|
|
setRootDir(cfg.rootDir);
|
|
})
|
|
.catch(() => {/* keep default */});
|
|
fetchSettings(currentProject?.id)
|
|
.then((s) => {
|
|
setAutoMerge(!!s.autoMerge);
|
|
setGlobalPaused(!!s.globalPause);
|
|
setEnginePaused(!!s.enginePaused);
|
|
setGithubTokenConfigured(!!s.githubTokenConfigured);
|
|
setTaskStuckTimeoutMs(s.taskStuckTimeoutMs);
|
|
})
|
|
.catch(() => {/* keep default */});
|
|
fetchAuthStatus()
|
|
.then(({ providers }) => {
|
|
if (providers.length > 0 && providers.every((p) => !p.authenticated)) {
|
|
setSettingsOpen(true);
|
|
setSettingsInitialSection("authentication");
|
|
}
|
|
})
|
|
.catch(() => {/* fail silently */});
|
|
}, [currentProject?.id]);
|
|
|
|
// Fetch available models
|
|
useEffect(() => {
|
|
fetchModels()
|
|
.then((response) => {
|
|
setAvailableModels(response.models);
|
|
setFavoriteProviders(response.favoriteProviders);
|
|
setFavoriteModels(response.favoriteModels);
|
|
})
|
|
.catch(() => {/* keep empty array on failure */});
|
|
}, []);
|
|
|
|
// Favorite provider/model toggle handlers (shared by ListView bulk edit)
|
|
const handleToggleFavorite = useCallback(async (provider: string) => {
|
|
const currentFavorites = favoriteProviders;
|
|
const isFavorite = currentFavorites.includes(provider);
|
|
const newFavorites = isFavorite
|
|
? currentFavorites.filter((p) => p !== provider)
|
|
: [provider, ...currentFavorites];
|
|
|
|
setFavoriteProviders(newFavorites);
|
|
|
|
try {
|
|
await updateGlobalSettings({ favoriteProviders: newFavorites, favoriteModels });
|
|
} catch {
|
|
setFavoriteProviders(currentFavorites);
|
|
addToast("Failed to update favorites", "error");
|
|
}
|
|
}, [favoriteProviders, favoriteModels, addToast]);
|
|
|
|
const handleToggleModelFavorite = useCallback(async (modelId: string) => {
|
|
const currentFavorites = favoriteModels;
|
|
const isFavorite = currentFavorites.includes(modelId);
|
|
const newFavorites = isFavorite
|
|
? currentFavorites.filter((m) => m !== modelId)
|
|
: [modelId, ...currentFavorites];
|
|
|
|
setFavoriteModels(newFavorites);
|
|
|
|
try {
|
|
await updateGlobalSettings({ favoriteProviders, favoriteModels: newFavorites });
|
|
} catch {
|
|
setFavoriteModels(currentFavorites);
|
|
addToast("Failed to update model favorites", "error");
|
|
}
|
|
}, [favoriteModels, favoriteProviders, addToast]);
|
|
|
|
// Handle deep link to task on mount (with optional project context)
|
|
// Uses a ref to prevent duplicate fetches when setCurrentProject triggers
|
|
// a re-run of this effect during project switching.
|
|
const deepLinkFetchedRef = useRef(false);
|
|
// Tracks the task ID currently open from a deep link so that dismissing
|
|
// the modal can clean the URL (one-time open behaviour).
|
|
const deepLinkTaskIdRef = useRef<string | null>(null);
|
|
useEffect(() => {
|
|
const params = new URLSearchParams(window.location.search);
|
|
const projectParam = params.get("project");
|
|
const taskId = params.get("task");
|
|
|
|
// If no task to load, nothing to do
|
|
if (!taskId) return;
|
|
|
|
// Wait for projects to finish loading before resolving deep links.
|
|
// Without this guard, an empty projects list during loading would
|
|
// produce a false "project not found" error toast.
|
|
if (projectsLoading) return;
|
|
|
|
// If project param is present, validate it and switch project if needed
|
|
if (projectParam) {
|
|
const matchingProject = projects.find((p) => p.id === projectParam);
|
|
if (!matchingProject) {
|
|
addToast(`Project '${projectParam}' not found`, "error");
|
|
return;
|
|
}
|
|
// Switch to the project if it's different from the current one
|
|
if (currentProject?.id !== matchingProject.id) {
|
|
setCurrentProject(matchingProject);
|
|
}
|
|
}
|
|
|
|
// Skip if we've already fetched this task (prevents double-fetch when
|
|
// setCurrentProject causes this effect to re-run).
|
|
if (deepLinkFetchedRef.current) return;
|
|
deepLinkFetchedRef.current = true;
|
|
|
|
// Use project param as the authoritative project context for the fetch
|
|
// when present; otherwise fall back to the current/default project.
|
|
const taskProjectId = projectParam ?? currentProject?.id;
|
|
fetchTaskDetail(taskId, taskProjectId)
|
|
.then((detail) => {
|
|
setDetailTask(detail);
|
|
// Mark this as a deep-linked open so dismissal can clean the URL
|
|
deepLinkTaskIdRef.current = taskId;
|
|
})
|
|
.catch(() => {
|
|
addToast(`Task ${taskId} not found`, "error");
|
|
});
|
|
}, [addToast, projects, projectsLoading, currentProject, setCurrentProject]);
|
|
|
|
// View change handlers
|
|
const handleChangeTaskView = useCallback((newView: TaskView) => {
|
|
setTaskView(newView);
|
|
}, []);
|
|
|
|
// Project selection handlers
|
|
const handleSelectProject = useCallback((project: ProjectInfo) => {
|
|
setCurrentProject(project);
|
|
setViewMode("project");
|
|
}, [setCurrentProject]);
|
|
|
|
const handleViewAllProjects = useCallback(() => {
|
|
clearCurrentProject();
|
|
setViewMode("overview");
|
|
}, [clearCurrentProject]);
|
|
|
|
const handleAddProject = useCallback(() => {
|
|
setSetupWizardOpen(true);
|
|
}, []);
|
|
|
|
const handleSetupComplete = useCallback((project: ProjectInfo) => {
|
|
setSetupWizardOpen(false);
|
|
setCurrentProject(project);
|
|
setViewMode("project");
|
|
addToast(`Project ${project.name} registered successfully`, "success");
|
|
refreshProjects();
|
|
}, [setCurrentProject, addToast, refreshProjects]);
|
|
|
|
const handlePauseProject = useCallback(async (project: ProjectInfo) => {
|
|
try {
|
|
await updateProject(project.id, { status: "paused" });
|
|
addToast(`Project ${project.name} paused`, "success");
|
|
refreshProjects();
|
|
} catch {
|
|
addToast(`Failed to pause project ${project.name}`, "error");
|
|
}
|
|
}, [addToast, refreshProjects]);
|
|
|
|
const handleResumeProject = useCallback(async (project: ProjectInfo) => {
|
|
try {
|
|
await updateProject(project.id, { status: "active" });
|
|
addToast(`Project ${project.name} resumed`, "success");
|
|
refreshProjects();
|
|
} catch {
|
|
addToast(`Failed to resume project ${project.name}`, "error");
|
|
}
|
|
}, [addToast, refreshProjects]);
|
|
|
|
const handleRemoveProject = useCallback(async (project: ProjectInfo) => {
|
|
try {
|
|
await unregisterProject(project.id);
|
|
addToast(`Project ${project.name} removed`, "success");
|
|
// If we removed the current project, go back to overview
|
|
if (currentProject?.id === project.id) {
|
|
clearCurrentProject();
|
|
setViewMode("overview");
|
|
}
|
|
refreshProjects();
|
|
} catch {
|
|
addToast(`Failed to remove project ${project.name}`, "error");
|
|
}
|
|
}, [unregisterProject, currentProject, clearCurrentProject, addToast, refreshProjects]);
|
|
|
|
// Task handlers
|
|
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 }, currentProject?.id);
|
|
} catch {
|
|
setAutoMerge(!next); // revert on failure
|
|
}
|
|
}, [autoMerge, currentProject?.id]);
|
|
|
|
const handleToggleGlobalPause = useCallback(async () => {
|
|
const next = !globalPaused;
|
|
setGlobalPaused(next);
|
|
try {
|
|
await updateSettings({ globalPause: next }, currentProject?.id);
|
|
} catch {
|
|
setGlobalPaused(!next); // revert on failure
|
|
}
|
|
}, [globalPaused, currentProject?.id]);
|
|
|
|
const handleToggleEnginePause = useCallback(async () => {
|
|
const next = !enginePaused;
|
|
setEnginePaused(next);
|
|
try {
|
|
await updateSettings({ enginePaused: next }, currentProject?.id);
|
|
} catch {
|
|
setEnginePaused(!next); // revert on failure
|
|
}
|
|
}, [enginePaused, currentProject?.id]);
|
|
|
|
const handleDetailOpen = useCallback((task: TaskDetail) => {
|
|
setDetailTask(task);
|
|
}, []);
|
|
|
|
const handleDetailClose = useCallback(() => {
|
|
// If the modal was opened from a deep link (?task=...), remove the task
|
|
// param from the URL so refreshing does not reopen it. Preserve any other
|
|
// query parameters (e.g. ?project=...).
|
|
if (deepLinkTaskIdRef.current) {
|
|
const params = new URLSearchParams(window.location.search);
|
|
params.delete("task");
|
|
const qs = params.toString();
|
|
window.history.replaceState(
|
|
null,
|
|
"",
|
|
qs ? `${window.location.pathname}?${qs}` : window.location.pathname,
|
|
);
|
|
deepLinkTaskIdRef.current = null;
|
|
}
|
|
setDetailTask(null);
|
|
}, []);
|
|
|
|
const handleGitHubImport = useCallback((task: Task) => {
|
|
addToast(`Imported ${task.id} from GitHub`, "success");
|
|
}, [addToast]);
|
|
|
|
const handleToggleTerminal = useCallback(() => {
|
|
setTerminalOpen((prev) => !prev);
|
|
}, []);
|
|
|
|
const handleOpenFiles = useCallback(() => {
|
|
setFilesOpen(true);
|
|
}, []);
|
|
|
|
const handleOpenChangedFiles = useCallback((taskId: string, worktree: string | undefined, column: string) => {
|
|
setChangedFilesState({ taskId, worktree, column });
|
|
}, []);
|
|
|
|
const handleCloseChangedFiles = useCallback(() => {
|
|
setChangedFilesState(null);
|
|
}, []);
|
|
|
|
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 handleCloseAgents = useCallback(() => setAgentsOpen(false), []);
|
|
|
|
// Scripts handlers
|
|
const handleOpenScripts = useCallback(() => setScriptsOpen(true), []);
|
|
const handleCloseScripts = useCallback(() => setScriptsOpen(false), []);
|
|
const handleRunScript = useCallback(async (name: string, command: string) => {
|
|
// Close the scripts modal immediately so the terminal becomes the
|
|
// topmost surface once it opens.
|
|
setScriptsOpen(false);
|
|
|
|
// Launch the script command in the interactive terminal modal.
|
|
// Reset the initial command ref so the terminal knows to run the
|
|
// new command even if it is already open.
|
|
setTerminalInitialCommand(command);
|
|
setTerminalOpen(true);
|
|
}, []);
|
|
|
|
// Terminal close handler
|
|
const handleTerminalClose = useCallback(() => {
|
|
setTerminalOpen(false);
|
|
setTerminalInitialCommand(undefined);
|
|
}, []);
|
|
|
|
// Render main content based on view mode
|
|
const renderMainContent = () => {
|
|
if (viewMode === "overview") {
|
|
return (
|
|
<ProjectOverview
|
|
projects={projects}
|
|
loading={projectsLoading}
|
|
onSelectProject={handleSelectProject}
|
|
onAddProject={handleAddProject}
|
|
onPauseProject={handlePauseProject}
|
|
onResumeProject={handleResumeProject}
|
|
onRemoveProject={handleRemoveProject}
|
|
/>
|
|
);
|
|
}
|
|
|
|
// Project view
|
|
if (taskView === "agents") {
|
|
return <AgentsView addToast={addToast} projectId={currentProject?.id} />;
|
|
}
|
|
|
|
if (taskView === "board") {
|
|
return (
|
|
<Board
|
|
tasks={tasks}
|
|
projectId={currentProject?.id}
|
|
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={handleOpenChangedFiles}
|
|
favoriteProviders={favoriteProviders}
|
|
favoriteModels={favoriteModels}
|
|
onToggleFavorite={handleToggleFavorite}
|
|
onToggleModelFavorite={handleToggleModelFavorite}
|
|
taskStuckTimeoutMs={taskStuckTimeoutMs}
|
|
/>
|
|
);
|
|
}
|
|
|
|
// List view
|
|
return (
|
|
<ListView
|
|
tasks={tasks}
|
|
projectId={currentProject?.id}
|
|
onMoveTask={moveTask}
|
|
onOpenDetail={handleDetailOpen}
|
|
addToast={addToast}
|
|
globalPaused={globalPaused}
|
|
onNewTask={handleNewTaskOpen}
|
|
onQuickCreate={handleBoardQuickCreate}
|
|
onPlanningMode={handleNewTaskPlanningMode}
|
|
onSubtaskBreakdown={handleSubtaskBreakdown}
|
|
availableModels={availableModels}
|
|
favoriteProviders={favoriteProviders}
|
|
favoriteModels={favoriteModels}
|
|
onToggleFavorite={handleToggleFavorite}
|
|
onToggleModelFavorite={handleToggleModelFavorite}
|
|
taskStuckTimeoutMs={taskStuckTimeoutMs}
|
|
/>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<Header
|
|
onOpenSettings={() => setSettingsOpen(true)}
|
|
onOpenGitHubImport={() => setGitHubImportOpen(true)}
|
|
onOpenPlanning={handlePlanningOpen}
|
|
onOpenUsage={handleOpenUsage}
|
|
onOpenActivityLog={handleOpenActivityLog}
|
|
onOpenSchedules={handleOpenSchedules}
|
|
onOpenGitManager={handleOpenGitManager}
|
|
onOpenWorkflowSteps={() => setWorkflowStepsOpen(true)}
|
|
onOpenMissions={viewMode === "project" && currentProject ? () => setMissionsOpen(true) : undefined}
|
|
onOpenScripts={handleOpenScripts}
|
|
onRunScript={handleRunScript}
|
|
onToggleTerminal={handleToggleTerminal}
|
|
onOpenFiles={handleOpenFiles}
|
|
filesOpen={filesOpen}
|
|
globalPaused={globalPaused}
|
|
enginePaused={enginePaused}
|
|
onToggleGlobalPause={handleToggleGlobalPause}
|
|
onToggleEnginePause={handleToggleEnginePause}
|
|
view={taskView}
|
|
onChangeView={viewMode === "project" && currentProject ? handleChangeTaskView : undefined}
|
|
searchQuery={searchQuery}
|
|
onSearchChange={setSearchQuery}
|
|
projects={projects}
|
|
currentProject={currentProject}
|
|
onSelectProject={handleSelectProject}
|
|
onViewAllProjects={handleViewAllProjects}
|
|
projectId={currentProject?.id}
|
|
/>
|
|
<div
|
|
className={`project-content${viewMode === "project" && currentProject ? " project-content--with-footer" : ""}`}
|
|
>
|
|
{renderMainContent()}
|
|
</div>
|
|
{viewMode === "project" && currentProject && (
|
|
<ExecutorStatusBar tasks={tasks} projectId={currentProject.id} taskStuckTimeoutMs={taskStuckTimeoutMs} />
|
|
)}
|
|
{detailTask && (
|
|
<TaskDetailModal
|
|
task={detailTask}
|
|
projectId={currentProject?.id}
|
|
tasks={tasks}
|
|
onClose={handleDetailClose}
|
|
onOpenDetail={handleDetailOpen}
|
|
onMoveTask={moveTask}
|
|
onDeleteTask={deleteTask}
|
|
onMergeTask={mergeTask}
|
|
onRetryTask={retryTask}
|
|
onDuplicateTask={duplicateTask}
|
|
onTaskUpdated={(updated) => setDetailTask(prev => prev ? { ...prev, ...updated } : prev)}
|
|
addToast={addToast}
|
|
githubTokenConfigured={githubTokenConfigured}
|
|
/>
|
|
)}
|
|
{settingsOpen && (
|
|
<SettingsModal
|
|
onClose={() => {
|
|
setSettingsOpen(false);
|
|
setSettingsInitialSection(undefined);
|
|
}}
|
|
addToast={addToast}
|
|
initialSection={settingsInitialSection}
|
|
projectId={currentProject?.id}
|
|
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}
|
|
projectId={currentProject?.id}
|
|
/>
|
|
<SubtaskBreakdownModal
|
|
isOpen={isSubtaskOpen}
|
|
onClose={handleSubtaskClose}
|
|
initialDescription={subtaskInitialDescription ?? ""}
|
|
onTasksCreated={handleSubtaskTasksCreated}
|
|
projectId={currentProject?.id}
|
|
/>
|
|
<TerminalModal
|
|
isOpen={terminalOpen}
|
|
onClose={handleTerminalClose}
|
|
initialCommand={terminalInitialCommand}
|
|
/>
|
|
<ScriptsModal
|
|
isOpen={scriptsOpen}
|
|
onClose={handleCloseScripts}
|
|
addToast={addToast}
|
|
onRunScript={handleRunScript}
|
|
projectId={currentProject?.id}
|
|
/>
|
|
{filesOpen && (
|
|
<FileBrowserModal
|
|
initialWorkspace={fileBrowserWorkspace}
|
|
isOpen={true}
|
|
onClose={() => setFilesOpen(false)}
|
|
onWorkspaceChange={handleWorkspaceChange}
|
|
/>
|
|
)}
|
|
{changedFilesState && (
|
|
<ChangedFilesModal
|
|
taskId={changedFilesState.taskId}
|
|
worktree={changedFilesState.worktree}
|
|
column={changedFilesState.column}
|
|
projectId={currentProject?.id}
|
|
isOpen={true}
|
|
onClose={handleCloseChangedFiles}
|
|
/>
|
|
)}
|
|
<UsageIndicator
|
|
isOpen={usageOpen}
|
|
onClose={handleCloseUsage}
|
|
/>
|
|
{schedulesOpen && (
|
|
<ScheduledTasksModal
|
|
onClose={handleCloseSchedules}
|
|
addToast={addToast}
|
|
/>
|
|
)}
|
|
<NewTaskModal
|
|
isOpen={newTaskModalOpen}
|
|
onClose={handleNewTaskClose}
|
|
tasks={tasks}
|
|
onCreateTask={handleModalCreate}
|
|
addToast={addToast}
|
|
projectId={currentProject?.id}
|
|
onPlanningMode={handleNewTaskPlanningMode}
|
|
onSubtaskBreakdown={handleSubtaskBreakdown}
|
|
/>
|
|
<ActivityLogModal
|
|
isOpen={activityLogOpen}
|
|
onClose={handleCloseActivityLog}
|
|
tasks={tasks}
|
|
projectId={currentProject?.id}
|
|
projects={projects}
|
|
currentProject={currentProject}
|
|
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}
|
|
projectId={currentProject?.id}
|
|
/>
|
|
<MissionManager
|
|
isOpen={missionsOpen}
|
|
onClose={() => setMissionsOpen(false)}
|
|
addToast={addToast}
|
|
projectId={currentProject?.id}
|
|
availableTasks={tasks.map((t) => ({ id: t.id, title: t.title }))}
|
|
onSelectTask={(taskId) => {
|
|
const task = tasks.find((t) => t.id === taskId);
|
|
if (task) {
|
|
setDetailTask(task as TaskDetail);
|
|
}
|
|
}}
|
|
/>
|
|
<AgentListModal
|
|
isOpen={agentsOpen}
|
|
onClose={handleCloseAgents}
|
|
addToast={addToast}
|
|
projectId={currentProject?.id}
|
|
/>
|
|
{setupWizardOpen && (
|
|
<SetupWizardModal
|
|
onProjectRegistered={handleSetupComplete}
|
|
onClose={() => setSetupWizardOpen(false)}
|
|
/>
|
|
)}
|
|
<ToastContainer toasts={toasts} onRemove={removeToast} />
|
|
</>
|
|
);
|
|
}
|
|
|
|
export function App() {
|
|
return (
|
|
<ToastProvider>
|
|
<AppInner />
|
|
</ToastProvider>
|
|
);
|
|
}
|