feat(FN-1748): merge fusion/fn-1748
This commit is contained in:
@@ -37,14 +37,13 @@ import { useRemoteNodeData } from "./hooks/useRemoteNodeData";
|
||||
import { useRemoteNodeEvents } from "./hooks/useRemoteNodeEvents";
|
||||
import { NodeProvider, useNodeContext } from "./context/NodeContext";
|
||||
import type { AiSessionSummary } from "./api";
|
||||
import { fetchAiSession } from "./api";
|
||||
|
||||
function AppInner() {
|
||||
const { toasts, addToast, removeToast } = useToast();
|
||||
const isElectron = typeof window !== "undefined" && Boolean((window as Window & { electronAPI?: unknown }).electronAPI);
|
||||
|
||||
// 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 { projects, loading: projectsLoading, refresh: refreshProjects } = useProjects();
|
||||
const { nodes } = useNodes();
|
||||
const { currentProject, setCurrentProject, clearCurrentProject, loading: currentProjectLoading } = useCurrentProject(projects);
|
||||
|
||||
@@ -67,11 +66,10 @@ function AppInner() {
|
||||
|
||||
// Remote node data and events when in remote mode (pass searchQuery for server-side filtering)
|
||||
const remoteData = useRemoteNodeData(currentNodeId, { projectId: currentProject?.id, searchQuery: searchQuery || undefined });
|
||||
const remoteEvents = useRemoteNodeEvents(currentNodeId);
|
||||
useRemoteNodeEvents(currentNodeId);
|
||||
|
||||
// Use remote data when in remote mode, local data otherwise
|
||||
const effectiveProjects = isRemote && remoteData.projects.length > 0 ? remoteData.projects : projects;
|
||||
const effectiveTasks = isRemote && remoteData.tasks.length > 0 ? remoteData.tasks : [];
|
||||
|
||||
// Tasks hook with project context and search query
|
||||
const { tasks, createTask, moveTask, deleteTask, mergeTask, retryTask, updateTask, duplicateTask, archiveTask, unarchiveTask, archiveAllDone, loadArchivedTasks, lastFetchTimeMs } = useTasks(
|
||||
@@ -132,7 +130,6 @@ function AppInner() {
|
||||
// Settings state
|
||||
const {
|
||||
maxConcurrent,
|
||||
rootDir,
|
||||
autoMerge,
|
||||
globalPaused,
|
||||
enginePaused,
|
||||
@@ -152,7 +149,7 @@ function AppInner() {
|
||||
toggleFavoriteModel,
|
||||
} = useFavorites();
|
||||
|
||||
const { viewMode, setViewMode, taskView, handleChangeTaskView, handleToggleTheme } = useViewState({
|
||||
const { viewMode, setViewMode, taskView, handleChangeTaskView } = useViewState({
|
||||
projectsLoading,
|
||||
currentProjectLoading,
|
||||
currentProject,
|
||||
@@ -245,12 +242,6 @@ function AppInner() {
|
||||
setNodesOpen((prev) => !prev);
|
||||
}, []);
|
||||
|
||||
const handleOpenMissionsView = useCallback(() => {
|
||||
setMissionTargetId(undefined);
|
||||
setMissionResumeSessionId(undefined);
|
||||
handleChangeTaskView("missions");
|
||||
}, [handleChangeTaskView]);
|
||||
|
||||
const handleOpenMission = useCallback((missionId: string) => {
|
||||
setMissionTargetId(missionId);
|
||||
setMissionResumeSessionId(undefined);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { memo, useCallback, useMemo } from "react";
|
||||
import {
|
||||
GitPullRequest,
|
||||
GitMerge,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { X, History, Trash2, Filter, RefreshCw, CheckCircle, XCircle, ArrowRight, Plus, Settings, AlertCircle, Loader2, Folder } from "lucide-react";
|
||||
import { clearActivityLog, type ActivityLogEntry, type ActivityEventType, type ActivityFeedEntry } from "../api";
|
||||
import { useActivityLog } from "../hooks/useActivityLog";
|
||||
@@ -81,6 +81,7 @@ export function ActivityLogModal({
|
||||
onProjectFilterChange,
|
||||
currentProject,
|
||||
}: ActivityLogModalProps) {
|
||||
// tasks parameter reserved for future use when filtering activity by task
|
||||
const [filteredType, setFilteredType] = useState<ActivityEventType | "all">("all");
|
||||
const [filteredProjectId, setFilteredProjectId] = useState<string | "all">(projectId || "all");
|
||||
const [showConfirmClear, setShowConfirmClear] = useState(false);
|
||||
@@ -136,7 +137,7 @@ export function ActivityLogModal({
|
||||
await clearActivityLog();
|
||||
refresh();
|
||||
setShowConfirmClear(false);
|
||||
} catch (err) {
|
||||
} catch {
|
||||
// Error handled by hook
|
||||
setShowConfirmClear(false);
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ export function AddNodeModal({ isOpen, onClose, onSubmit, addToast }: AddNodeMod
|
||||
|
||||
try {
|
||||
await onSubmit(input);
|
||||
addToast(`Node \"${input.name}\" registered`, "success");
|
||||
addToast(`Node "${input.name}" registered`, "success");
|
||||
closeModal();
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Failed to register node";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect, useCallback, useRef, useMemo } from "react";
|
||||
import type { JSX } from "react";
|
||||
import { X, Plus, Play, Pause, Square, Activity, Heart, Trash2, RefreshCw, Bot, LayoutGrid, List, Filter } from "lucide-react";
|
||||
import { X, Plus, Play, Pause, Square, Trash2, RefreshCw, Bot, LayoutGrid, List, Filter } from "lucide-react";
|
||||
import type { Agent, AgentCapability, AgentState } from "../api";
|
||||
import { fetchAgents, createAgent, updateAgent, updateAgentState, deleteAgent } from "../api";
|
||||
import { getScopedItem, setScopedItem } from "../utils/projectStorage";
|
||||
@@ -158,7 +158,7 @@ export function AgentListModal({ isOpen, onClose, addToast, projectId }: AgentLi
|
||||
}
|
||||
};
|
||||
|
||||
const handleRoleKeyDown = (e: React.KeyboardEvent, agentId: string) => {
|
||||
const handleRoleKeyDown = (e: React.KeyboardEvent, _agentId: string) => {
|
||||
if (e.key === "Escape") {
|
||||
setEditingRoleForAgent(null);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect, useCallback, useRef, useMemo } from "react";
|
||||
import type { JSX } from "react";
|
||||
import { Plus, Play, Pause, Square, Activity, Heart, Trash2, RefreshCw, Bot, LayoutGrid, List, ChevronRight, ChevronDown, GitBranch, Filter, Upload, Network } from "lucide-react";
|
||||
import { Plus, Play, Pause, Square, Activity, Trash2, RefreshCw, Bot, LayoutGrid, List, ChevronRight, ChevronDown, GitBranch, Filter, Upload, Network } from "lucide-react";
|
||||
import type { Agent, AgentCapability, AgentState, OrgTreeNode } from "../api";
|
||||
import { fetchAgents, updateAgent, updateAgentState, deleteAgent, startAgentRun, fetchOrgTree } from "../api";
|
||||
import { AgentDetailView } from "./AgentDetailView";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ProjectInfo } from "../api";
|
||||
import type { ColorTheme, Column, MergeResult, Task, TaskCreateInput, ThemeMode } from "@fusion/core";
|
||||
import type { ColorTheme, Column, MergeResult, Task, ThemeMode } from "@fusion/core";
|
||||
import type { UseProjectActionsResult } from "../hooks/useProjectActions";
|
||||
import type { ModalManager } from "../hooks/useModalManager";
|
||||
import type { UseTaskHandlersResult } from "../hooks/useTaskHandlers";
|
||||
|
||||
@@ -95,11 +95,6 @@ export function CustomModelDropdown({
|
||||
}, [favoriteModels, filteredModels]);
|
||||
|
||||
// Sort providers: favorites first (in order), then alphabetically
|
||||
// Exclude providers that are already favorited as models (they appear at top as pinned rows)
|
||||
const favoritedProviderSet = new Set(favoriteModels.map((fullId) => {
|
||||
const idx = fullId.indexOf("/");
|
||||
return idx !== -1 ? fullId.slice(0, idx) : fullId;
|
||||
}));
|
||||
const sortedProviderEntries = useMemo(() => {
|
||||
const entries = Object.entries(modelsByProvider);
|
||||
const favoritesSet = new Set(favoriteProviders);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo } from "react";
|
||||
import type { Task } from "@fusion/core";
|
||||
import { Activity, AlertTriangle, Clock, Pause, Play, Zap } from "lucide-react";
|
||||
import { AlertTriangle, Clock, Pause, Play, Zap } from "lucide-react";
|
||||
import { useExecutorStats } from "../hooks/useExecutorStats";
|
||||
import type { ExecutorState, AiSessionSummary } from "../api";
|
||||
import { BackgroundTasksIndicator } from "./BackgroundTasksIndicator";
|
||||
|
||||
@@ -319,7 +319,6 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId
|
||||
// Determine state flags
|
||||
const hasRemotes = remotes.length > 0;
|
||||
const singleRemote = remotes.length === 1;
|
||||
const multipleRemotes = remotes.length > 1;
|
||||
|
||||
// Tab-specific counts
|
||||
const importedIssueCount = issues.filter((issue) => importedUrls.has(issue.html_url)).length;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useCallback, useMemo, Fragment, useEffect, useRef } from "react";
|
||||
import { LayoutGrid, List as ListIcon, ArrowUpDown, ArrowUp, ArrowDown, Link, Columns3, EyeOff, Eye, ChevronRight } from "lucide-react";
|
||||
import { ArrowUpDown, ArrowUp, ArrowDown, Link, Columns3, EyeOff, Eye, ChevronRight } from "lucide-react";
|
||||
import type { Task, TaskDetail, Column, TaskStep, TaskCreateInput } from "@fusion/core";
|
||||
import { COLUMN_LABELS, COLUMNS } from "@fusion/core";
|
||||
import { batchUpdateTaskModels } from "../api";
|
||||
@@ -122,8 +122,6 @@ interface ListViewProps {
|
||||
onTasksUpdated?: (updatedTasks: Task[]) => void;
|
||||
/** Project ID for multi-project context (optional) */
|
||||
projectId?: string;
|
||||
/** Project name for display (optional) */
|
||||
projectName?: string;
|
||||
/** Project-level stuck task timeout in milliseconds (undefined = disabled) */
|
||||
taskStuckTimeoutMs?: number;
|
||||
/** External search query from header search (defaults to "") */
|
||||
@@ -161,7 +159,6 @@ export function ListView({
|
||||
onSubtaskBreakdown,
|
||||
onTasksUpdated,
|
||||
projectId,
|
||||
projectName,
|
||||
taskStuckTimeoutMs,
|
||||
searchQuery = "",
|
||||
lastFetchTimeMs,
|
||||
|
||||
@@ -117,8 +117,6 @@ export function PluginManager({ addToast, projectId }: PluginManagerProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentPlugins = pluginsRef.current;
|
||||
|
||||
switch (payload.transition) {
|
||||
case "installing":
|
||||
case "enabled":
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { memo, useCallback, useState } from "react";
|
||||
import { Play, Pause, AlertCircle, Loader2, MoreHorizontal, Trash2, Folder, ArrowRight } from "lucide-react";
|
||||
import { Play, Pause, AlertCircle, Loader2, Trash2, Folder, ArrowRight } from "lucide-react";
|
||||
import type { RegisteredProject, ProjectHealth, ProjectStatus } from "@fusion/core";
|
||||
import type { NodeInfo } from "../api";
|
||||
|
||||
|
||||
@@ -102,12 +102,13 @@ export function ProjectOverview({
|
||||
case "name":
|
||||
comparison = a.project.name.localeCompare(b.project.name);
|
||||
break;
|
||||
case "activity":
|
||||
case "activity": {
|
||||
const aTime = a.project.lastActivityAt || a.health?.lastActivityAt || a.project.updatedAt;
|
||||
const bTime = b.project.lastActivityAt || b.health?.lastActivityAt || b.project.updatedAt;
|
||||
comparison = new Date(bTime).getTime() - new Date(aTime).getTime();
|
||||
break;
|
||||
case "status":
|
||||
}
|
||||
case "status": {
|
||||
const statusOrder: Record<ProjectStatus, number> = {
|
||||
errored: 0,
|
||||
initializing: 1,
|
||||
@@ -116,6 +117,7 @@ export function ProjectOverview({
|
||||
};
|
||||
comparison = statusOrder[a.project.status] - statusOrder[b.project.status];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return sortDirection === "asc" ? comparison : -comparison;
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Calendar, Webhook, Code, Zap } from "lucide-react";
|
||||
import type {
|
||||
Routine,
|
||||
RoutineCreateInput,
|
||||
RoutineUpdateInput,
|
||||
RoutineTrigger,
|
||||
RoutineTriggerType,
|
||||
RoutineCronTrigger,
|
||||
@@ -22,7 +21,7 @@ function isLikelyCron(expr: string): boolean {
|
||||
const parts = expr.trim().split(/\s+/);
|
||||
if (parts.length !== 5) return false;
|
||||
// Each field should contain digits, *, /, -, or ,
|
||||
return parts.every((p) => /^[\d*,/\-]+$/.test(p));
|
||||
return parts.every((p) => /^[\d*,/-]+$/.test(p));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -38,7 +38,7 @@ function isLikelyCron(expr: string): boolean {
|
||||
const parts = expr.trim().split(/\s+/);
|
||||
if (parts.length !== 5) return false;
|
||||
// Each field should contain digits, *, /, -, or ,
|
||||
return parts.every((p) => /^[\d*,/\-]+$/.test(p));
|
||||
return parts.every((p) => /^[\d*,/-]+$/.test(p));
|
||||
}
|
||||
|
||||
type ScheduleMode = "simple" | "advanced";
|
||||
|
||||
@@ -431,8 +431,6 @@ export function ScheduledTasksModal({ onClose, addToast }: ScheduledTasksModalPr
|
||||
// Determine if we're in "list" view for showing the "New" button
|
||||
const isShowingList =
|
||||
activeTab === "schedules" ? view === "list" && schedules.length > 0 : routineView === "list" && routines.length > 0;
|
||||
const isShowingEmptyState =
|
||||
activeTab === "schedules" ? view === "list" && schedules.length === 0 && !loading : routineView === "list" && routines.length === 0;
|
||||
|
||||
return (
|
||||
<div className="modal-overlay open" onClick={handleOverlayClick}>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { Globe, Folder } from "lucide-react";
|
||||
import { THINKING_LEVELS, PROMPT_KEY_CATALOG, isGlobalSettingsKey, isProjectSettingsKey } from "@fusion/core";
|
||||
import type { Settings, GlobalSettings, ThemeMode, ColorTheme, ModelPreset, NtfyNotificationEvent, PromptKey, AgentPromptsConfig } from "@fusion/core";
|
||||
import { THINKING_LEVELS, isGlobalSettingsKey, isProjectSettingsKey } from "@fusion/core";
|
||||
import type { Settings, GlobalSettings, ThemeMode, ColorTheme, ModelPreset, NtfyNotificationEvent, AgentPromptsConfig } from "@fusion/core";
|
||||
import { fetchSettings, updateSettings, updateGlobalSettings, fetchAuthStatus, loginProvider, logoutProvider, saveApiKey, clearApiKey, fetchModels, testNtfyNotification, fetchBackups, createBackup, exportSettings, importSettings, fetchMemory, saveMemory, fetchGlobalConcurrency, updateGlobalConcurrency } from "../api";
|
||||
import type { AuthProvider, ModelInfo, BackupListResponse, SettingsExportData, MemoryBackendCapabilities } from "../api";
|
||||
import type { AuthProvider, ModelInfo, BackupListResponse, SettingsExportData } from "../api";
|
||||
import { useMemoryBackendStatus } from "../hooks/useMemoryBackendStatus";
|
||||
import type { ToastType } from "../hooks/useToast";
|
||||
import { ThemeSelector } from "./ThemeSelector";
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Pencil, Bot, X, ChevronDown } from "lucide-react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import type { Task, TaskDetail, TaskAttachment, Column, MergeResult, PrInfo, Settings, AgentLogEntry, Agent } from "@fusion/core";
|
||||
import type { Task, TaskDetail, TaskAttachment, Column, MergeResult, Settings, AgentLogEntry, Agent } from "@fusion/core";
|
||||
import { COLUMN_LABELS, VALID_TRANSITIONS } from "@fusion/core";
|
||||
import { uploadAttachment, deleteAttachment, updateTask, pauseTask, unpauseTask, fetchTaskDetail, fetchSettings, requestSpecRevision, rebuildTaskSpec, approvePlan, rejectPlan, refineTask, fetchWorkflowResults, assignTask, fetchAgents, fetchAgent } from "../api";
|
||||
import type { WorkflowStepResult } from "@fusion/core";
|
||||
@@ -23,24 +23,10 @@ interface ModelSelection {
|
||||
modelId?: string;
|
||||
}
|
||||
|
||||
function normalizeModelField(value: string | null | undefined): string | undefined {
|
||||
function _normalizeModelField(value: string | null | undefined): string | undefined {
|
||||
return value ?? undefined;
|
||||
}
|
||||
|
||||
function getExecutorSelection(task: Task | TaskDetail): ModelSelection {
|
||||
return {
|
||||
provider: normalizeModelField(task.modelProvider),
|
||||
modelId: normalizeModelField(task.modelId),
|
||||
};
|
||||
}
|
||||
|
||||
function getValidatorSelection(task: Task | TaskDetail): ModelSelection {
|
||||
return {
|
||||
provider: normalizeModelField(task.validatorModelProvider),
|
||||
modelId: normalizeModelField(task.validatorModelId),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the effective executor model following the engine's resolution order:
|
||||
* 1. Per-task modelProvider/modelId (both must be set)
|
||||
@@ -359,7 +345,7 @@ export function TaskDetailModal({
|
||||
})
|
||||
.catch((err: any) => {
|
||||
if (!cancelled) {
|
||||
addToast(`Failed to load workflow results: ${err.message}`, "error");
|
||||
addToast(`Failed to load workflow results: ${err instanceof Error ? err.message : String(err)}`, "error");
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -557,8 +543,8 @@ export function TaskDetailModal({
|
||||
setEditPendingImages([]);
|
||||
addToast(`Updated ${task.id}`, "success");
|
||||
setIsEditing(false);
|
||||
} catch (err: any) {
|
||||
addToast(`Failed to update ${task.id}: ${err.message}`, "error");
|
||||
} catch (err: unknown) {
|
||||
addToast(`Failed to update ${task.id}: ${err instanceof Error ? err.message : String(err)}`, "error");
|
||||
} finally {
|
||||
if (mountedRef.current) {
|
||||
setIsSaving(false);
|
||||
@@ -570,8 +556,8 @@ export function TaskDetailModal({
|
||||
try {
|
||||
await updateTask(task.id, { description }, projectId);
|
||||
addToast("Description saved", "success");
|
||||
} catch (err: any) {
|
||||
addToast(`Failed to save: ${err.message}`, "error");
|
||||
} catch (err: unknown) {
|
||||
addToast(`Failed to save: ${err instanceof Error ? err.message : String(err)}`, "error");
|
||||
}
|
||||
}, [task.id, addToast, projectId]);
|
||||
|
||||
@@ -620,8 +606,8 @@ export function TaskDetailModal({
|
||||
await onMoveTask(task.id, column);
|
||||
onClose();
|
||||
addToast(`Moved to ${COLUMN_LABELS[column]}`, "success");
|
||||
} catch (err: any) {
|
||||
addToast(err.message, "error");
|
||||
} catch (err: unknown) {
|
||||
addToast(err instanceof Error ? err.message : String(err), "error");
|
||||
}
|
||||
},
|
||||
[task.id, onMoveTask, onClose, addToast],
|
||||
@@ -633,8 +619,8 @@ export function TaskDetailModal({
|
||||
await onDeleteTask(task.id);
|
||||
onClose();
|
||||
addToast(`Deleted ${task.id}`, "info");
|
||||
} catch (err: any) {
|
||||
addToast(err.message, "error");
|
||||
} catch (err: unknown) {
|
||||
addToast(err instanceof Error ? err.message : String(err), "error");
|
||||
}
|
||||
}, [task.id, onDeleteTask, onClose, addToast]);
|
||||
|
||||
@@ -650,7 +636,7 @@ export function TaskDetailModal({
|
||||
addToast(msg, "success");
|
||||
})
|
||||
.catch((err: any) => {
|
||||
addToast(err.message, "error");
|
||||
addToast(err instanceof Error ? err.message : String(err), "error");
|
||||
});
|
||||
}, [task.id, onMergeTask, onClose, addToast]);
|
||||
|
||||
@@ -660,8 +646,8 @@ export function TaskDetailModal({
|
||||
await onRetryTask(task.id);
|
||||
onClose();
|
||||
addToast(`Retrying ${task.id}...`, "info");
|
||||
} catch (err: any) {
|
||||
addToast(err.message, "error");
|
||||
} catch (err: unknown) {
|
||||
addToast(err instanceof Error ? err.message : String(err), "error");
|
||||
}
|
||||
}, [task.id, onRetryTask, onClose, addToast]);
|
||||
|
||||
@@ -672,8 +658,8 @@ export function TaskDetailModal({
|
||||
const newTask = await onDuplicateTask(task.id);
|
||||
onClose();
|
||||
addToast(`Duplicated ${task.id} → ${newTask.id}`, "success");
|
||||
} catch (err: any) {
|
||||
addToast(err.message, "error");
|
||||
} catch (err: unknown) {
|
||||
addToast(err instanceof Error ? err.message : String(err), "error");
|
||||
}
|
||||
}, [task.id, onDuplicateTask, onClose, addToast]);
|
||||
|
||||
@@ -687,8 +673,8 @@ export function TaskDetailModal({
|
||||
addToast(`Paused ${task.id}`, "success");
|
||||
}
|
||||
onClose();
|
||||
} catch (err: any) {
|
||||
addToast(err.message, "error");
|
||||
} catch (err: unknown) {
|
||||
addToast(err instanceof Error ? err.message : String(err), "error");
|
||||
}
|
||||
}, [task.id, task.paused, onClose, addToast]);
|
||||
|
||||
@@ -697,8 +683,8 @@ export function TaskDetailModal({
|
||||
await approvePlan(task.id, projectId);
|
||||
addToast(`Plan approved — ${task.id} moved to Todo`, "success");
|
||||
onClose();
|
||||
} catch (err: any) {
|
||||
addToast(err.message, "error");
|
||||
} catch (err: unknown) {
|
||||
addToast(err instanceof Error ? err.message : String(err), "error");
|
||||
}
|
||||
}, [task.id, onClose, addToast]);
|
||||
|
||||
@@ -708,8 +694,8 @@ export function TaskDetailModal({
|
||||
await rejectPlan(task.id, projectId);
|
||||
addToast(`Plan rejected — ${task.id} returned to Triage for re-specification`, "info");
|
||||
onClose();
|
||||
} catch (err: any) {
|
||||
addToast(err.message, "error");
|
||||
} catch (err: unknown) {
|
||||
addToast(err instanceof Error ? err.message : String(err), "error");
|
||||
}
|
||||
}, [task.id, onClose, addToast]);
|
||||
|
||||
@@ -719,8 +705,8 @@ export function TaskDetailModal({
|
||||
await rebuildTaskSpec(task.id, projectId);
|
||||
onClose();
|
||||
addToast(`Respecifying ${task.id}...`, "info");
|
||||
} catch (err: any) {
|
||||
addToast(err.message, "error");
|
||||
} catch (err: unknown) {
|
||||
addToast(err instanceof Error ? err.message : String(err), "error");
|
||||
}
|
||||
}, [task.id, projectId, onClose, addToast]);
|
||||
|
||||
@@ -771,8 +757,8 @@ export function TaskDetailModal({
|
||||
const newTask = await refineTask(task.id, refineFeedback.trim(), projectId);
|
||||
addToast(`Refinement task created: ${newTask.id}`, "success");
|
||||
onClose();
|
||||
} catch (err: any) {
|
||||
addToast(err.message, "error");
|
||||
} catch (err: unknown) {
|
||||
addToast(err instanceof Error ? err.message : String(err), "error");
|
||||
} finally {
|
||||
setIsRefining(false);
|
||||
}
|
||||
@@ -784,8 +770,8 @@ export function TaskDetailModal({
|
||||
const attachment = await uploadAttachment(task.id, file, projectId);
|
||||
setAttachments((prev) => [...prev, attachment]);
|
||||
addToast("Screenshot attached", "success");
|
||||
} catch (err: any) {
|
||||
addToast(err.message, "error");
|
||||
} catch (err: unknown) {
|
||||
addToast(err instanceof Error ? err.message : String(err), "error");
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
@@ -839,8 +825,8 @@ export function TaskDetailModal({
|
||||
await deleteAttachment(task.id, filename, projectId);
|
||||
setAttachments((prev) => prev.filter((a) => a.filename !== filename));
|
||||
addToast("Attachment deleted", "info");
|
||||
} catch (err: any) {
|
||||
addToast(err.message, "error");
|
||||
} catch (err: unknown) {
|
||||
addToast(err instanceof Error ? err.message : String(err), "error");
|
||||
}
|
||||
}, [task.id, addToast]);
|
||||
|
||||
@@ -852,9 +838,9 @@ export function TaskDetailModal({
|
||||
const updatedTask = await updateTask(task.id, { enabledWorkflowSteps }, projectId);
|
||||
addToast("Workflow steps updated", "success");
|
||||
onTaskUpdated?.(updatedTask);
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
setWorkflowEnabledSteps(previousSteps);
|
||||
addToast(`Failed to update workflow steps: ${err.message}`, "error");
|
||||
addToast(`Failed to update workflow steps: ${err instanceof Error ? err.message : String(err)}`, "error");
|
||||
}
|
||||
}, [task.id, projectId, workflowEnabledSteps, onTaskUpdated, addToast]);
|
||||
|
||||
@@ -864,8 +850,8 @@ export function TaskDetailModal({
|
||||
const loadedAgents = await fetchAgents(undefined, projectId);
|
||||
setAgents(loadedAgents);
|
||||
setShowAgentPicker(true);
|
||||
} catch (err: any) {
|
||||
addToast(`Failed to load agents: ${err.message}`, "error");
|
||||
} catch (err: unknown) {
|
||||
addToast(`Failed to load agents: ${err instanceof Error ? err.message : String(err)}`, "error");
|
||||
setShowAgentPicker(false);
|
||||
} finally {
|
||||
setAgentsLoading(false);
|
||||
@@ -884,8 +870,8 @@ export function TaskDetailModal({
|
||||
setShowAgentPicker(false);
|
||||
onTaskUpdated?.(updatedTask);
|
||||
addToast("Assigned agent updated", "success");
|
||||
} catch (err: any) {
|
||||
addToast(`Failed to assign agent: ${err.message}`, "error");
|
||||
} catch (err: unknown) {
|
||||
addToast(`Failed to assign agent: ${err instanceof Error ? err.message : String(err)}`, "error");
|
||||
}
|
||||
}, [task.id, projectId, agents, onTaskUpdated, addToast]);
|
||||
|
||||
@@ -896,8 +882,8 @@ export function TaskDetailModal({
|
||||
setShowAgentPicker(false);
|
||||
onTaskUpdated?.(updatedTask);
|
||||
addToast("Agent unassigned", "success");
|
||||
} catch (err: any) {
|
||||
addToast(`Failed to unassign agent: ${err.message}`, "error");
|
||||
} catch (err: unknown) {
|
||||
addToast(`Failed to unassign agent: ${err instanceof Error ? err.message : String(err)}`, "error");
|
||||
}
|
||||
}, [task.id, projectId, onTaskUpdated, addToast]);
|
||||
|
||||
@@ -906,9 +892,10 @@ export function TaskDetailModal({
|
||||
setDependencies(newDeps);
|
||||
try {
|
||||
await updateTask(task.id, { dependencies: newDeps }, projectId);
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
setDependencies(dependencies);
|
||||
addToast(err.message, "error");
|
||||
const message = err instanceof Error ? err instanceof Error ? err.message : String(err) : String(err);
|
||||
addToast(message, "error");
|
||||
}
|
||||
}, [task.id, dependencies, addToast]);
|
||||
|
||||
@@ -918,9 +905,10 @@ export function TaskDetailModal({
|
||||
setDependencies(newDeps);
|
||||
try {
|
||||
await updateTask(task.id, { dependencies: newDeps }, projectId);
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
setDependencies(dependencies);
|
||||
addToast(err.message, "error");
|
||||
const message = err instanceof Error ? err instanceof Error ? err.message : String(err) : String(err);
|
||||
addToast(message, "error");
|
||||
}
|
||||
}, [task.id, dependencies, addToast]);
|
||||
|
||||
@@ -928,7 +916,7 @@ export function TaskDetailModal({
|
||||
try {
|
||||
const detail = await fetchTaskDetail(depId, projectId);
|
||||
onOpenDetail(detail);
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
addToast(`Failed to load dependency ${depId}`, "error");
|
||||
}
|
||||
}, [onOpenDetail, addToast]);
|
||||
@@ -943,8 +931,8 @@ export function TaskDetailModal({
|
||||
if (fullDetail) {
|
||||
fullDetail.prompt = newContent;
|
||||
}
|
||||
} catch (err: any) {
|
||||
addToast(err.message, "error");
|
||||
} catch (err: unknown) {
|
||||
addToast(err instanceof Error ? err.message : String(err), "error");
|
||||
throw err;
|
||||
} finally {
|
||||
setIsSavingSpec(false);
|
||||
@@ -958,11 +946,12 @@ export function TaskDetailModal({
|
||||
addToast("AI revision requested. Task moved to triage.", "success");
|
||||
// Task has been moved to triage, close modal
|
||||
onClose();
|
||||
} catch (err: any) {
|
||||
if (err.message?.includes("in-review") || err.message?.includes("done")) {
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
if (message?.includes("in-review") || message?.includes("done")) {
|
||||
addToast("Cannot request revision: Task must be in 'todo' or 'in-progress' column.", "error");
|
||||
} else {
|
||||
addToast(err.message, "error");
|
||||
addToast(message, "error");
|
||||
}
|
||||
} finally {
|
||||
setIsRequestingRevision(false);
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { FileText, ChevronDown, ChevronUp, Plus, Trash2, History, X } from "lucide-react";
|
||||
import { FileText, ChevronDown, ChevronUp, Plus, Trash2, History } from "lucide-react";
|
||||
import type { Task, TaskDocument, TaskDocumentRevision } from "@fusion/core";
|
||||
import type { ToastType } from "../hooks/useToast";
|
||||
import {
|
||||
fetchTaskDocuments,
|
||||
fetchTaskDocument,
|
||||
fetchTaskDocumentRevisions,
|
||||
putTaskDocument,
|
||||
deleteTaskDocument,
|
||||
|
||||
@@ -69,8 +69,9 @@ export async function retryDynamicImport<T>(
|
||||
/** Whether the current device is likely mobile (touch-primary, small viewport). */
|
||||
function isMobileDevice(): boolean {
|
||||
if (typeof window === "undefined") return false;
|
||||
const nav = navigator as Navigator & { maxTouchPoints?: number };
|
||||
const hasTouchScreen =
|
||||
"ontouchstart" in window || (navigator as any).maxTouchPoints > 0;
|
||||
"ontouchstart" in window || (nav.maxTouchPoints ?? 0) > 0;
|
||||
const isNarrow = window.innerWidth <= 768;
|
||||
return hasTouchScreen && isNarrow;
|
||||
}
|
||||
@@ -601,7 +602,9 @@ export function TerminalModal({ isOpen, onClose, initialCommand, projectId }: Te
|
||||
if (xtermRef.current) {
|
||||
const currentSize = xtermRef.current.options.fontSize || 14;
|
||||
xtermRef.current.options.fontSize = Math.min(currentSize + 1, 32);
|
||||
fitAddonRef.current && (fitAddonRef.current as InstanceType<typeof FitAddon>).fit();
|
||||
if (fitAddonRef.current) {
|
||||
(fitAddonRef.current as InstanceType<typeof FitAddon>).fit();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -612,7 +615,9 @@ export function TerminalModal({ isOpen, onClose, initialCommand, projectId }: Te
|
||||
if (xtermRef.current) {
|
||||
const currentSize = xtermRef.current.options.fontSize || 14;
|
||||
xtermRef.current.options.fontSize = Math.max(currentSize - 1, 8);
|
||||
fitAddonRef.current && (fitAddonRef.current as InstanceType<typeof FitAddon>).fit();
|
||||
if (fitAddonRef.current) {
|
||||
(fitAddonRef.current as InstanceType<typeof FitAddon>).fit();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -622,7 +627,9 @@ export function TerminalModal({ isOpen, onClose, initialCommand, projectId }: Te
|
||||
e.preventDefault();
|
||||
if (xtermRef.current) {
|
||||
xtermRef.current.options.fontSize = 14;
|
||||
fitAddonRef.current && (fitAddonRef.current as InstanceType<typeof FitAddon>).fit();
|
||||
if (fitAddonRef.current) {
|
||||
(fitAddonRef.current as InstanceType<typeof FitAddon>).fit();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -726,8 +733,9 @@ export function TerminalModal({ isOpen, onClose, initialCommand, projectId }: Te
|
||||
// Restart the active tab's session
|
||||
try {
|
||||
await restartActiveTab();
|
||||
} catch (err: any) {
|
||||
setError(err.message || "Failed to restart terminal session");
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
setError(message || "Failed to restart terminal session");
|
||||
}
|
||||
}, [restartActiveTab]);
|
||||
|
||||
|
||||
@@ -209,8 +209,8 @@ export interface MissionWithHierarchy extends Mission {
|
||||
export type MissionEventType = CoreMissionEventType;
|
||||
|
||||
/** Mission lifecycle event persisted in the mission event log. */
|
||||
export interface MissionEvent extends CoreMissionEvent {}
|
||||
export type MissionEvent = CoreMissionEvent;
|
||||
|
||||
/** Computed mission health snapshot returned by observability APIs. */
|
||||
export interface MissionHealth extends CoreMissionHealth {}
|
||||
export type MissionHealth = CoreMissionHealth;
|
||||
|
||||
|
||||
@@ -68,11 +68,12 @@ export function useBatchBadgeFetch(projectId?: string): UseBatchBadgeFetchResult
|
||||
try {
|
||||
const results = await fetchBatchStatus(taskIds, projectId);
|
||||
return results;
|
||||
} catch (err: any) {
|
||||
lastError = err instanceof Error ? err : new Error(String(err));
|
||||
} catch (err: unknown) {
|
||||
const error = err instanceof Error ? err : new Error(String(err));
|
||||
lastError = error;
|
||||
|
||||
// If it's a 429 rate limit error, wait before retrying with exponential backoff
|
||||
if (err?.message?.includes("429") || err?.message?.toLowerCase().includes("rate limit")) {
|
||||
if (error.message.includes("429") || error.message.toLowerCase().includes("rate limit")) {
|
||||
const delayMs = Math.min(1000 * Math.pow(2, attempt), 30000); // Max 30s delay
|
||||
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
||||
continue;
|
||||
@@ -143,7 +144,7 @@ export function useBatchBadgeFetch(projectId?: string): UseBatchBadgeFetchResult
|
||||
batchBadgeStore.data.set(getScopedTaskKey(taskId, projectId), { result, timestamp });
|
||||
}
|
||||
batchBadgeStore.lastFetchTime = timestamp;
|
||||
} catch (err) {
|
||||
} catch {
|
||||
// Even on error, we don't throw - the hook handles errors gracefully
|
||||
// and partial results are still stored
|
||||
} finally {
|
||||
|
||||
@@ -56,9 +56,10 @@ export function useFileBrowser(taskId: string, enabled: boolean, projectId?: str
|
||||
if (!cancelled) {
|
||||
setEntries(response.entries);
|
||||
}
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
if (!cancelled) {
|
||||
setError(err.message || "Failed to load files");
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
setError(message || "Failed to load files");
|
||||
setEntries([]);
|
||||
}
|
||||
} finally {
|
||||
|
||||
@@ -65,9 +65,10 @@ export function useFileEditor(
|
||||
setOriginalContent(response.content);
|
||||
setMtime(response.mtime);
|
||||
}
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
if (!cancelled) {
|
||||
setError(err.message || "Failed to load file");
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
setError(message || "Failed to load file");
|
||||
setContentState("");
|
||||
setOriginalContent("");
|
||||
setMtime(null);
|
||||
@@ -100,8 +101,9 @@ export function useFileEditor(
|
||||
const response: SaveFileResponse = await saveFileContent(taskId, filePath, content, projectId);
|
||||
setOriginalContent(content);
|
||||
setMtime(response.mtime);
|
||||
} catch (err: any) {
|
||||
setError(err.message || "Failed to save file");
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
setError(message || "Failed to save file");
|
||||
throw err;
|
||||
} finally {
|
||||
setSaving(false);
|
||||
|
||||
@@ -54,9 +54,10 @@ export function useProjectFileBrowser(rootPath: string, enabled: boolean): UsePr
|
||||
if (!cancelled) {
|
||||
setEntries(response.entries);
|
||||
}
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
if (!cancelled) {
|
||||
setError(err.message || "Failed to load files");
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
setError(message || "Failed to load files");
|
||||
setEntries([]);
|
||||
}
|
||||
} finally {
|
||||
|
||||
@@ -63,9 +63,10 @@ export function useProjectFileEditor(
|
||||
setOriginalContent(response.content);
|
||||
setMtime(response.mtime);
|
||||
}
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
if (!cancelled) {
|
||||
setError(err.message || "Failed to load file");
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
setError(message || "Failed to load file");
|
||||
setContentState("");
|
||||
setOriginalContent("");
|
||||
setMtime(null);
|
||||
@@ -98,8 +99,9 @@ export function useProjectFileEditor(
|
||||
const response: SaveFileResponse = await saveWorkspaceFileContent("project", filePath, content);
|
||||
setOriginalContent(content);
|
||||
setMtime(response.mtime);
|
||||
} catch (err: any) {
|
||||
setError(err.message || "Failed to save file");
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
setError(message || "Failed to save file");
|
||||
throw err;
|
||||
} finally {
|
||||
setSaving(false);
|
||||
|
||||
@@ -25,11 +25,6 @@ export interface TerminalTab {
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
interface StoredTab extends TerminalTab {
|
||||
/** Marked as unverified during server validation */
|
||||
_verified?: boolean;
|
||||
}
|
||||
|
||||
interface UseTerminalSessionsReturn {
|
||||
/** All terminal tabs */
|
||||
tabs: TerminalTab[];
|
||||
|
||||
@@ -57,14 +57,15 @@ export function useUsageData(options: UseUsageDataOptions = {}) {
|
||||
error: null,
|
||||
lastUpdated: new Date(),
|
||||
});
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
// Don't update state if the request was aborted
|
||||
if (err.name === "AbortError") return;
|
||||
if (err instanceof Error && err.name === "AbortError") return;
|
||||
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
error: err.message || "Failed to fetch usage data",
|
||||
error: message || "Failed to fetch usage data",
|
||||
}));
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -65,9 +65,10 @@ export function useWorkspaceFileBrowser(
|
||||
if (!cancelled) {
|
||||
setEntries(response.entries);
|
||||
}
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
if (!cancelled) {
|
||||
setError(err.message || "Failed to load files");
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
setError(message || "Failed to load files");
|
||||
setEntries([]);
|
||||
}
|
||||
} finally {
|
||||
|
||||
@@ -63,9 +63,10 @@ export function useWorkspaceFileEditor(
|
||||
setOriginalContent(response.content);
|
||||
setMtime(response.mtime);
|
||||
}
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
if (!cancelled) {
|
||||
setError(err.message || "Failed to load file");
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
setError(message || "Failed to load file");
|
||||
setContentState("");
|
||||
setOriginalContent("");
|
||||
setMtime(null);
|
||||
@@ -98,8 +99,9 @@ export function useWorkspaceFileEditor(
|
||||
const response: SaveFileResponse = await saveWorkspaceFileContent(workspace, filePath, content, projectId);
|
||||
setOriginalContent(content);
|
||||
setMtime(response.mtime);
|
||||
} catch (err: any) {
|
||||
setError(err.message || "Failed to save file");
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
setError(message || "Failed to save file");
|
||||
throw err;
|
||||
} finally {
|
||||
setSaving(false);
|
||||
|
||||
@@ -56,9 +56,10 @@ export function useWorkspaces(projectId?: string): UseWorkspacesReturn {
|
||||
setProjectName(getProjectName(response.project));
|
||||
setWorkspaces(response.tasks.map(mapTaskWorkspace));
|
||||
setError(null);
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
if (!cancelled) {
|
||||
setError(err.message || "Failed to load workspaces");
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
setError(message || "Failed to load workspaces");
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
|
||||
@@ -22,7 +22,7 @@ if (typeof window !== "undefined") {
|
||||
}
|
||||
|
||||
// Mock fetch for project API tests
|
||||
const originalFetch = globalThis.fetch;
|
||||
const _originalFetch = globalThis.fetch;
|
||||
|
||||
globalThis.fetch = vi.fn(async (url: RequestInfo | URL) => {
|
||||
const urlString = url.toString();
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { ModelInfo } from "../api";
|
||||
* Preserves spaces (which serve as field/word boundaries) and alphanumeric chars.
|
||||
*/
|
||||
function normalize(s: string): string {
|
||||
return s.toLowerCase().replace(/[-_.\/]/g, "");
|
||||
return s.toLowerCase().replace(/[-_./]/g, "");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user