feat(FN-1748): merge fusion/fn-1748

This commit is contained in:
gsxdsm
2026-04-14 22:45:59 -07:00
parent 9fc5314f4d
commit 2800f57d78
34 changed files with 133 additions and 151 deletions

View File

@@ -37,14 +37,13 @@ import { useRemoteNodeData } from "./hooks/useRemoteNodeData";
import { useRemoteNodeEvents } from "./hooks/useRemoteNodeEvents"; import { useRemoteNodeEvents } from "./hooks/useRemoteNodeEvents";
import { NodeProvider, useNodeContext } from "./context/NodeContext"; import { NodeProvider, useNodeContext } from "./context/NodeContext";
import type { AiSessionSummary } from "./api"; import type { AiSessionSummary } from "./api";
import { fetchAiSession } from "./api";
function AppInner() { function AppInner() {
const { toasts, addToast, removeToast } = useToast(); const { toasts, addToast, removeToast } = useToast();
const isElectron = typeof window !== "undefined" && Boolean((window as Window & { electronAPI?: unknown }).electronAPI); const isElectron = typeof window !== "undefined" && Boolean((window as Window & { electronAPI?: unknown }).electronAPI);
// Project management hooks - MUST be called before any conditional logic // 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 { nodes } = useNodes();
const { currentProject, setCurrentProject, clearCurrentProject, loading: currentProjectLoading } = useCurrentProject(projects); 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) // 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 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 // Use remote data when in remote mode, local data otherwise
const effectiveProjects = isRemote && remoteData.projects.length > 0 ? remoteData.projects : projects; 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 // Tasks hook with project context and search query
const { tasks, createTask, moveTask, deleteTask, mergeTask, retryTask, updateTask, duplicateTask, archiveTask, unarchiveTask, archiveAllDone, loadArchivedTasks, lastFetchTimeMs } = useTasks( const { tasks, createTask, moveTask, deleteTask, mergeTask, retryTask, updateTask, duplicateTask, archiveTask, unarchiveTask, archiveAllDone, loadArchivedTasks, lastFetchTimeMs } = useTasks(
@@ -132,7 +130,6 @@ function AppInner() {
// Settings state // Settings state
const { const {
maxConcurrent, maxConcurrent,
rootDir,
autoMerge, autoMerge,
globalPaused, globalPaused,
enginePaused, enginePaused,
@@ -152,7 +149,7 @@ function AppInner() {
toggleFavoriteModel, toggleFavoriteModel,
} = useFavorites(); } = useFavorites();
const { viewMode, setViewMode, taskView, handleChangeTaskView, handleToggleTheme } = useViewState({ const { viewMode, setViewMode, taskView, handleChangeTaskView } = useViewState({
projectsLoading, projectsLoading,
currentProjectLoading, currentProjectLoading,
currentProject, currentProject,
@@ -245,12 +242,6 @@ function AppInner() {
setNodesOpen((prev) => !prev); setNodesOpen((prev) => !prev);
}, []); }, []);
const handleOpenMissionsView = useCallback(() => {
setMissionTargetId(undefined);
setMissionResumeSessionId(undefined);
handleChangeTaskView("missions");
}, [handleChangeTaskView]);
const handleOpenMission = useCallback((missionId: string) => { const handleOpenMission = useCallback((missionId: string) => {
setMissionTargetId(missionId); setMissionTargetId(missionId);
setMissionResumeSessionId(undefined); setMissionResumeSessionId(undefined);

View File

@@ -1,6 +1,5 @@
import { memo, useCallback, useMemo } from "react"; import { memo, useCallback, useMemo } from "react";
import { import {
GitPullRequest,
GitMerge, GitMerge,
CheckCircle, CheckCircle,
XCircle, XCircle,

View File

@@ -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 { 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 { clearActivityLog, type ActivityLogEntry, type ActivityEventType, type ActivityFeedEntry } from "../api";
import { useActivityLog } from "../hooks/useActivityLog"; import { useActivityLog } from "../hooks/useActivityLog";
@@ -81,6 +81,7 @@ export function ActivityLogModal({
onProjectFilterChange, onProjectFilterChange,
currentProject, currentProject,
}: ActivityLogModalProps) { }: ActivityLogModalProps) {
// tasks parameter reserved for future use when filtering activity by task
const [filteredType, setFilteredType] = useState<ActivityEventType | "all">("all"); const [filteredType, setFilteredType] = useState<ActivityEventType | "all">("all");
const [filteredProjectId, setFilteredProjectId] = useState<string | "all">(projectId || "all"); const [filteredProjectId, setFilteredProjectId] = useState<string | "all">(projectId || "all");
const [showConfirmClear, setShowConfirmClear] = useState(false); const [showConfirmClear, setShowConfirmClear] = useState(false);
@@ -136,7 +137,7 @@ export function ActivityLogModal({
await clearActivityLog(); await clearActivityLog();
refresh(); refresh();
setShowConfirmClear(false); setShowConfirmClear(false);
} catch (err) { } catch {
// Error handled by hook // Error handled by hook
setShowConfirmClear(false); setShowConfirmClear(false);
} }

View File

@@ -109,7 +109,7 @@ export function AddNodeModal({ isOpen, onClose, onSubmit, addToast }: AddNodeMod
try { try {
await onSubmit(input); await onSubmit(input);
addToast(`Node \"${input.name}\" registered`, "success"); addToast(`Node "${input.name}" registered`, "success");
closeModal(); closeModal();
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : "Failed to register node"; const message = error instanceof Error ? error.message : "Failed to register node";

View File

@@ -1,6 +1,6 @@
import { useState, useEffect, useCallback, useRef, useMemo } from "react"; import { useState, useEffect, useCallback, useRef, useMemo } from "react";
import type { JSX } 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 type { Agent, AgentCapability, AgentState } from "../api";
import { fetchAgents, createAgent, updateAgent, updateAgentState, deleteAgent } from "../api"; import { fetchAgents, createAgent, updateAgent, updateAgentState, deleteAgent } from "../api";
import { getScopedItem, setScopedItem } from "../utils/projectStorage"; 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") { if (e.key === "Escape") {
setEditingRoleForAgent(null); setEditingRoleForAgent(null);
} }

View File

@@ -1,6 +1,6 @@
import { useState, useEffect, useCallback, useRef, useMemo } from "react"; import { useState, useEffect, useCallback, useRef, useMemo } from "react";
import type { JSX } 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 type { Agent, AgentCapability, AgentState, OrgTreeNode } from "../api";
import { fetchAgents, updateAgent, updateAgentState, deleteAgent, startAgentRun, fetchOrgTree } from "../api"; import { fetchAgents, updateAgent, updateAgentState, deleteAgent, startAgentRun, fetchOrgTree } from "../api";
import { AgentDetailView } from "./AgentDetailView"; import { AgentDetailView } from "./AgentDetailView";

View File

@@ -1,5 +1,5 @@
import type { ProjectInfo } from "../api"; 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 { UseProjectActionsResult } from "../hooks/useProjectActions";
import type { ModalManager } from "../hooks/useModalManager"; import type { ModalManager } from "../hooks/useModalManager";
import type { UseTaskHandlersResult } from "../hooks/useTaskHandlers"; import type { UseTaskHandlersResult } from "../hooks/useTaskHandlers";

View File

@@ -95,11 +95,6 @@ export function CustomModelDropdown({
}, [favoriteModels, filteredModels]); }, [favoriteModels, filteredModels]);
// Sort providers: favorites first (in order), then alphabetically // 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 sortedProviderEntries = useMemo(() => {
const entries = Object.entries(modelsByProvider); const entries = Object.entries(modelsByProvider);
const favoritesSet = new Set(favoriteProviders); const favoritesSet = new Set(favoriteProviders);

View File

@@ -1,6 +1,6 @@
import { useMemo } from "react"; import { useMemo } from "react";
import type { Task } from "@fusion/core"; 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 { useExecutorStats } from "../hooks/useExecutorStats";
import type { ExecutorState, AiSessionSummary } from "../api"; import type { ExecutorState, AiSessionSummary } from "../api";
import { BackgroundTasksIndicator } from "./BackgroundTasksIndicator"; import { BackgroundTasksIndicator } from "./BackgroundTasksIndicator";

View File

@@ -319,7 +319,6 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId
// Determine state flags // Determine state flags
const hasRemotes = remotes.length > 0; const hasRemotes = remotes.length > 0;
const singleRemote = remotes.length === 1; const singleRemote = remotes.length === 1;
const multipleRemotes = remotes.length > 1;
// Tab-specific counts // Tab-specific counts
const importedIssueCount = issues.filter((issue) => importedUrls.has(issue.html_url)).length; const importedIssueCount = issues.filter((issue) => importedUrls.has(issue.html_url)).length;

View File

@@ -1,5 +1,5 @@
import { useState, useCallback, useMemo, Fragment, useEffect, useRef } from "react"; 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 type { Task, TaskDetail, Column, TaskStep, TaskCreateInput } from "@fusion/core";
import { COLUMN_LABELS, COLUMNS } from "@fusion/core"; import { COLUMN_LABELS, COLUMNS } from "@fusion/core";
import { batchUpdateTaskModels } from "../api"; import { batchUpdateTaskModels } from "../api";
@@ -122,8 +122,6 @@ interface ListViewProps {
onTasksUpdated?: (updatedTasks: Task[]) => void; onTasksUpdated?: (updatedTasks: Task[]) => void;
/** Project ID for multi-project context (optional) */ /** Project ID for multi-project context (optional) */
projectId?: string; projectId?: string;
/** Project name for display (optional) */
projectName?: string;
/** Project-level stuck task timeout in milliseconds (undefined = disabled) */ /** Project-level stuck task timeout in milliseconds (undefined = disabled) */
taskStuckTimeoutMs?: number; taskStuckTimeoutMs?: number;
/** External search query from header search (defaults to "") */ /** External search query from header search (defaults to "") */
@@ -161,7 +159,6 @@ export function ListView({
onSubtaskBreakdown, onSubtaskBreakdown,
onTasksUpdated, onTasksUpdated,
projectId, projectId,
projectName,
taskStuckTimeoutMs, taskStuckTimeoutMs,
searchQuery = "", searchQuery = "",
lastFetchTimeMs, lastFetchTimeMs,

View File

@@ -117,8 +117,6 @@ export function PluginManager({ addToast, projectId }: PluginManagerProps) {
return; return;
} }
const currentPlugins = pluginsRef.current;
switch (payload.transition) { switch (payload.transition) {
case "installing": case "installing":
case "enabled": case "enabled":

View File

@@ -1,5 +1,5 @@
import { memo, useCallback, useState } from "react"; 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 { RegisteredProject, ProjectHealth, ProjectStatus } from "@fusion/core";
import type { NodeInfo } from "../api"; import type { NodeInfo } from "../api";

View File

@@ -102,12 +102,13 @@ export function ProjectOverview({
case "name": case "name":
comparison = a.project.name.localeCompare(b.project.name); comparison = a.project.name.localeCompare(b.project.name);
break; break;
case "activity": case "activity": {
const aTime = a.project.lastActivityAt || a.health?.lastActivityAt || a.project.updatedAt; const aTime = a.project.lastActivityAt || a.health?.lastActivityAt || a.project.updatedAt;
const bTime = b.project.lastActivityAt || b.health?.lastActivityAt || b.project.updatedAt; const bTime = b.project.lastActivityAt || b.health?.lastActivityAt || b.project.updatedAt;
comparison = new Date(bTime).getTime() - new Date(aTime).getTime(); comparison = new Date(bTime).getTime() - new Date(aTime).getTime();
break; break;
case "status": }
case "status": {
const statusOrder: Record<ProjectStatus, number> = { const statusOrder: Record<ProjectStatus, number> = {
errored: 0, errored: 0,
initializing: 1, initializing: 1,
@@ -116,6 +117,7 @@ export function ProjectOverview({
}; };
comparison = statusOrder[a.project.status] - statusOrder[b.project.status]; comparison = statusOrder[a.project.status] - statusOrder[b.project.status];
break; break;
}
} }
return sortDirection === "asc" ? comparison : -comparison; return sortDirection === "asc" ? comparison : -comparison;

View File

@@ -3,7 +3,6 @@ import { Calendar, Webhook, Code, Zap } from "lucide-react";
import type { import type {
Routine, Routine,
RoutineCreateInput, RoutineCreateInput,
RoutineUpdateInput,
RoutineTrigger, RoutineTrigger,
RoutineTriggerType, RoutineTriggerType,
RoutineCronTrigger, RoutineCronTrigger,
@@ -22,7 +21,7 @@ function isLikelyCron(expr: string): boolean {
const parts = expr.trim().split(/\s+/); const parts = expr.trim().split(/\s+/);
if (parts.length !== 5) return false; if (parts.length !== 5) return false;
// Each field should contain digits, *, /, -, or , // Each field should contain digits, *, /, -, or ,
return parts.every((p) => /^[\d*,/\-]+$/.test(p)); return parts.every((p) => /^[\d*,/-]+$/.test(p));
} }
/** /**

View File

@@ -38,7 +38,7 @@ function isLikelyCron(expr: string): boolean {
const parts = expr.trim().split(/\s+/); const parts = expr.trim().split(/\s+/);
if (parts.length !== 5) return false; if (parts.length !== 5) return false;
// Each field should contain digits, *, /, -, or , // Each field should contain digits, *, /, -, or ,
return parts.every((p) => /^[\d*,/\-]+$/.test(p)); return parts.every((p) => /^[\d*,/-]+$/.test(p));
} }
type ScheduleMode = "simple" | "advanced"; type ScheduleMode = "simple" | "advanced";

View File

@@ -431,8 +431,6 @@ export function ScheduledTasksModal({ onClose, addToast }: ScheduledTasksModalPr
// Determine if we're in "list" view for showing the "New" button // Determine if we're in "list" view for showing the "New" button
const isShowingList = const isShowingList =
activeTab === "schedules" ? view === "list" && schedules.length > 0 : routineView === "list" && routines.length > 0; 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 ( return (
<div className="modal-overlay open" onClick={handleOverlayClick}> <div className="modal-overlay open" onClick={handleOverlayClick}>

View File

@@ -1,9 +1,9 @@
import { useState, useEffect, useCallback, useRef } from "react"; import { useState, useEffect, useCallback, useRef } from "react";
import { Globe, Folder } from "lucide-react"; import { Globe, Folder } from "lucide-react";
import { THINKING_LEVELS, PROMPT_KEY_CATALOG, isGlobalSettingsKey, isProjectSettingsKey } from "@fusion/core"; import { THINKING_LEVELS, isGlobalSettingsKey, isProjectSettingsKey } from "@fusion/core";
import type { Settings, GlobalSettings, ThemeMode, ColorTheme, ModelPreset, NtfyNotificationEvent, PromptKey, AgentPromptsConfig } 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 { 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 { useMemoryBackendStatus } from "../hooks/useMemoryBackendStatus";
import type { ToastType } from "../hooks/useToast"; import type { ToastType } from "../hooks/useToast";
import { ThemeSelector } from "./ThemeSelector"; import { ThemeSelector } from "./ThemeSelector";

View File

@@ -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 { Pencil, Bot, X, ChevronDown } from "lucide-react";
import ReactMarkdown from "react-markdown"; import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm"; 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 { 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 { uploadAttachment, deleteAttachment, updateTask, pauseTask, unpauseTask, fetchTaskDetail, fetchSettings, requestSpecRevision, rebuildTaskSpec, approvePlan, rejectPlan, refineTask, fetchWorkflowResults, assignTask, fetchAgents, fetchAgent } from "../api";
import type { WorkflowStepResult } from "@fusion/core"; import type { WorkflowStepResult } from "@fusion/core";
@@ -23,24 +23,10 @@ interface ModelSelection {
modelId?: string; modelId?: string;
} }
function normalizeModelField(value: string | null | undefined): string | undefined { function _normalizeModelField(value: string | null | undefined): string | undefined {
return value ?? 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: * Resolve the effective executor model following the engine's resolution order:
* 1. Per-task modelProvider/modelId (both must be set) * 1. Per-task modelProvider/modelId (both must be set)
@@ -359,7 +345,7 @@ export function TaskDetailModal({
}) })
.catch((err: any) => { .catch((err: any) => {
if (!cancelled) { 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(() => { .finally(() => {
@@ -557,8 +543,8 @@ export function TaskDetailModal({
setEditPendingImages([]); setEditPendingImages([]);
addToast(`Updated ${task.id}`, "success"); addToast(`Updated ${task.id}`, "success");
setIsEditing(false); setIsEditing(false);
} catch (err: any) { } catch (err: unknown) {
addToast(`Failed to update ${task.id}: ${err.message}`, "error"); addToast(`Failed to update ${task.id}: ${err instanceof Error ? err.message : String(err)}`, "error");
} finally { } finally {
if (mountedRef.current) { if (mountedRef.current) {
setIsSaving(false); setIsSaving(false);
@@ -570,8 +556,8 @@ export function TaskDetailModal({
try { try {
await updateTask(task.id, { description }, projectId); await updateTask(task.id, { description }, projectId);
addToast("Description saved", "success"); addToast("Description saved", "success");
} catch (err: any) { } catch (err: unknown) {
addToast(`Failed to save: ${err.message}`, "error"); addToast(`Failed to save: ${err instanceof Error ? err.message : String(err)}`, "error");
} }
}, [task.id, addToast, projectId]); }, [task.id, addToast, projectId]);
@@ -620,8 +606,8 @@ export function TaskDetailModal({
await onMoveTask(task.id, column); await onMoveTask(task.id, column);
onClose(); onClose();
addToast(`Moved to ${COLUMN_LABELS[column]}`, "success"); addToast(`Moved to ${COLUMN_LABELS[column]}`, "success");
} catch (err: any) { } catch (err: unknown) {
addToast(err.message, "error"); addToast(err instanceof Error ? err.message : String(err), "error");
} }
}, },
[task.id, onMoveTask, onClose, addToast], [task.id, onMoveTask, onClose, addToast],
@@ -633,8 +619,8 @@ export function TaskDetailModal({
await onDeleteTask(task.id); await onDeleteTask(task.id);
onClose(); onClose();
addToast(`Deleted ${task.id}`, "info"); addToast(`Deleted ${task.id}`, "info");
} catch (err: any) { } catch (err: unknown) {
addToast(err.message, "error"); addToast(err instanceof Error ? err.message : String(err), "error");
} }
}, [task.id, onDeleteTask, onClose, addToast]); }, [task.id, onDeleteTask, onClose, addToast]);
@@ -650,7 +636,7 @@ export function TaskDetailModal({
addToast(msg, "success"); addToast(msg, "success");
}) })
.catch((err: any) => { .catch((err: any) => {
addToast(err.message, "error"); addToast(err instanceof Error ? err.message : String(err), "error");
}); });
}, [task.id, onMergeTask, onClose, addToast]); }, [task.id, onMergeTask, onClose, addToast]);
@@ -660,8 +646,8 @@ export function TaskDetailModal({
await onRetryTask(task.id); await onRetryTask(task.id);
onClose(); onClose();
addToast(`Retrying ${task.id}...`, "info"); addToast(`Retrying ${task.id}...`, "info");
} catch (err: any) { } catch (err: unknown) {
addToast(err.message, "error"); addToast(err instanceof Error ? err.message : String(err), "error");
} }
}, [task.id, onRetryTask, onClose, addToast]); }, [task.id, onRetryTask, onClose, addToast]);
@@ -672,8 +658,8 @@ export function TaskDetailModal({
const newTask = await onDuplicateTask(task.id); const newTask = await onDuplicateTask(task.id);
onClose(); onClose();
addToast(`Duplicated ${task.id} → ${newTask.id}`, "success"); addToast(`Duplicated ${task.id} → ${newTask.id}`, "success");
} catch (err: any) { } catch (err: unknown) {
addToast(err.message, "error"); addToast(err instanceof Error ? err.message : String(err), "error");
} }
}, [task.id, onDuplicateTask, onClose, addToast]); }, [task.id, onDuplicateTask, onClose, addToast]);
@@ -687,8 +673,8 @@ export function TaskDetailModal({
addToast(`Paused ${task.id}`, "success"); addToast(`Paused ${task.id}`, "success");
} }
onClose(); onClose();
} catch (err: any) { } catch (err: unknown) {
addToast(err.message, "error"); addToast(err instanceof Error ? err.message : String(err), "error");
} }
}, [task.id, task.paused, onClose, addToast]); }, [task.id, task.paused, onClose, addToast]);
@@ -697,8 +683,8 @@ export function TaskDetailModal({
await approvePlan(task.id, projectId); await approvePlan(task.id, projectId);
addToast(`Plan approved — ${task.id} moved to Todo`, "success"); addToast(`Plan approved — ${task.id} moved to Todo`, "success");
onClose(); onClose();
} catch (err: any) { } catch (err: unknown) {
addToast(err.message, "error"); addToast(err instanceof Error ? err.message : String(err), "error");
} }
}, [task.id, onClose, addToast]); }, [task.id, onClose, addToast]);
@@ -708,8 +694,8 @@ export function TaskDetailModal({
await rejectPlan(task.id, projectId); await rejectPlan(task.id, projectId);
addToast(`Plan rejected — ${task.id} returned to Triage for re-specification`, "info"); addToast(`Plan rejected — ${task.id} returned to Triage for re-specification`, "info");
onClose(); onClose();
} catch (err: any) { } catch (err: unknown) {
addToast(err.message, "error"); addToast(err instanceof Error ? err.message : String(err), "error");
} }
}, [task.id, onClose, addToast]); }, [task.id, onClose, addToast]);
@@ -719,8 +705,8 @@ export function TaskDetailModal({
await rebuildTaskSpec(task.id, projectId); await rebuildTaskSpec(task.id, projectId);
onClose(); onClose();
addToast(`Respecifying ${task.id}...`, "info"); addToast(`Respecifying ${task.id}...`, "info");
} catch (err: any) { } catch (err: unknown) {
addToast(err.message, "error"); addToast(err instanceof Error ? err.message : String(err), "error");
} }
}, [task.id, projectId, onClose, addToast]); }, [task.id, projectId, onClose, addToast]);
@@ -771,8 +757,8 @@ export function TaskDetailModal({
const newTask = await refineTask(task.id, refineFeedback.trim(), projectId); const newTask = await refineTask(task.id, refineFeedback.trim(), projectId);
addToast(`Refinement task created: ${newTask.id}`, "success"); addToast(`Refinement task created: ${newTask.id}`, "success");
onClose(); onClose();
} catch (err: any) { } catch (err: unknown) {
addToast(err.message, "error"); addToast(err instanceof Error ? err.message : String(err), "error");
} finally { } finally {
setIsRefining(false); setIsRefining(false);
} }
@@ -784,8 +770,8 @@ export function TaskDetailModal({
const attachment = await uploadAttachment(task.id, file, projectId); const attachment = await uploadAttachment(task.id, file, projectId);
setAttachments((prev) => [...prev, attachment]); setAttachments((prev) => [...prev, attachment]);
addToast("Screenshot attached", "success"); addToast("Screenshot attached", "success");
} catch (err: any) { } catch (err: unknown) {
addToast(err.message, "error"); addToast(err instanceof Error ? err.message : String(err), "error");
} finally { } finally {
setUploading(false); setUploading(false);
} }
@@ -839,8 +825,8 @@ export function TaskDetailModal({
await deleteAttachment(task.id, filename, projectId); await deleteAttachment(task.id, filename, projectId);
setAttachments((prev) => prev.filter((a) => a.filename !== filename)); setAttachments((prev) => prev.filter((a) => a.filename !== filename));
addToast("Attachment deleted", "info"); addToast("Attachment deleted", "info");
} catch (err: any) { } catch (err: unknown) {
addToast(err.message, "error"); addToast(err instanceof Error ? err.message : String(err), "error");
} }
}, [task.id, addToast]); }, [task.id, addToast]);
@@ -852,9 +838,9 @@ export function TaskDetailModal({
const updatedTask = await updateTask(task.id, { enabledWorkflowSteps }, projectId); const updatedTask = await updateTask(task.id, { enabledWorkflowSteps }, projectId);
addToast("Workflow steps updated", "success"); addToast("Workflow steps updated", "success");
onTaskUpdated?.(updatedTask); onTaskUpdated?.(updatedTask);
} catch (err: any) { } catch (err: unknown) {
setWorkflowEnabledSteps(previousSteps); 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]); }, [task.id, projectId, workflowEnabledSteps, onTaskUpdated, addToast]);
@@ -864,8 +850,8 @@ export function TaskDetailModal({
const loadedAgents = await fetchAgents(undefined, projectId); const loadedAgents = await fetchAgents(undefined, projectId);
setAgents(loadedAgents); setAgents(loadedAgents);
setShowAgentPicker(true); setShowAgentPicker(true);
} catch (err: any) { } catch (err: unknown) {
addToast(`Failed to load agents: ${err.message}`, "error"); addToast(`Failed to load agents: ${err instanceof Error ? err.message : String(err)}`, "error");
setShowAgentPicker(false); setShowAgentPicker(false);
} finally { } finally {
setAgentsLoading(false); setAgentsLoading(false);
@@ -884,8 +870,8 @@ export function TaskDetailModal({
setShowAgentPicker(false); setShowAgentPicker(false);
onTaskUpdated?.(updatedTask); onTaskUpdated?.(updatedTask);
addToast("Assigned agent updated", "success"); addToast("Assigned agent updated", "success");
} catch (err: any) { } catch (err: unknown) {
addToast(`Failed to assign agent: ${err.message}`, "error"); addToast(`Failed to assign agent: ${err instanceof Error ? err.message : String(err)}`, "error");
} }
}, [task.id, projectId, agents, onTaskUpdated, addToast]); }, [task.id, projectId, agents, onTaskUpdated, addToast]);
@@ -896,8 +882,8 @@ export function TaskDetailModal({
setShowAgentPicker(false); setShowAgentPicker(false);
onTaskUpdated?.(updatedTask); onTaskUpdated?.(updatedTask);
addToast("Agent unassigned", "success"); addToast("Agent unassigned", "success");
} catch (err: any) { } catch (err: unknown) {
addToast(`Failed to unassign agent: ${err.message}`, "error"); addToast(`Failed to unassign agent: ${err instanceof Error ? err.message : String(err)}`, "error");
} }
}, [task.id, projectId, onTaskUpdated, addToast]); }, [task.id, projectId, onTaskUpdated, addToast]);
@@ -906,9 +892,10 @@ export function TaskDetailModal({
setDependencies(newDeps); setDependencies(newDeps);
try { try {
await updateTask(task.id, { dependencies: newDeps }, projectId); await updateTask(task.id, { dependencies: newDeps }, projectId);
} catch (err: any) { } catch (err: unknown) {
setDependencies(dependencies); 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]); }, [task.id, dependencies, addToast]);
@@ -918,9 +905,10 @@ export function TaskDetailModal({
setDependencies(newDeps); setDependencies(newDeps);
try { try {
await updateTask(task.id, { dependencies: newDeps }, projectId); await updateTask(task.id, { dependencies: newDeps }, projectId);
} catch (err: any) { } catch (err: unknown) {
setDependencies(dependencies); 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]); }, [task.id, dependencies, addToast]);
@@ -928,7 +916,7 @@ export function TaskDetailModal({
try { try {
const detail = await fetchTaskDetail(depId, projectId); const detail = await fetchTaskDetail(depId, projectId);
onOpenDetail(detail); onOpenDetail(detail);
} catch (err: any) { } catch (err: unknown) {
addToast(`Failed to load dependency ${depId}`, "error"); addToast(`Failed to load dependency ${depId}`, "error");
} }
}, [onOpenDetail, addToast]); }, [onOpenDetail, addToast]);
@@ -943,8 +931,8 @@ export function TaskDetailModal({
if (fullDetail) { if (fullDetail) {
fullDetail.prompt = newContent; fullDetail.prompt = newContent;
} }
} catch (err: any) { } catch (err: unknown) {
addToast(err.message, "error"); addToast(err instanceof Error ? err.message : String(err), "error");
throw err; throw err;
} finally { } finally {
setIsSavingSpec(false); setIsSavingSpec(false);
@@ -958,11 +946,12 @@ export function TaskDetailModal({
addToast("AI revision requested. Task moved to triage.", "success"); addToast("AI revision requested. Task moved to triage.", "success");
// Task has been moved to triage, close modal // Task has been moved to triage, close modal
onClose(); onClose();
} catch (err: any) { } catch (err: unknown) {
if (err.message?.includes("in-review") || err.message?.includes("done")) { 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"); addToast("Cannot request revision: Task must be in 'todo' or 'in-progress' column.", "error");
} else { } else {
addToast(err.message, "error"); addToast(message, "error");
} }
} finally { } finally {
setIsRequestingRevision(false); setIsRequestingRevision(false);

View File

@@ -1,10 +1,9 @@
import { useCallback, useEffect, useState } from "react"; 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 { Task, TaskDocument, TaskDocumentRevision } from "@fusion/core";
import type { ToastType } from "../hooks/useToast"; import type { ToastType } from "../hooks/useToast";
import { import {
fetchTaskDocuments, fetchTaskDocuments,
fetchTaskDocument,
fetchTaskDocumentRevisions, fetchTaskDocumentRevisions,
putTaskDocument, putTaskDocument,
deleteTaskDocument, deleteTaskDocument,

View File

@@ -69,8 +69,9 @@ export async function retryDynamicImport<T>(
/** Whether the current device is likely mobile (touch-primary, small viewport). */ /** Whether the current device is likely mobile (touch-primary, small viewport). */
function isMobileDevice(): boolean { function isMobileDevice(): boolean {
if (typeof window === "undefined") return false; if (typeof window === "undefined") return false;
const nav = navigator as Navigator & { maxTouchPoints?: number };
const hasTouchScreen = const hasTouchScreen =
"ontouchstart" in window || (navigator as any).maxTouchPoints > 0; "ontouchstart" in window || (nav.maxTouchPoints ?? 0) > 0;
const isNarrow = window.innerWidth <= 768; const isNarrow = window.innerWidth <= 768;
return hasTouchScreen && isNarrow; return hasTouchScreen && isNarrow;
} }
@@ -601,7 +602,9 @@ export function TerminalModal({ isOpen, onClose, initialCommand, projectId }: Te
if (xtermRef.current) { if (xtermRef.current) {
const currentSize = xtermRef.current.options.fontSize || 14; const currentSize = xtermRef.current.options.fontSize || 14;
xtermRef.current.options.fontSize = Math.min(currentSize + 1, 32); 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; return;
} }
@@ -612,7 +615,9 @@ export function TerminalModal({ isOpen, onClose, initialCommand, projectId }: Te
if (xtermRef.current) { if (xtermRef.current) {
const currentSize = xtermRef.current.options.fontSize || 14; const currentSize = xtermRef.current.options.fontSize || 14;
xtermRef.current.options.fontSize = Math.max(currentSize - 1, 8); 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; return;
} }
@@ -622,7 +627,9 @@ export function TerminalModal({ isOpen, onClose, initialCommand, projectId }: Te
e.preventDefault(); e.preventDefault();
if (xtermRef.current) { if (xtermRef.current) {
xtermRef.current.options.fontSize = 14; 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; return;
} }
@@ -726,8 +733,9 @@ export function TerminalModal({ isOpen, onClose, initialCommand, projectId }: Te
// Restart the active tab's session // Restart the active tab's session
try { try {
await restartActiveTab(); await restartActiveTab();
} catch (err: any) { } catch (err: unknown) {
setError(err.message || "Failed to restart terminal session"); const message = err instanceof Error ? err.message : String(err);
setError(message || "Failed to restart terminal session");
} }
}, [restartActiveTab]); }, [restartActiveTab]);

View File

@@ -209,8 +209,8 @@ export interface MissionWithHierarchy extends Mission {
export type MissionEventType = CoreMissionEventType; export type MissionEventType = CoreMissionEventType;
/** Mission lifecycle event persisted in the mission event log. */ /** 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. */ /** Computed mission health snapshot returned by observability APIs. */
export interface MissionHealth extends CoreMissionHealth {} export type MissionHealth = CoreMissionHealth;

View File

@@ -68,11 +68,12 @@ export function useBatchBadgeFetch(projectId?: string): UseBatchBadgeFetchResult
try { try {
const results = await fetchBatchStatus(taskIds, projectId); const results = await fetchBatchStatus(taskIds, projectId);
return results; return results;
} catch (err: any) { } catch (err: unknown) {
lastError = err instanceof Error ? err : new Error(String(err)); 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 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 const delayMs = Math.min(1000 * Math.pow(2, attempt), 30000); // Max 30s delay
await new Promise((resolve) => setTimeout(resolve, delayMs)); await new Promise((resolve) => setTimeout(resolve, delayMs));
continue; continue;
@@ -143,7 +144,7 @@ export function useBatchBadgeFetch(projectId?: string): UseBatchBadgeFetchResult
batchBadgeStore.data.set(getScopedTaskKey(taskId, projectId), { result, timestamp }); batchBadgeStore.data.set(getScopedTaskKey(taskId, projectId), { result, timestamp });
} }
batchBadgeStore.lastFetchTime = timestamp; batchBadgeStore.lastFetchTime = timestamp;
} catch (err) { } catch {
// Even on error, we don't throw - the hook handles errors gracefully // Even on error, we don't throw - the hook handles errors gracefully
// and partial results are still stored // and partial results are still stored
} finally { } finally {

View File

@@ -56,9 +56,10 @@ export function useFileBrowser(taskId: string, enabled: boolean, projectId?: str
if (!cancelled) { if (!cancelled) {
setEntries(response.entries); setEntries(response.entries);
} }
} catch (err: any) { } catch (err: unknown) {
if (!cancelled) { 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([]); setEntries([]);
} }
} finally { } finally {

View File

@@ -65,9 +65,10 @@ export function useFileEditor(
setOriginalContent(response.content); setOriginalContent(response.content);
setMtime(response.mtime); setMtime(response.mtime);
} }
} catch (err: any) { } catch (err: unknown) {
if (!cancelled) { 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(""); setContentState("");
setOriginalContent(""); setOriginalContent("");
setMtime(null); setMtime(null);
@@ -100,8 +101,9 @@ export function useFileEditor(
const response: SaveFileResponse = await saveFileContent(taskId, filePath, content, projectId); const response: SaveFileResponse = await saveFileContent(taskId, filePath, content, projectId);
setOriginalContent(content); setOriginalContent(content);
setMtime(response.mtime); setMtime(response.mtime);
} catch (err: any) { } catch (err: unknown) {
setError(err.message || "Failed to save file"); const message = err instanceof Error ? err.message : String(err);
setError(message || "Failed to save file");
throw err; throw err;
} finally { } finally {
setSaving(false); setSaving(false);

View File

@@ -54,9 +54,10 @@ export function useProjectFileBrowser(rootPath: string, enabled: boolean): UsePr
if (!cancelled) { if (!cancelled) {
setEntries(response.entries); setEntries(response.entries);
} }
} catch (err: any) { } catch (err: unknown) {
if (!cancelled) { 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([]); setEntries([]);
} }
} finally { } finally {

View File

@@ -63,9 +63,10 @@ export function useProjectFileEditor(
setOriginalContent(response.content); setOriginalContent(response.content);
setMtime(response.mtime); setMtime(response.mtime);
} }
} catch (err: any) { } catch (err: unknown) {
if (!cancelled) { 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(""); setContentState("");
setOriginalContent(""); setOriginalContent("");
setMtime(null); setMtime(null);
@@ -98,8 +99,9 @@ export function useProjectFileEditor(
const response: SaveFileResponse = await saveWorkspaceFileContent("project", filePath, content); const response: SaveFileResponse = await saveWorkspaceFileContent("project", filePath, content);
setOriginalContent(content); setOriginalContent(content);
setMtime(response.mtime); setMtime(response.mtime);
} catch (err: any) { } catch (err: unknown) {
setError(err.message || "Failed to save file"); const message = err instanceof Error ? err.message : String(err);
setError(message || "Failed to save file");
throw err; throw err;
} finally { } finally {
setSaving(false); setSaving(false);

View File

@@ -25,11 +25,6 @@ export interface TerminalTab {
createdAt: number; createdAt: number;
} }
interface StoredTab extends TerminalTab {
/** Marked as unverified during server validation */
_verified?: boolean;
}
interface UseTerminalSessionsReturn { interface UseTerminalSessionsReturn {
/** All terminal tabs */ /** All terminal tabs */
tabs: TerminalTab[]; tabs: TerminalTab[];

View File

@@ -57,14 +57,15 @@ export function useUsageData(options: UseUsageDataOptions = {}) {
error: null, error: null,
lastUpdated: new Date(), lastUpdated: new Date(),
}); });
} catch (err: any) { } catch (err: unknown) {
// Don't update state if the request was aborted // 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) => ({ setState((prev) => ({
...prev, ...prev,
loading: false, loading: false,
error: err.message || "Failed to fetch usage data", error: message || "Failed to fetch usage data",
})); }));
} }
}, []); }, []);

View File

@@ -65,9 +65,10 @@ export function useWorkspaceFileBrowser(
if (!cancelled) { if (!cancelled) {
setEntries(response.entries); setEntries(response.entries);
} }
} catch (err: any) { } catch (err: unknown) {
if (!cancelled) { 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([]); setEntries([]);
} }
} finally { } finally {

View File

@@ -63,9 +63,10 @@ export function useWorkspaceFileEditor(
setOriginalContent(response.content); setOriginalContent(response.content);
setMtime(response.mtime); setMtime(response.mtime);
} }
} catch (err: any) { } catch (err: unknown) {
if (!cancelled) { 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(""); setContentState("");
setOriginalContent(""); setOriginalContent("");
setMtime(null); setMtime(null);
@@ -98,8 +99,9 @@ export function useWorkspaceFileEditor(
const response: SaveFileResponse = await saveWorkspaceFileContent(workspace, filePath, content, projectId); const response: SaveFileResponse = await saveWorkspaceFileContent(workspace, filePath, content, projectId);
setOriginalContent(content); setOriginalContent(content);
setMtime(response.mtime); setMtime(response.mtime);
} catch (err: any) { } catch (err: unknown) {
setError(err.message || "Failed to save file"); const message = err instanceof Error ? err.message : String(err);
setError(message || "Failed to save file");
throw err; throw err;
} finally { } finally {
setSaving(false); setSaving(false);

View File

@@ -56,9 +56,10 @@ export function useWorkspaces(projectId?: string): UseWorkspacesReturn {
setProjectName(getProjectName(response.project)); setProjectName(getProjectName(response.project));
setWorkspaces(response.tasks.map(mapTaskWorkspace)); setWorkspaces(response.tasks.map(mapTaskWorkspace));
setError(null); setError(null);
} catch (err: any) { } catch (err: unknown) {
if (!cancelled) { 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 { } finally {
if (!cancelled) { if (!cancelled) {

View File

@@ -22,7 +22,7 @@ if (typeof window !== "undefined") {
} }
// Mock fetch for project API tests // Mock fetch for project API tests
const originalFetch = globalThis.fetch; const _originalFetch = globalThis.fetch;
globalThis.fetch = vi.fn(async (url: RequestInfo | URL) => { globalThis.fetch = vi.fn(async (url: RequestInfo | URL) => {
const urlString = url.toString(); const urlString = url.toString();

View File

@@ -8,7 +8,7 @@ import type { ModelInfo } from "../api";
* Preserves spaces (which serve as field/word boundaries) and alphanumeric chars. * Preserves spaces (which serve as field/word boundaries) and alphanumeric chars.
*/ */
function normalize(s: string): string { function normalize(s: string): string {
return s.toLowerCase().replace(/[-_.\/]/g, ""); return s.toLowerCase().replace(/[-_./]/g, "");
} }
/** /**