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