diff --git a/app/desktop/src/components/SettingsPage.tsx b/app/desktop/src/components/SettingsPage.tsx index f74302170..8e6e66ee3 100644 --- a/app/desktop/src/components/SettingsPage.tsx +++ b/app/desktop/src/components/SettingsPage.tsx @@ -1,338 +1,120 @@ -import { type ReactNode, useMemo, useState, useEffect, useRef, useCallback } from 'react'; +import { useState, useMemo } from 'react'; +import { useQuery } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { - Archive, - ArrowLeft, - Bot, - Check, - ChevronRight, - ClipboardList, - Code2, - Computer, - Cpu, - Eye, - FolderGit2, - GitBranch, - Globe2, - HardDrive, - Keyboard, - Link2, - LockKeyhole, - LogIn, - LogOut, - MessageSquareText, - Monitor, - Palette, - Plug, - RefreshCw, - Route, - Search, - Server, - ShieldCheck, - SlidersHorizontal, - TerminalSquare, - UserCircle, - Wrench, - XCircle, + Archive, ArrowLeft, Bot, ClipboardList, Code2, Computer, Cpu, Eye, + FolderGit2, GitBranch, Globe2, HardDrive, Keyboard, Link2, LockKeyhole, LogIn, + LogOut, MessageSquareText, Monitor, Palette, Plug, Route, Server, + ShieldCheck, SlidersHorizontal, TerminalSquare, UserCircle, Wrench, } from 'lucide-react'; import { useTheme } from '@/contexts/ThemeContext'; import { useHubStore } from '@/stores/hubStore'; -import { APP_VERSION, HUB_URL } from '@/config'; +import { createHubClient } from '@/api/hubClient'; import { useAgentList } from '@/api/agentQueries'; -import { useHubExecutionTargets, usePingHubExecutionTarget } from '@/api/executionTargetQueries'; import { useCancelRun, useRuns } from '@/api/runQueries'; import { useHealth } from '@/hooks/useHealth'; import { useAuth } from '@/hooks/useAuth'; -import { useTaskBridgeStore, type AgentTask } from '@/stores/taskBridgeStore'; +import { useDeviceRegistration } from '@/hooks/useDeviceRegistration'; +import { useTaskBridgeStore } from '@/stores/taskBridgeStore'; import { preferredProfileAlias } from '@/utils/agentProfile'; +import { useModelSettingsStore } from '@/stores/modelSettingsStore'; +import type { ContactInfo, FriendRequestInfo, HubNotification, Session } from '@/api/hubClient'; + +import GeneralSection from './settings/sections/GeneralSection'; +import AppearanceSection from './settings/sections/AppearanceSection'; +import ConfigurationSection from './settings/sections/ConfigurationSection'; +import PersonalizationSection from './settings/sections/PersonalizationSection'; +import PermissionsSection from './settings/sections/PermissionsSection'; +import AgentProfilesSection from './settings/sections/AgentProfilesSection'; +import ExecutionTargetsSection from './settings/sections/ExecutionTargetsSection'; +import TasksSection from './settings/sections/TasksSection'; +import OnlineImSection from './settings/sections/OnlineImSection'; +import GroupChatSection from './settings/sections/GroupChatSection'; +import AgentSchedulingSection from './settings/sections/AgentSchedulingSection'; +import AgentMarketSection from './settings/sections/AgentMarketSection'; +import KeyboardSection from './settings/sections/KeyboardSection'; +import McpSection from './settings/sections/McpSection'; +import SkillsSection from './settings/sections/SkillsSection'; +import HooksSection from './settings/sections/HooksSection'; +import ModelsSection from './settings/sections/ModelsSection'; +import ModelMappingSection from './settings/sections/ModelMappingSection'; +import CcSwitchSection from './settings/sections/CcSwitchSection'; +import ConnectionsSection from './settings/sections/ConnectionsSection'; +import RemoteControlSection from './settings/sections/RemoteControlSection'; +import GitSection from './settings/sections/GitSection'; +import EnvironmentSection from './settings/sections/EnvironmentSection'; +import WorktreeSection from './settings/sections/WorktreeSection'; +import BrowserSection from './settings/sections/BrowserSection'; +import ComputerUseSection from './settings/sections/ComputerUseSection'; +import PlatformsSection from './settings/sections/PlatformsSection'; +import AccountSection from './settings/sections/AccountSection'; +import SecurityAuditSection from './settings/sections/SecurityAuditSection'; +import ArchivedSection from './settings/sections/ArchivedSection'; import { - useModelSettingsStore, - type ProviderHealth, - type ReasoningEffortPreference, - type ResolvedRunModelSettings, -} from '@/stores/modelSettingsStore'; -import type { AgentInfo, RunInfo, RunnerHealthItem } from '@shared/types'; -import type { ExecutionTarget, ExecutionTargetHealthState, ExecutionTargetType } from '@/api/hubClient'; + useStoredBooleanState, useStoredValueState, + readBrowserStorage, isActiveRun, getRecentRuns, statusLabelFromQuery, + statusLabelFromDevice, shortId, formatTimestamp, +} from './settings/utils'; import styles from './SettingsPage.module.css'; export type SectionId = - | 'general' - | 'appearance' - | 'configuration' - | 'personalization' - | 'permissions' - | 'agentProfiles' - | 'executionTargets' - | 'tasks' - | 'onlineIm' - | 'groupChat' - | 'agentScheduling' - | 'agentMarket' - | 'keyboard' - | 'mcp' - | 'skills' - | 'hooks' - | 'models' - | 'modelMapping' - | 'ccSwitch' - | 'connections' - | 'remoteControl' - | 'git' - | 'environment' - | 'worktree' - | 'browser' - | 'computerUse' - | 'platforms' - | 'account' - | 'securityAudit' - | 'archived'; + | 'general' | 'appearance' | 'configuration' | 'personalization' | 'permissions' + | 'agentProfiles' | 'executionTargets' | 'tasks' | 'onlineIm' | 'groupChat' + | 'agentScheduling' | 'agentMarket' | 'keyboard' | 'mcp' | 'skills' | 'hooks' + | 'models' | 'modelMapping' | 'ccSwitch' | 'connections' | 'remoteControl' + | 'git' | 'environment' | 'worktree' | 'browser' | 'computerUse' | 'platforms' + | 'account' | 'securityAudit' | 'archived'; type SelectValue = 'balanced' | 'detailed' | 'manual' | 'auto' | 'ask' | 'never'; -type SettingsSelectValue = SelectValue | ReasoningEffortPreference | ProviderHealth | string; -interface Props { - onBack: () => void; - onOpenAuth: () => void; - initialSection?: SectionId; +interface HubIMSnapshot { + contacts: ContactInfo[]; + sessions: Session[]; + friendRequests: FriendRequestInfo[]; + notifications: HubNotification[]; } interface NavItem { id: SectionId; label: string; - icon: ReactNode; + icon: React.ReactNode; group: 'workspace' | 'automation' | 'system'; } -interface ShortcutRow { - keys: string[]; - action: string; -} - -interface ProjectSkill { - id: string; - title: string; - descriptionKey: string; - status: 'ready' | 'review'; - hasScripts: boolean; - hasReferences: boolean; -} - -const STORAGE_PREFIX = 'agenthub-settings.'; const DEVICE_ID_KEY = 'agenthub_device_id'; -const MODEL_OPTIONS = [ - ['auto', 'Auto'], - ['claude-opus-4-7', 'claude-opus-4-7'], - ['claude-sonnet-4-6', 'claude-sonnet-4-6'], - ['claude-haiku-4-5', 'claude-haiku-4-5'], - ['gpt-5.5', 'gpt-5.5'], - ['glm-5.1', 'glm-5.1'], -] as const; - -const PROVIDER_OPTIONS = [ - ['tokendance-gateway', 'TokenDance Gateway'], - ['anthropic', 'Anthropic'], - ['openai', 'OpenAI'], - ['cc-switch-local', 'cc-switch local'], -] as const; - -const REASONING_OPTIONS = [ - ['low', 'Low'], - ['medium', 'Medium'], - ['high', 'High'], - ['max', 'Max'], -] as const; - -const PROVIDER_HEALTH_OPTIONS = [ - ['ready', 'Ready'], - ['degraded', 'Degraded'], - ['disabled', 'Disabled'], -] as const; - -const PROJECT_SKILLS: ProjectSkill[] = [ - { - id: 'adapter-dev', - title: 'adapter-dev', - descriptionKey: 'settings.skill.adapterDevDesc', - status: 'ready', - hasScripts: false, - hasReferences: false, - }, - { - id: 'dev-loop', - title: 'dev-loop', - descriptionKey: 'settings.skill.devLoopDesc', - status: 'ready', - hasScripts: false, - hasReferences: true, - }, - { - id: 'env-sandbox', - title: 'env-sandbox', - descriptionKey: 'settings.skill.envSandboxDesc', - status: 'ready', - hasScripts: false, - hasReferences: false, - }, - { - id: 'integration-test', - title: 'integration-test', - descriptionKey: 'settings.skill.integrationTestDesc', - status: 'ready', - hasScripts: false, - hasReferences: false, - }, - { - id: 'pre-push', - title: 'pre-push', - descriptionKey: 'settings.skill.prePushDesc', - status: 'review', - hasScripts: false, - hasReferences: false, - }, - { - id: 'test-coverage', - title: 'test-coverage', - descriptionKey: 'settings.skill.testCoverageDesc', - status: 'ready', - hasScripts: false, - hasReferences: false, - }, - { - id: 'ui-screenshot', - title: 'ui-screenshot', - descriptionKey: 'settings.skill.uiScreenshotDesc', - status: 'ready', - hasScripts: true, - hasReferences: false, - }, -]; - -function readStoredBoolean(key: string, fallback: boolean) { - try { - const stored = localStorage.getItem(`${STORAGE_PREFIX}${key}`); - if (stored === 'true') return true; - if (stored === 'false') return false; - } catch { - /* localStorage unavailable */ - } - return fallback; -} - -function readStoredValue(key: string, fallback: T) { - try { - const stored = localStorage.getItem(`${STORAGE_PREFIX}${key}`); - if (stored) return stored as T; - } catch { - /* localStorage unavailable */ - } - return fallback; -} - -function writeStoredValue(key: string, value: string | boolean) { - try { - localStorage.setItem(`${STORAGE_PREFIX}${key}`, String(value)); - } catch { - /* localStorage unavailable */ - } -} - -function readBrowserStorage(storage: 'local' | 'session', key: string) { - try { - const target = storage === 'local' ? localStorage : sessionStorage; - return target.getItem(key); - } catch { - return null; - } -} - -function isHubTargetConnected(target: ExecutionTarget) { - return target.is_online || target.health_state === 'healthy'; -} - -function filterTargetsByType(targets: ExecutionTarget[], types: ExecutionTargetType[]) { - const typeSet = new Set(types); - return targets.filter((target) => typeSet.has(target.target_type)); -} - -function countTargetsByHealth(targets: ExecutionTarget[], health: ExecutionTargetHealthState) { - return targets.filter((target) => (target.health_state ?? 'unknown') === health).length; -} - -function getTargetGroupHealth(targets: ExecutionTarget[]): ExecutionTargetHealthState { - if (targets.some((target) => target.health_state === 'healthy' || target.is_online)) return 'healthy'; - if (targets.some((target) => target.health_state === 'degraded')) return 'degraded'; - if (targets.some((target) => target.health_state === 'offline')) return 'offline'; - return 'unknown'; -} - -function formatTargetEndpoint(target: ExecutionTarget) { - if (target.host && target.port) return `${target.host}:${target.port}`; - if (target.host) return target.host; - if (target.workspace_root) return target.workspace_root; - if (target.device_id) return shortId(target.device_id); - return ''; +interface Props { + onBack: () => void; + onOpenAuth: () => void; + initialSection?: SectionId; } export default function SettingsPage({ onBack, onOpenAuth, initialSection = 'general' }: Props) { const { t } = useTranslation(); - const { themeMode, setThemeMode } = useTheme(); + const { themeMode, setThemeMode: rawSetThemeMode } = useTheme(); + const setThemeMode = (mode: string) => rawSetThemeMode(mode as 'dark' | 'light' | 'system'); const hubAuth = useAuth(); - const hubInventoryEnabled = hubAuth.isAuthenticated && Boolean(hubAuth.token); - const hubTargetsQuery = useHubExecutionTargets({ - enabled: hubInventoryEnabled, - getToken: () => hubAuth.token, - }); - const pingHubTargetMutation = usePingHubExecutionTarget({ - getToken: () => hubAuth.token, - }); const { online: edgeOnline, health } = useHealth(); const { data: agentData } = useAgentList(edgeOnline); - const { - data: runData, - isError: runsError, - isFetching: runsFetching, - isLoading: runsLoading, - refetch: refetchRuns, - } = useRuns(); + const { data: runData, isError: runsError, isFetching: runsFetching, isLoading: runsLoading, refetch: refetchRuns } = useRuns(); const cancelRunMutation = useCancelRun(); const bridgedTasks = useTaskBridgeStore((s) => s.tasks); const hubAuthenticated = useHubStore((s) => s.authenticated); const username = useHubStore((s) => s.username); const [active, setActive] = useState(initialSection); - const [navSearch, setNavSearch] = useState(''); - const navSearchRef = useRef(null); - - // Keyboard shortcut: `/` focuses the search input - const handleSettingsKeyDown = useCallback((e: KeyboardEvent) => { - const target = e.target as HTMLElement; - if (target.closest('input, textarea, select, [contenteditable]')) return; - if (e.key === '/') { - e.preventDefault(); - navSearchRef.current?.focus(); - } - }, []); - - useEffect(() => { - window.addEventListener('keydown', handleSettingsKeyDown); - return () => window.removeEventListener('keydown', handleSettingsKeyDown); - }, [handleSettingsKeyDown]); - + // eslint-disable-next-line @typescript-eslint/no-unused-vars const [compactMode, setCompactMode] = useStoredBooleanState('compactMode', false); const [autoReview, setAutoReview] = useStoredBooleanState('autoReview', true); const [fullAccess, setFullAccess] = useStoredBooleanState('fullAccess', false); const [enableMcp, setEnableMcp] = useStoredBooleanState('enableMcp', true); - const [skillSync, setSkillSync] = useStoredBooleanState('skillSync', true); const [taskSync, setTaskSync] = useStoredBooleanState('taskSync', true); const [groupChatEnabled, setGroupChatEnabled] = useStoredBooleanState('groupChat', true); const [agentSchedulingEnabled, setAgentSchedulingEnabled] = useStoredBooleanState('agentScheduling', true); const [enableHooks, setEnableHooks] = useStoredBooleanState('enableHooks', false); - const [remoteControlEnabled, setRemoteControlEnabled] = useStoredBooleanState('remoteControl', false); const [autoDetectGit, setAutoDetectGit] = useStoredBooleanState('autoDetectGit', true); const [worktreeIsolation, setWorktreeIsolation] = useStoredBooleanState('worktreeIsolation', true); const [browserPreview, setBrowserPreview] = useStoredBooleanState('browserPreview', true); const [computerConfirm, setComputerConfirm] = useStoredBooleanState('computerConfirm', true); - const [platformSync, setPlatformSync] = useStoredBooleanState('platformSync', true); const [auditTrail, setAuditTrail] = useStoredBooleanState('auditTrail', true); const [detailLevel, setDetailLevel] = useStoredValueState('detailLevel', 'detailed'); const [approvalMode, setApprovalMode] = useStoredValueState('approvalMode', 'ask'); @@ -354,25 +136,37 @@ export default function SettingsPage({ onBack, onOpenAuth, initialSection = 'gen const setCcSwitchBridge = useModelSettingsStore((s) => s.setCcSwitchBridgeEnabled); const updateCcSwitchProvider = useModelSettingsStore((s) => s.updateProvider); const resolveRunRequestOptions = useModelSettingsStore((s) => s.resolveRunRequestOptions); + const hubSessionActive = hubAuthenticated || hubAuth.isAuthenticated; + const settingsHubClient = useMemo(() => createHubClient({ getToken: () => hubAuth.token ?? null }), [hubAuth.token]); + const deviceRegistration = useDeviceRegistration(hubSessionActive ? settingsHubClient : null); + const shouldLoadAgentMarket = hubSessionActive && active === 'agentMarket'; + const shouldLoadIMSnapshot = hubSessionActive && (active === 'onlineIm' || active === 'groupChat'); + const customAgentsQuery = useQuery({ + queryKey: ['hub-settings', 'custom-agents', hubAuth.token], + queryFn: () => settingsHubClient.listCustomAgents() as Promise[]>, + enabled: shouldLoadAgentMarket, + retry: false, + }); + const hubIMSnapshotQuery = useQuery({ + queryKey: ['hub-settings', 'im-snapshot', hubAuth.token], + queryFn: async () => { + const [contacts, sessions, friendRequests, notifications] = await Promise.all([ + settingsHubClient.listContacts(), settingsHubClient.listSessions(), + settingsHubClient.listFriendRequests(), settingsHubClient.listNotifications({ limit: 20 }), + ]); + return { contacts, sessions, friendRequests, notifications }; + }, + enabled: shouldLoadIMSnapshot, + retry: false, + }); const agents = agentData?.items ?? []; const localAgentProfiles = useMemo( () => agents.map((agent) => ({ - agent, - alias: preferredProfileAlias(agent), - route: resolveRunRequestOptions({ model: preferredProfileAlias(agent) }), + agent, alias: preferredProfileAlias(agent) ?? '', + route: resolveRunRequestOptions({ model: preferredProfileAlias(agent) ?? undefined }), })), - [ - agents, - defaultModel, - defaultProvider, - modelAliases, - modelMappingEnabled, - modelReasoningEffort, - providerFallbackEnabled, - resolveRunRequestOptions, - ], + [agents, resolveRunRequestOptions], ); - const availableRuntimes = agents.filter((agent) => agent.status === 'available').length; const runnerHealth = health?.checks?.runners; const runnerItems = runnerHealth?.items ?? []; const availableRunners = runnerHealth?.available ?? runnerItems.filter((item) => item.status === 'online').length; @@ -380,163 +174,123 @@ export default function SettingsPage({ onBack, onOpenAuth, initialSection = 'gen const runnerSummary = edgeOnline ? t('settings.runnerSummary', { available: availableRunners, total: totalRunners }) : t('settings.edgeOffline'); - const hubTargets = hubTargetsQuery.data?.items ?? []; - const hubOnlineTargets = hubTargets.filter(isHubTargetConnected).length; - const hubHealthyTargets = countTargetsByHealth(hubTargets, 'healthy'); - const hubDegradedTargets = countTargetsByHealth(hubTargets, 'degraded'); - const hubOfflineTargets = countTargetsByHealth(hubTargets, 'offline'); - const hubUnknownTargets = countTargetsByHealth(hubTargets, 'unknown'); - const hubRelayTargets = filterTargetsByType(hubTargets, ['hub_relay']); - const remoteHubTargets = filterTargetsByType(hubTargets, ['remote_ssh', 'tailscale']); - const cloudHubTargets = filterTargetsByType(hubTargets, ['cloud_edge']); - const hubTargetErrorMessage = hubTargetsQuery.error instanceof Error - ? hubTargetsQuery.error.message - : t('settings.targetHubErrorDesc'); - const hubTargetInventoryDetail = !hubInventoryEnabled - ? t('settings.targetHubSignedOutDesc') - : hubTargetsQuery.isLoading - ? t('settings.targetHubLoading') - : hubTargetsQuery.isError - ? t('settings.targetHubError') - : hubTargets.length > 0 - ? t('settings.targetCountSummary', { online: hubOnlineTargets, total: hubTargets.length }) - : t('settings.targetHubEmpty'); - const targetHealthBreakdown = t('settings.targetHealthBreakdown', { - healthy: hubHealthyTargets, - degraded: hubDegradedTargets, - offline: hubOfflineTargets, - unknown: hubUnknownTargets, - }); - const getHubTargetGroupStatus = (targets: ExecutionTarget[]) => { - if (!hubInventoryEnabled) return t('settings.targetHubSignInRequired'); - if (hubTargetsQuery.isLoading) return t('settings.targetHubLoading'); - if (hubTargetsQuery.isError) return t('settings.targetHubError'); - if (targets.length === 0) return t('settings.targetHubEmpty'); - return t(`settings.targetHealth.${getTargetGroupHealth(targets)}`); - }; - const getHubTargetGroupMetric = (targets: ExecutionTarget[]) => { - if (!hubInventoryEnabled) return t('settings.targetHubSignInRequired'); - if (hubTargetsQuery.isLoading) return t('settings.targetHubLoading'); - if (hubTargetsQuery.isError) return t('settings.targetHubError'); - return t('settings.targetCountSummary', { - online: targets.filter(isHubTargetConnected).length, - total: targets.length, - }); - }; const runs = runData?.items ?? []; const activeRuns = runs.filter(isActiveRun).length; const latestRun = getRecentRuns(runs, 1)[0]; - const recentRuns = getRecentRuns(runs, 5); - const activeHubTasks = bridgedTasks.filter(isActiveBridgeTask).length; - const recentBridgeTasks = getRecentTasks(bridgedTasks, 5); - const schedulerActiveItems = activeRuns + activeHubTasks; - const schedulerTotalItems = runs.length + bridgedTasks.length; - const schedulerTargetReadyCount = [ - edgeOnline, - hubOnlineTargets > 0, - remoteControlEnabled, - false, - ].filter(Boolean).length; - const schedulerLocalMetric = totalRunners > 0 ? runnerSummary : edgeOnline ? t('settings.edgeOnline') : t('settings.edgeOffline'); - const marketPublishReady = agents.filter((agent) => agent.status === 'available').length; - const marketCapabilityCount = countAgentCapabilities(agents); - const skillScriptCount = PROJECT_SKILLS.filter((skill) => skill.hasScripts).length; - const skillReferenceCount = PROJECT_SKILLS.filter((skill) => skill.hasReferences).length; - const skillReadyCount = PROJECT_SKILLS.filter((skill) => skill.status === 'ready').length; - const mcpCapableAgents = agents.filter((agent) => agent.capabilities.mcpIntegration).length; - const mcpPermissionHookAgents = agents.filter((agent) => agent.capabilities.permissionHooks).length; - const mcpSubAgentAgents = agents.filter((agent) => agent.capabilities.subAgentSpawn).length; - const hubSessionActive = hubAuthenticated || hubAuth.isAuthenticated; const accountName = hubAuth.user?.username ?? username ?? t('settings.signedIn'); - const tokenSource = hubAuth.tokenSource; - const tokenSourceLabel = - tokenSource === 'tokendance' - ? 'TokenDance ID' - : tokenSource === 'hub' - ? t('settings.hubLocalLogin') - : t('settings.notConfigured'); - const deviceId = readBrowserStorage('local', DEVICE_ID_KEY); - const tokenDanceOidcStatus = tokenSource === 'tokendance' ? t('settings.statusReady') : t('settings.statusInProgress'); - const handleSignOut = () => { - void hubAuth.logout(); - }; - const handleRefreshRuns = () => { - void refetchRuns(); - }; - const handleCancelRun = (runId: string) => { - void cancelRunMutation.mutateAsync(runId); - }; - const schedulerPolicyReadyCount = [ - modelMappingEnabled, - ccSwitchBridge, - autoReview, - remoteControlEnabled, - ].filter(Boolean).length; - - const navItems = useMemo( - () => [ - { id: 'general', label: t('settings.general'), icon: , group: 'workspace' }, - { id: 'appearance', label: t('settings.appearance'), icon: , group: 'workspace' }, - { id: 'configuration', label: t('settings.configuration'), icon: , group: 'workspace' }, - { id: 'personalization', label: t('settings.personalization'), icon: , group: 'workspace' }, - { id: 'permissions', label: t('settings.permissions'), icon: , group: 'workspace' }, - { id: 'agentProfiles', label: t('settings.agentProfiles'), icon: , group: 'workspace' }, - { id: 'executionTargets', label: t('settings.executionTargets'), icon: , group: 'workspace' }, - { id: 'tasks', label: t('settings.tasks'), icon: , group: 'workspace' }, - { id: 'onlineIm', label: t('settings.onlineIm'), icon: , group: 'workspace' }, - { id: 'groupChat', label: t('settings.groupChat'), icon: , group: 'workspace' }, - { id: 'agentScheduling', label: t('settings.agentScheduling'), icon: , group: 'workspace' }, - { id: 'agentMarket', label: t('settings.agentMarket'), icon: , group: 'workspace' }, - { id: 'keyboard', label: t('settings.keyboard'), icon: , group: 'workspace' }, - { id: 'mcp', label: t('settings.mcp'), icon: , group: 'automation' }, - { id: 'skills', label: t('settings.skills'), icon: , group: 'automation' }, - { id: 'hooks', label: t('settings.hooks'), icon: , group: 'automation' }, - { id: 'models', label: t('settings.models'), icon: , group: 'automation' }, - { id: 'modelMapping', label: t('settings.modelMapping'), icon: , group: 'automation' }, - { id: 'ccSwitch', label: t('settings.ccSwitch'), icon: , group: 'automation' }, - { id: 'connections', label: t('settings.connections'), icon: , group: 'automation' }, - { id: 'remoteControl', label: t('settings.remoteControl'), icon: , group: 'automation' }, - { id: 'git', label: t('settings.git'), icon: , group: 'automation' }, - { id: 'environment', label: t('settings.environment'), icon: , group: 'system' }, - { id: 'worktree', label: t('settings.worktree'), icon: , group: 'system' }, - { id: 'browser', label: t('settings.browser'), icon: , group: 'system' }, - { id: 'computerUse', label: t('settings.computerUse'), icon: , group: 'system' }, - { id: 'platforms', label: t('settings.platforms'), icon: , group: 'system' }, - { id: 'account', label: t('settings.account'), icon: , group: 'system' }, - { id: 'securityAudit', label: t('settings.securityAudit'), icon: , group: 'system' }, - { id: 'archived', label: t('settings.archived'), icon: , group: 'system' }, - ], - [t], - ); - - const filteredNavItems = useMemo(() => { - if (!navSearch.trim()) return navItems; - const query = navSearch.toLowerCase(); - return navItems.filter((item) => item.label.toLowerCase().includes(query)); - }, [navItems, navSearch]); - - const groupedNavItems = useMemo(() => { - const groups = ['workspace', 'automation', 'system'] as const; - return groups.map((group) => ({ - group, - items: filteredNavItems.filter((item) => item.group === group), - })); - }, [filteredNavItems]); + const tokenSource = hubAuth.tokenSource ?? 'none'; + const tokenSourceLabel = tokenSource === 'tokendance' ? 'TokenDance ID' : tokenSource === 'hub' ? t('settings.hubLocalLogin') : t('settings.notConfigured'); + const imSnapshot = hubIMSnapshotQuery.data; + const imSnapshotStatus = statusLabelFromQuery({ + signedIn: hubSessionActive, isLoading: hubIMSnapshotQuery.isLoading, + isFetching: hubIMSnapshotQuery.isFetching, isError: hubIMSnapshotQuery.isError, + isSuccess: hubIMSnapshotQuery.isSuccess, t, + }); + const desktopDeviceStatus = statusLabelFromDevice({ + signedIn: hubSessionActive, status: deviceRegistration.status, + registeredLabel: 'registered', idleLabel: 'deviceStatus', t, + }); + const deviceId = deviceRegistration.deviceId ?? readBrowserStorage('local', DEVICE_ID_KEY); + const handleSignOut = () => { void hubAuth.logout(); }; + + const navItems = useMemo(() => [ + { id: 'general', label: t('settings.general'), icon: , group: 'workspace' }, + { id: 'appearance', label: t('settings.appearance'), icon: , group: 'workspace' }, + { id: 'configuration', label: t('settings.configuration'), icon: , group: 'workspace' }, + { id: 'personalization', label: t('settings.personalization'), icon: , group: 'workspace' }, + { id: 'permissions', label: t('settings.permissions'), icon: , group: 'workspace' }, + { id: 'agentProfiles', label: t('settings.agentProfiles'), icon: , group: 'workspace' }, + { id: 'executionTargets', label: t('settings.executionTargets'), icon: , group: 'workspace' }, + { id: 'tasks', label: t('settings.tasks'), icon: , group: 'workspace' }, + { id: 'onlineIm', label: t('settings.onlineIm'), icon: , group: 'workspace' }, + { id: 'groupChat', label: t('settings.groupChat'), icon: , group: 'workspace' }, + { id: 'agentScheduling', label: t('settings.agentScheduling'), icon: , group: 'workspace' }, + { id: 'agentMarket', label: t('settings.agentMarket'), icon: , group: 'workspace' }, + { id: 'keyboard', label: t('settings.keyboard'), icon: , group: 'workspace' }, + { id: 'mcp', label: t('settings.mcp'), icon: , group: 'automation' }, + { id: 'skills', label: t('settings.skills'), icon: , group: 'automation' }, + { id: 'hooks', label: t('settings.hooks'), icon: , group: 'automation' }, + { id: 'models', label: t('settings.models'), icon: , group: 'automation' }, + { id: 'modelMapping', label: t('settings.modelMapping'), icon: , group: 'automation' }, + { id: 'ccSwitch', label: t('settings.ccSwitch'), icon: , group: 'automation' }, + { id: 'connections', label: t('settings.connections'), icon: , group: 'automation' }, + { id: 'remoteControl', label: t('settings.remoteControl'), icon: , group: 'automation' }, + { id: 'git', label: t('settings.git'), icon: , group: 'automation' }, + { id: 'environment', label: t('settings.environment'), icon: , group: 'system' }, + { id: 'worktree', label: t('settings.worktree'), icon: , group: 'system' }, + { id: 'browser', label: t('settings.browser'), icon: , group: 'system' }, + { id: 'computerUse', label: t('settings.computerUse'), icon: , group: 'system' }, + { id: 'platforms', label: t('settings.platforms'), icon: , group: 'system' }, + { id: 'account', label: t('settings.account'), icon: , group: 'system' }, + { id: 'securityAudit', label: t('settings.securityAudit'), icon: , group: 'system' }, + { id: 'archived', label: t('settings.archived'), icon: , group: 'system' }, + ], [t]); const activeLabel = navItems.find((item) => item.id === active)?.label ?? t('settings.title'); - const shortcuts: ShortcutRow[] = [ - { keys: ['Enter'], action: t('shortcut.send') }, - { keys: ['Shift', 'Enter'], action: t('shortcut.newline') }, - { keys: ['Ctrl', 'K'], action: t('shortcut.search') }, - { keys: ['⌘/Ctrl', 'B'], action: t('shortcut.toggleSidebar') }, - { keys: ['⌘/Ctrl', 'J'], action: t('shortcut.toggleRunPanel') }, - { keys: ['Esc'], action: t('shortcut.close') }, - { keys: ['?'], action: t('shortcut.help') }, - ]; - const setBooleanSetting = (key: string, setter: (value: boolean) => void) => (value: boolean) => { - setter(value); - writeStoredValue(key, value); + const renderSection = (active: SectionId) => { + switch (active) { + case 'general': + return ; + case 'appearance': + return ; + case 'configuration': + return ; + case 'personalization': + return ; + case 'permissions': + return ; + case 'agentProfiles': + return ; + case 'executionTargets': + return ; + case 'tasks': + return ; + case 'onlineIm': + return void hubIMSnapshotQuery.refetch()} deviceRegistrationStatus={deviceRegistration.status} onOpenAuth={onOpenAuth} />; + case 'groupChat': + return ; + case 'agentScheduling': + return ; + case 'agentMarket': + return []) ?? []} isLoading={customAgentsQuery.isLoading} isFetching={customAgentsQuery.isFetching} isError={customAgentsQuery.isError} isSuccess={customAgentsQuery.isSuccess} refetch={() => void customAgentsQuery.refetch()} onOpenAuth={onOpenAuth} />; + case 'keyboard': + return ; + case 'mcp': + return ; + case 'skills': + return ; + case 'hooks': + return ; + case 'models': + return ; + case 'modelMapping': + return ; + case 'ccSwitch': + return ; + case 'connections': + return ; + case 'remoteControl': + return ; + case 'git': + return ; + case 'environment': + return ; + case 'worktree': + return ; + case 'browser': + return ; + case 'computerUse': + return ; + case 'platforms': + return ; + case 'account': + return ; + case 'securityAudit': + return ; + case 'archived': + return ; + } }; return ( @@ -546,1853 +300,39 @@ export default function SettingsPage({ onBack, onOpenAuth, initialSection = 'gen {t('settings.back')} - -
- - setNavSearch(e.target.value)} - /> -
- -
{hubSessionActive ? ( - + ) : ( - + )}
-
{t('settings.title')}

{activeLabel}

- - {active === 'general' && ( - <> - -
- } - title={t('settings.modeCoding')} - description={t('settings.modeCodingDesc')} - onClick={() => { - setDetailLevel('detailed'); - writeStoredValue('detailLevel', 'detailed'); - }} - /> - } - title={t('settings.modeDaily')} - description={t('settings.modeDailyDesc')} - onClick={() => { - setDetailLevel('balanced'); - writeStoredValue('detailLevel', 'balanced'); - }} - /> -
-
- - - - } - /> - { - setDetailLevel(value as SelectValue); - writeStoredValue('detailLevel', value); - }} - options={[ - ['detailed', t('settings.detailLevel.detailed')], - ['balanced', t('settings.detailLevel.balanced')], - ]} - /> - } - /> - - - )} - - {active === 'appearance' && ( - <> - -
- {(['dark', 'light', 'system'] as const).map((mode) => ( - - ))} -
-
- - - } - /> - - - )} - - {active === 'configuration' && ( - - - - { - setApprovalMode(value as SelectValue); - writeStoredValue('approvalMode', value); - }} - options={[ - ['ask', t('settings.approvalMode.ask')], - ['auto', t('settings.approvalMode.auto')], - ['manual', t('settings.approvalMode.manual')], - ]} - /> - } - /> - - )} - - {active === 'personalization' && ( - - - - - - )} - - {active === 'permissions' && ( - - } - /> - } - /> - - - )} - - {active === 'agentProfiles' && ( - -
- } - label={t('settings.profileAvailable')} - value={`${availableRuntimes}/${agents.length}`} - detail={edgeOnline ? t('settings.runtimeInventoryDesc') : t('settings.edgeOffline')} - /> - } - label={t('settings.profileRuntimeCoverage')} - value={runnerSummary} - detail={t('settings.profileRuntimeCoverageDesc')} - /> -
-
-
- {t('settings.runtimeInventory')} - {t('settings.runtimeInventoryDesc')} -
- {agents.length > 0 ? ( -
- {agents.map((agent) => )} -
- ) : ( - - )} -
-
-
- {t('settings.profileComposition')} - {t('settings.profileCompositionDesc')} -
- {localAgentProfiles.length > 0 ? ( -
- {localAgentProfiles.map((profile) => ( - - ))} -
- ) : ( - - )} -
- 0 ? t('settings.statusReady') : t('settings.notConfigured')} - /> - - - -
-
- - -
- )} - - {active === 'executionTargets' && ( - -
- } - label={t('settings.targetLocalInventory')} - value={`${availableRunners}/${totalRunners}`} - detail={edgeOnline ? runnerSummary : t('settings.edgeOffline')} - /> - } - label={t('settings.targetHubInventory')} - value={hubTargetsQuery.isLoading ? t('settings.loading') : `${hubOnlineTargets}/${hubTargets.length}`} - detail={hubTargetInventoryDetail} - /> - } - label={t('settings.targetHubHealth')} - value={hubTargets.length > 0 ? `${hubHealthyTargets}/${hubTargets.length}` : t('settings.noData')} - detail={targetHealthBreakdown} - /> -
-
- } - title={t('settings.targetLocalEdge')} - description={t('settings.targetLocalEdgeDesc')} - status={edgeOnline ? health?.status ?? 'ok' : t('settings.offline')} - metric={runnerSummary} - connected={edgeOnline && availableRunners > 0} - /> - } - title={t('settings.targetHubRelay')} - description={t('settings.targetHubRelayDesc')} - status={getHubTargetGroupStatus(hubRelayTargets)} - metric={getHubTargetGroupMetric(hubRelayTargets)} - connected={hubRelayTargets.some(isHubTargetConnected)} - /> - } - title={t('settings.targetSsh')} - description={t('settings.targetSshDesc')} - status={getHubTargetGroupStatus(remoteHubTargets)} - metric={getHubTargetGroupMetric(remoteHubTargets)} - connected={remoteHubTargets.some(isHubTargetConnected)} - /> - } - title={t('settings.targetCloudEdge')} - description={t('settings.targetCloudEdgeDesc')} - status={getHubTargetGroupStatus(cloudHubTargets)} - metric={getHubTargetGroupMetric(cloudHubTargets)} - connected={cloudHubTargets.some(isHubTargetConnected)} - /> -
- {runnerItems.length > 0 ? ( -
- {runnerItems.map((runner) => )} -
- ) : ( - - )} -
-
- {t('settings.targetHubInventory')} - {t('settings.targetHubInventoryDesc')} -
- {!hubInventoryEnabled ? ( - - ) : hubTargetsQuery.isLoading ? ( - - ) : hubTargetsQuery.isError ? ( - - ) : hubTargets.length > 0 ? ( -
- {hubTargets.map((target) => ( - pingHubTargetMutation.mutate(targetId)} - /> - ))} -
- ) : ( - - )} -
-
- )} - - {active === 'tasks' && ( - -
- } - label={t('settings.taskLocalRuns')} - value={`${activeRuns}/${runs.length}`} - detail={runsLoading ? t('settings.loading') : t('settings.taskLocalRunsDesc')} - /> - } - label={t('settings.taskHubBridge')} - value={`${activeHubTasks}/${bridgedTasks.length}`} - detail={hubAuthenticated ? t('settings.taskHubBridgeDesc') : t('settings.taskHubBridgeSignedOut')} - /> - } - label={t('settings.taskLastRun')} - value={latestRun ? t(`run.status.${latestRun.status}`, { defaultValue: latestRun.status }) : t('settings.noData')} - detail={latestRun ? formatTimestamp(latestRun.finishedAt ?? latestRun.startedAt ?? latestRun.createdAt) : t('settings.taskLastRunDesc')} - /> - } - label={t('settings.taskApprovalQueue')} - value={t('settings.statusPlanned')} - detail={t('settings.taskApprovalQueueDesc')} - /> -
- } - /> - - -
-
-
-
- {t('settings.taskRecentRuns')} - {runsFetching ? t('settings.taskRefreshingRuns') : t('settings.taskRecentRunsDesc')} -
-
- - {runsError ? t('settings.edgeOffline') : t('settings.taskRunLive')} - - -
-
-
- {recentRuns.length > 0 ? ( -
- {recentRuns.map((run) => ( - - ))} -
- ) : ( - - )} -
-
-
- {t('settings.taskBridgeQueue')} - {t('settings.taskBridgeQueueDesc')} -
- {recentBridgeTasks.length > 0 ? ( -
- {recentBridgeTasks.map((task) => ( - - ))} -
- ) : ( - - )} -
-
- )} - - {active === 'onlineIm' && ( - -
- - - -
-
- )} - - {active === 'groupChat' && ( - - } - /> - - - - - )} - - {active === 'agentScheduling' && ( - -
- } - label={t('settings.schedulerQueueLive')} - value={`${schedulerActiveItems}/${schedulerTotalItems}`} - detail={runsLoading ? t('settings.loading') : t('settings.schedulerQueueLiveDesc')} - /> - } - label={t('settings.schedulerProfiles')} - value={`${availableRuntimes}/${agents.length}`} - detail={edgeOnline ? t('settings.schedulerProfilesDesc') : t('settings.edgeOffline')} - /> - } - label={t('settings.schedulerTargets')} - value={`${schedulerTargetReadyCount}/4`} - detail={t('settings.schedulerTargetsDesc')} - /> - } - label={t('settings.schedulerPolicyReady')} - value={`${schedulerPolicyReadyCount}/4`} - detail={t('settings.schedulerPolicyReadyDesc')} - /> -
- } - /> -
-
- {t('settings.schedulerLiveQueue')} - {t('settings.schedulerLiveQueueDesc')} -
- {recentRuns.length > 0 || recentBridgeTasks.length > 0 ? ( -
- {recentRuns.slice(0, 3).map((run) => ( - - ))} - {recentBridgeTasks.slice(0, 3).map((task) => ( - - ))} -
- ) : ( - - )} -
-
-
- {t('settings.schedulerTargets')} - {t('settings.schedulerTargetsDesc')} -
-
- } - title={t('settings.schedulerRouteLocal')} - description={t('settings.schedulerRouteLocalDesc')} - status={edgeOnline ? t('settings.enabled') : t('settings.offline')} - metric={schedulerLocalMetric} - connected={edgeOnline} - /> - } - title={t('settings.schedulerRouteHub')} - description={t('settings.schedulerRouteHubDesc')} - status={hubAuthenticated ? t('settings.enabled') : t('settings.notConfigured')} - metric={hubAuthenticated ? t('settings.targetHubSignedIn') : t('settings.targetHubSignInRequired')} - connected={hubAuthenticated} - /> - } - title={t('settings.schedulerRouteRemote')} - description={t('settings.schedulerRouteRemoteDesc')} - status={remoteControlEnabled ? t('settings.statusInProgress') : t('settings.statusPlanned')} - metric="SSH / Tailscale" - connected={remoteControlEnabled} - /> - } - title={t('settings.schedulerRouteCloud')} - description={t('settings.schedulerRouteCloudDesc')} - status={t('settings.statusPlanned')} - metric="Cloud Edge" - /> -
-
-
-
- {t('settings.schedulerPolicy')} - {t('settings.schedulerPolicyDesc')} -
-
- - - - -
-
- -
- )} - - {active === 'agentMarket' && ( - -
- } - label={t('settings.marketLocalProfiles')} - value={`${agents.length}`} - detail={edgeOnline ? t('settings.marketLocalProfilesDesc') : t('settings.edgeOffline')} - /> - } - label={t('settings.marketPublishReady')} - value={`${marketPublishReady}/${agents.length}`} - detail={t('settings.marketPublishReadyDesc')} - /> - } - label={t('settings.marketCapabilities')} - value={`${marketCapabilityCount}`} - detail={t('settings.marketCapabilitiesDesc')} - /> - } - label={t('settings.marketHubSync')} - value={hubAuthenticated ? t('settings.enabled') : t('settings.notConfigured')} - detail={hubAuthenticated ? t('settings.marketHubSyncDesc') : t('settings.marketHubSyncSignedOut')} - /> -
-
-
- {t('settings.marketInstalledProfiles')} - {t('settings.marketInstalledProfilesDesc')} -
- {agents.length > 0 ? ( -
- {agents.map((agent) => ( - - ))} -
- ) : ( - - )} -
-
-
- {t('settings.marketReleaseReadiness')} - {t('settings.marketReleaseReadinessDesc')} -
-
- 0 ? t('settings.statusInProgress') : t('settings.statusPlanned')} - /> - 0 ? t('settings.statusReady') : t('settings.statusPlanned')} - /> - - -
-
- -
- )} - - {active === 'keyboard' && ( - -
- {shortcuts.map((shortcut) => ( -
- {shortcut.action} -
- {shortcut.keys.map((key) => ( - {key} - ))} -
-
- ))} -
-
- )} - - {active === 'mcp' && ( - -
- } - label={t('settings.mcpRuntimeSupport')} - value={`${mcpCapableAgents}/${agents.length}`} - detail={edgeOnline ? t('settings.mcpRuntimeSupportDesc') : t('settings.edgeOffline')} - /> - } - label={t('settings.mcpPermissionHooks')} - value={`${mcpPermissionHookAgents}`} - detail={t('settings.mcpPermissionHooksDesc')} - /> - } - label={t('settings.mcpSubAgentSpawn')} - value={`${mcpSubAgentAgents}`} - detail={t('settings.mcpSubAgentSpawnDesc')} - /> - } - label={t('settings.mcpHubSync')} - value={hubAuthenticated && enableMcp ? t('settings.enabled') : t('settings.notConfigured')} - detail={hubAuthenticated ? t('settings.mcpHubSyncDesc') : t('settings.mcpHubSyncSignedOut')} - /> -
- } - /> -
-
- {t('settings.mcpRuntimeMatrix')} - {t('settings.mcpRuntimeMatrixDesc')} -
- {agents.length > 0 ? ( -
- {agents.map((agent) => ( - - ))} -
- ) : ( - - )} -
-
-
- {t('settings.mcpTemplates')} - {t('settings.mcpTemplatesDesc')} -
-
- - - - -
-
- -
- )} - - {active === 'skills' && ( - -
- } - label={t('settings.skillProjectRegistry')} - value={`${PROJECT_SKILLS.length}`} - detail={t('settings.skillProjectRegistryDesc')} - /> - } - label={t('settings.skillReviewReady')} - value={`${skillReadyCount}/${PROJECT_SKILLS.length}`} - detail={t('settings.skillReviewReadyDesc')} - /> - } - label={t('settings.skillScripts')} - value={`${skillScriptCount}`} - detail={t('settings.skillScriptsDesc')} - /> - } - label={t('settings.skillHubSync')} - value={hubAuthenticated && skillSync ? t('settings.enabled') : t('settings.notConfigured')} - detail={hubAuthenticated ? t('settings.skillHubSyncDesc') : t('settings.skillHubSyncSignedOut')} - /> -
- } - /> -
-
- {t('settings.skillInstalled')} - {t('settings.skillInstalledDesc')} -
-
- {PROJECT_SKILLS.map((skill) => ( - - ))} -
-
-
-
- {t('settings.skillGovernance')} - {t('settings.skillGovernanceDesc')} -
-
- - - - -
-
- -
- )} - - {active === 'hooks' && ( - - } - /> - - - - )} - - {active === 'models' && ( - - [value, label])} - onChange={setDefaultModel} - /> - } - /> - [value, label])} - onChange={setDefaultProvider} - /> - } - /> - [value, label])} - onChange={(value) => setModelReasoningEffort(value as ReasoningEffortPreference)} - /> - } - /> - } - /> - - - )} - - {active === 'modelMapping' && ( - - } - /> -
-
- {t('settings.modelAlias')} - {t('settings.modelAliasDesc')} -
-
- {modelAliases.map((item) => ( - toggleModelAlias(item.alias)} - onModelChange={(model) => updateModelAlias(item.alias, { model })} - onProviderChange={(provider) => updateModelAlias(item.alias, { provider })} - onReasoningChange={(reasoningEffort) => updateModelAlias(item.alias, { reasoningEffort })} - /> - ))} -
-
- -
- )} - - {active === 'ccSwitch' && ( - - } - /> -
-
- {t('settings.ccSwitchProviders')} - {t('settings.ccSwitchProvidersDesc')} -
-
- {ccSwitchProviders.map((provider) => ( - updateCcSwitchProvider(provider.id, { health })} - onNotesChange={(notes) => updateCcSwitchProvider(provider.id, { notes })} - /> - ))} -
-
- -
- )} - - {active === 'connections' && ( - - - - - - )} - - {active === 'remoteControl' && ( - - } - /> - - - - )} - - {active === 'git' && ( - - - } - /> - - - - )} - - {active === 'environment' && ( - - - - - - )} - - {active === 'worktree' && ( - - - - } - /> - - - )} - - {active === 'browser' && ( - - - } - /> - - - )} - - {active === 'computerUse' && ( - - - } - /> - - - )} - - {active === 'platforms' && ( - - } - /> -
- - - - -
-
- )} - - {active === 'account' && ( - -
- -
- {hubSessionActive ? accountName : t('settings.notSignedIn')} - {hubSessionActive ? t('settings.accountConnected') : t('settings.accountDisconnected')} -
- {hubSessionActive ? ( - - ) : ( - - )} -
-
- } - label={t('settings.hubSession')} - value={hubSessionActive ? t('settings.enabled') : t('settings.notConfigured')} - detail={hubSessionActive ? t('settings.hubSessionDesc') : t('settings.hubSessionSignedOutDesc')} - /> - } - label="TokenDance ID" - value={tokenSource === 'tokendance' ? t('settings.enabled') : t('settings.statusInProgress')} - detail={tokenSource === 'tokendance' ? t('settings.tokenDanceSessionDesc') : t('settings.tokenDanceOidcPendingDesc')} - /> - } - label={t('settings.desktopDevice')} - value={deviceId ? shortId(deviceId) : t('settings.notConfigured')} - detail={deviceId ? t('settings.desktopDeviceDesc') : t('settings.desktopDeviceMissingDesc')} - /> - } - label={t('settings.syncScope')} - value={hubSessionActive ? 'Hub' : t('settings.notConfigured')} - detail={t('settings.syncScopeDesc')} - /> -
-
-
- {t('settings.identityBoundary')} - {t('settings.identityBoundaryDesc')} -
-
- - - - -
-
- - - -
- )} - - {active === 'securityAudit' && ( - - } - /> - - - - - )} - - {active === 'archived' && ( - - - - )} + {renderSection(active)}
); } - -function useStoredBooleanState(key: string, fallback: boolean) { - return useState(() => readStoredBoolean(key, fallback)); -} - -function useStoredValueState(key: string, fallback: T) { - return useState(() => readStoredValue(key, fallback)); -} - -function isActiveRun(run: RunInfo) { - return ['queued', 'started', 'running', 'cancelling'].includes(run.status); -} - -function isActiveBridgeTask(task: AgentTask) { - return task.status === 'queued' || task.status === 'running'; -} - -function getRecentRuns(runs: RunInfo[], limit: number) { - return [...runs] - .sort((a, b) => timestampOf(b.finishedAt ?? b.startedAt ?? b.createdAt) - timestampOf(a.finishedAt ?? a.startedAt ?? a.createdAt)) - .slice(0, limit); -} - -function getRecentTasks(tasks: AgentTask[], limit: number) { - return [...tasks].sort((a, b) => timestampOf(b.createdAt) - timestampOf(a.createdAt)).slice(0, limit); -} - -function countAgentCapabilities(agents: AgentInfo[]) { - const names = new Set(); - for (const agent of agents) { - for (const [name, enabled] of Object.entries(agent.capabilities)) { - if (enabled) names.add(name); - } - } - return names.size; -} - -function timestampOf(value?: string) { - if (!value) return 0; - const parsed = Date.parse(value); - return Number.isNaN(parsed) ? 0 : parsed; -} - -function formatTimestamp(value?: string) { - if (!value) return '--'; - const parsed = new Date(value); - if (Number.isNaN(parsed.getTime())) return value; - return parsed.toLocaleString(undefined, { - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - }); -} - -function shortId(value?: string) { - if (!value) return '--'; - return value.length > 14 ? `${value.slice(0, 8)}...${value.slice(-4)}` : value; -} - -function Panel({ title, description, children }: { title: string; description?: string; children: ReactNode }) { - return ( -
-
-

{title}

- {description ?

{description}

: null} -
-
{children}
-
- ); -} - -function TaskRunRow({ - run, - onCancel, - cancelling = false, -}: { - run: RunInfo; - onCancel?: (runId: string) => void; - cancelling?: boolean; -}) { - const { t } = useTranslation(); - const timestamp = run.finishedAt ?? run.startedAt ?? run.createdAt; - return ( -
-
- -
-
- {shortId(run.runId)} - {run.projectId} / {run.threadId} -
- {formatTimestamp(timestamp)} -
-
- - {t(`run.status.${run.status}`, { defaultValue: run.status })} - - {onCancel ? ( - - ) : null} -
- ); -} - -function HubTaskRow({ task }: { task: AgentTask }) { - const { t } = useTranslation(); - return ( -
-
- -
-
- {shortId(task.taskId)} - {task.prompt} -
- {task.agentId} - {task.runId ? shortId(task.runId) : t('settings.taskUnbound')} -
-
- - {t(`settings.taskStatus.${task.status}`, { defaultValue: task.status })} - -
- ); -} - -function ModeCard({ - active, - icon, - title, - description, - onClick, -}: { - active: boolean; - icon: ReactNode; - title: string; - description: string; - onClick: () => void; -}) { - return ( - - ); -} - -function CapabilityCard({ title, description, status }: { title: string; description: string; status: string }) { - return ( -
- {title} - {description} - {status} -
- ); -} - -function SummaryCard({ icon, label, value, detail }: { icon: ReactNode; label: string; value: string; detail: string }) { - return ( -
-
{icon}
-
- {label} - {value} - {detail} -
-
- ); -} - -function AliasMappingRow({ - alias, - model, - provider, - reasoningEffort, - enabled, - onToggle, - onModelChange, - onProviderChange, - onReasoningChange, -}: { - alias: string; - model: string; - provider: string; - reasoningEffort: ReasoningEffortPreference; - enabled: boolean; - onToggle: () => void; - onModelChange: (model: string) => void; - onProviderChange: (provider: string) => void; - onReasoningChange: (reasoningEffort: ReasoningEffortPreference) => void; -}) { - const { t } = useTranslation(); - return ( -
-
-
- {alias} - {t('settings.modelAliasRoute', { model, provider })} -
- -
-
- - - -
-
- ); -} - -function ProviderHealthRow({ - id, - name, - health, - modelCount, - notes, - onHealthChange, - onNotesChange, -}: { - id: string; - name: string; - health: ProviderHealth; - modelCount: number; - notes: string; - onHealthChange: (health: ProviderHealth) => void; - onNotesChange: (notes: string) => void; -}) { - const { t } = useTranslation(); - return ( -
-
-
- -
-
- {name} - {id} -
- {t('settings.ccSwitchModelCount', { count: modelCount })} -
-
- - {t(`settings.providerHealth.${health}`)} - -
-
- -