From 0c5d92727b2836b55dda5e00b24d8dec4026db4b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 19 Nov 2025 15:50:26 +0000 Subject: [PATCH 1/5] chore(deps): update dependency eslint-plugin-react-hooks to v7 --- package.json | 2 +- pnpm-lock.yaml | 31 ++++++++++++++++++++++--------- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index e828894e9..7f5e7d5db 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "dotenv": "^17.0.0", "electron": "39.0.0", "eslint": "^9.25.0", - "eslint-plugin-react-hooks": "^6.0.0", + "eslint-plugin-react-hooks": "^7.0.0", "eslint-plugin-react-refresh": "^0.4.19", "globals": "^16.0.0", "husky": "^9.1.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0bb2918e0..002b7aaa7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -292,8 +292,8 @@ importers: specifier: ^9.25.0 version: 9.39.1(jiti@2.6.1) eslint-plugin-react-hooks: - specifier: ^6.0.0 - version: 6.1.1(eslint@9.39.1(jiti@2.6.1)) + specifier: ^7.0.0 + version: 7.0.1(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-react-refresh: specifier: ^0.4.19 version: 0.4.24(eslint@9.39.1(jiti@2.6.1)) @@ -4516,8 +4516,8 @@ packages: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} - eslint-plugin-react-hooks@6.1.1: - resolution: {integrity: sha512-St9EKZzOAQF704nt2oJvAKZHjhrpg25ClQoaAlHmPZuajFldVLqRDW4VBNAS01NzeiQF0m0qhG1ZA807K6aVaQ==} + eslint-plugin-react-hooks@7.0.1: + resolution: {integrity: sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==} engines: {node: '>=18'} peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 @@ -5034,6 +5034,12 @@ packages: headers-polyfill@4.0.3: resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==} + hermes-estree@0.25.1: + resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} + + hermes-parser@0.25.1: + resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} @@ -8079,7 +8085,7 @@ snapshots: '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4) '@babel/helpers': 7.28.4 - '@babel/parser': 7.28.4 + '@babel/parser': 7.28.5 '@babel/template': 7.27.2 '@babel/traverse': 7.28.5 '@babel/types': 7.28.5 @@ -8114,7 +8120,7 @@ snapshots: '@babel/generator@7.28.3': dependencies: - '@babel/parser': 7.28.4 + '@babel/parser': 7.28.5 '@babel/types': 7.28.5 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 @@ -12742,11 +12748,12 @@ snapshots: escape-string-regexp@5.0.0: {} - eslint-plugin-react-hooks@6.1.1(eslint@9.39.1(jiti@2.6.1)): + eslint-plugin-react-hooks@7.0.1(eslint@9.39.1(jiti@2.6.1)): dependencies: - '@babel/core': 7.28.4 - '@babel/parser': 7.28.4 + '@babel/core': 7.28.5 + '@babel/parser': 7.28.5 eslint: 9.39.1(jiti@2.6.1) + hermes-parser: 0.25.1 zod: 4.1.12 zod-validation-error: 4.0.2(zod@4.1.12) transitivePeerDependencies: @@ -13447,6 +13454,12 @@ snapshots: headers-polyfill@4.0.3: {} + hermes-estree@0.25.1: {} + + hermes-parser@0.25.1: + dependencies: + hermes-estree: 0.25.1 + hosted-git-info@2.8.9: {} html-encoding-sniffer@4.0.0: From a7883a5f8aefa35cefb193b385274f9954389b70 Mon Sep 17 00:00:00 2001 From: Giuseppe Scuglia Date: Mon, 24 Nov 2025 17:17:05 +0100 Subject: [PATCH 2/5] fix: react-hooks linter --- .../components/settings/tabs/general-tab.tsx | 13 +++---- renderer/src/common/components/ui/tooltip.tsx | 5 ++- .../common/hooks/use-check-server-status.tsx | 2 +- .../hooks/use-cleanup-meta-optimizer.ts | 17 ++++++---- .../hooks/use-experimental-features.tsx | 2 +- .../features/chat/components/error-alert.tsx | 16 +++------ .../chat/components/mcp-tools-modal.tsx | 23 +++++++------ .../features/chat/hooks/use-chat-settings.ts | 12 ++----- .../components/card-mcp-server/index.tsx | 34 +++++++++---------- .../components/customize-tools-table.tsx | 2 ++ .../local-mcp/dialog-form-local-mcp.tsx | 6 ++-- .../remote-mcp/dialog-form-remote-mcp.tsx | 8 ++--- .../hooks/use-customize-tools-table.ts | 15 ++++---- .../mcp-servers/sub-pages/logs-page/index.tsx | 25 ++++++++------ .../components/group-selector-form.tsx | 7 ++-- .../hooks/use-mcp-optimizer-groups.ts | 8 ++--- .../dialog-form-remote-registry-mcp.tsx | 8 ++--- .../multi-server-install-wizard.tsx | 28 ++++++++++----- 18 files changed, 121 insertions(+), 110 deletions(-) diff --git a/renderer/src/common/components/settings/tabs/general-tab.tsx b/renderer/src/common/components/settings/tabs/general-tab.tsx index 8055be135..721d5c2ae 100644 --- a/renderer/src/common/components/settings/tabs/general-tab.tsx +++ b/renderer/src/common/components/settings/tabs/general-tab.tsx @@ -12,7 +12,7 @@ import { } from '@/common/hooks/use-auto-launch' import { useTheme } from '@/common/hooks/use-theme' import { useMutation, useQuery } from '@tanstack/react-query' -import { useState, useEffect } from 'react' +import { useState } from 'react' import { Sun, Moon, Monitor } from 'lucide-react' import log from 'electron-log/renderer' import { trackEvent } from '@/common/lib/analytics' @@ -138,14 +138,9 @@ function AutoLaunchField() { } function QuitConfirmationField() { - const [skipQuitConfirmation, setSkipQuitConfirmation] = - useState(false) - - useEffect(() => { - const quitConfirmationDisabled = - localStorage.getItem(CONFIRM_QUIT_STORAGE_KEY) === 'true' - setSkipQuitConfirmation(quitConfirmationDisabled) - }, []) + const [skipQuitConfirmation, setSkipQuitConfirmation] = useState( + () => localStorage.getItem(CONFIRM_QUIT_STORAGE_KEY) === 'true' + ) const handleQuitConfirmationToggle = () => { const newValue = !skipQuitConfirmation diff --git a/renderer/src/common/components/ui/tooltip.tsx b/renderer/src/common/components/ui/tooltip.tsx index 7347882c0..369169b3d 100644 --- a/renderer/src/common/components/ui/tooltip.tsx +++ b/renderer/src/common/components/ui/tooltip.tsx @@ -71,7 +71,10 @@ const TooltipTrigger = React.forwardRef< const setRefs = React.useCallback( (node: HTMLButtonElement | null) => { // internal: keep track for truncation measurement - if (ctx) ctx.triggerRef.current = node + if (ctx) { + const triggerRef = ctx.triggerRef + triggerRef.current = node + } // forward to consumer if (typeof forwardedRef === 'function') { forwardedRef(node) diff --git a/renderer/src/common/hooks/use-check-server-status.tsx b/renderer/src/common/hooks/use-check-server-status.tsx index 5860884a5..340002d85 100644 --- a/renderer/src/common/hooks/use-check-server-status.tsx +++ b/renderer/src/common/hooks/use-check-server-status.tsx @@ -16,7 +16,7 @@ import { META_MCP_SERVER_NAME } from '../lib/constants' * Returns a function that polls server status and invalidates queries when ready. */ export function useCheckServerStatus() { - const toastIdRef = useRef(new Date(Date.now()).toISOString()) + const toastIdRef = useRef(new Date().getTime()) const queryClient = useQueryClient() diff --git a/renderer/src/common/hooks/use-cleanup-meta-optimizer.ts b/renderer/src/common/hooks/use-cleanup-meta-optimizer.ts index 3f7dfe8d3..6b7532018 100644 --- a/renderer/src/common/hooks/use-cleanup-meta-optimizer.ts +++ b/renderer/src/common/hooks/use-cleanup-meta-optimizer.ts @@ -95,13 +95,16 @@ export function useCleanupMetaOptimizer() { enabled: isMetaOptimizerEnabled, }) - const removeClientsFromGroup = async (clients: string[]) => { - for (const clientType of clients) { - await unregisterClients({ - clientType, - }) - } - } + const removeClientsFromGroup = useCallback( + async (clients: string[]) => { + for (const clientType of clients) { + await unregisterClients({ + clientType, + }) + } + }, + [unregisterClients] + ) const mcpOptimizerGroup = groupsList?.groups?.find( (g) => g.name === MCP_OPTIMIZER_GROUP_NAME diff --git a/renderer/src/common/hooks/use-experimental-features.tsx b/renderer/src/common/hooks/use-experimental-features.tsx index 8f0b26a17..618a43ad7 100644 --- a/renderer/src/common/hooks/use-experimental-features.tsx +++ b/renderer/src/common/hooks/use-experimental-features.tsx @@ -55,7 +55,7 @@ function formatFeatureFlagDescription(key: string): React.ReactNode { } export function useExperimentalFeatures() { - const toastIdRef = useRef(new Date(Date.now()).toISOString()) + const toastIdRef = useRef(new Date().getTime()) const { handleCreateOptimizerGroup, isCreatingOptimizerGroup } = useCreateOptimizerGroup() const { cleanupMetaOptimizer } = useCleanupMetaOptimizer() diff --git a/renderer/src/features/chat/components/error-alert.tsx b/renderer/src/features/chat/components/error-alert.tsx index d7bbfd78e..4c74902c8 100644 --- a/renderer/src/features/chat/components/error-alert.tsx +++ b/renderer/src/features/chat/components/error-alert.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react' +import { useState } from 'react' import { X, AlertTriangle } from 'lucide-react' import { Alert, @@ -14,16 +14,10 @@ interface ErrorAlertProps { } export function ErrorAlert({ error, className = '' }: ErrorAlertProps) { - const [isDismissed, setIsDismissed] = useState(false) + const [dismissedError, setDismissedError] = useState(null) - // Reset dismissed state when a new error occurs - useEffect(() => { - if (error) { - setIsDismissed(false) - } - }, [error]) - - if (!error || isDismissed) { + // Show alert only if there's an error and it hasn't been dismissed + if (!error || error === dismissedError) { return null } @@ -50,7 +44,7 @@ export function ErrorAlert({ error, className = '' }: ErrorAlertProps) { variant="ghost" size="sm" className="hover:bg-destructive/20 absolute top-2 right-2 h-6 w-6 p-0" - onClick={() => setIsDismissed(true)} + onClick={() => setDismissedError(error)} > Dismiss error diff --git a/renderer/src/features/chat/components/mcp-tools-modal.tsx b/renderer/src/features/chat/components/mcp-tools-modal.tsx index c60b784e2..f0d235c52 100644 --- a/renderer/src/features/chat/components/mcp-tools-modal.tsx +++ b/renderer/src/features/chat/components/mcp-tools-modal.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react' +import { useState } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { Dialog, @@ -48,6 +48,9 @@ export function McpToolsModal({ }: McpToolsModalProps) { const [searchQuery, setSearchQuery] = useState('') const [localEnabledTools, setLocalEnabledTools] = useState([]) + const [lastInitializedServer, setLastInitializedServer] = useState< + string | null + >(null) const queryClient = useQueryClient() // Fetch tools for the specific server @@ -66,16 +69,14 @@ export function McpToolsModal({ refetchOnMount: true, // Always refetch when component mounts }) - // Initialize local state when data loads - useEffect(() => { - if (serverTools?.tools) { - const enabledTools = serverTools.tools - .filter((tool) => tool.enabled) - .map((tool) => tool.name) - - setLocalEnabledTools(enabledTools) - } - }, [serverTools, serverName]) + // Initialize local state when server or tools change + if (serverTools?.tools && lastInitializedServer !== serverName) { + const enabledTools = serverTools.tools + .filter((tool) => tool.enabled) + .map((tool) => tool.name) + setLocalEnabledTools(enabledTools) + setLastInitializedServer(serverName) + } // Save tools mutation const saveToolsMutation = useMutation({ diff --git a/renderer/src/features/chat/hooks/use-chat-settings.ts b/renderer/src/features/chat/hooks/use-chat-settings.ts index d3d209adf..d49b90da8 100644 --- a/renderer/src/features/chat/hooks/use-chat-settings.ts +++ b/renderer/src/features/chat/hooks/use-chat-settings.ts @@ -207,13 +207,7 @@ export function useChatSettings() { enabledTools: allEnabledTools, } } - }, [ - selectedModel?.provider, - selectedModel?.model, - providerSettings, - enabledMcpTools, - enabledMcpServers, - ]) + }, [selectedModel, providerSettings, enabledMcpTools, enabledMcpServers]) // Mutation to update selected model const updateSelectedModelMutation = useMutation({ @@ -285,7 +279,7 @@ export function useChatSettings() { throw error } }, - [selectedModel?.provider, selectedModel?.model, updateSelectedModelMutation] + [selectedModel, updateSelectedModelMutation] ) // Update only enabled tools @@ -316,7 +310,7 @@ export function useChatSettings() { throw error } }, - [selectedModel?.provider, updateProviderSettingsMutation] + [selectedModel, updateProviderSettingsMutation] ) // Load persisted settings for a provider diff --git a/renderer/src/features/mcp-servers/components/card-mcp-server/index.tsx b/renderer/src/features/mcp-servers/components/card-mcp-server/index.tsx index a96eb2975..5e5912e09 100644 --- a/renderer/src/features/mcp-servers/components/card-mcp-server/index.tsx +++ b/renderer/src/features/mcp-servers/components/card-mcp-server/index.tsx @@ -11,7 +11,7 @@ import { useMutationRestartServer } from '../../hooks/use-mutation-restart-serve import { useMutationStopServerList } from '../../hooks/use-mutation-stop-server' import { useSearch } from '@tanstack/react-router' -import { useEffect, useRef, useState } from 'react' +import { useRef, useState } from 'react' import { twMerge } from 'tailwind-merge' import { trackEvent } from '@/common/lib/analytics' import { @@ -110,11 +110,16 @@ export function CardMcpServer({ strict: false, }) const [isNewServer, setIsNewServer] = useState(false) + const [lastNewServerName, setLastNewServerName] = useState( + null + ) - useEffect(() => { - // Check if the server is new by looking for a specific search parameter - // This could be a query parameter or any other condition that indicates a new server - if ('newServerName' in search && search.newServerName === name) { + // Check if the server is new by looking for a specific search parameter + const searchNewServerName = + 'newServerName' in search ? search.newServerName : null + if (searchNewServerName !== lastNewServerName) { + setLastNewServerName(searchNewServerName) + if (searchNewServerName === name) { setIsNewServer(true) // clear state after 2 seconds setTimeout(() => { @@ -123,11 +128,7 @@ export function CardMcpServer({ } else { setIsNewServer(false) } - - return () => { - setIsNewServer(false) - } - }, [name, search]) + } // Check if the server is in deleting state const isDeleting = status === 'deleting' @@ -137,15 +138,14 @@ export function CardMcpServer({ const [hadRecentStatusChange, setHadRecentStatusChange] = useState(false) const [prevStatus, setPrevStatus] = useState(status) - useEffect(() => { - // show a brief animation for status transitions that are immediate - if (prevStatus !== status && ['running'].includes(status ?? '')) { + // show a brief animation for status transitions that are immediate + if (prevStatus !== status) { + setPrevStatus(status) + if (['running'].includes(status ?? '')) { setHadRecentStatusChange(true) - const timeout = setTimeout(() => setHadRecentStatusChange(false), 2500) - return () => clearTimeout(timeout) + setTimeout(() => setHadRecentStatusChange(false), 2500) } - setPrevStatus(status) - }, [status, prevStatus]) + } return ( groupsData?.groups ?? [], [groupsData]) const form = useForm({ resolver: zodV4Resolver( @@ -283,7 +283,7 @@ export function DialogFormLocalMcp({ )} setActiveTab(value as Tab)} > diff --git a/renderer/src/features/mcp-servers/components/remote-mcp/dialog-form-remote-mcp.tsx b/renderer/src/features/mcp-servers/components/remote-mcp/dialog-form-remote-mcp.tsx index b0e08270d..6fd19696b 100644 --- a/renderer/src/features/mcp-servers/components/remote-mcp/dialog-form-remote-mcp.tsx +++ b/renderer/src/features/mcp-servers/components/remote-mcp/dialog-form-remote-mcp.tsx @@ -8,7 +8,7 @@ import { import { Input } from '@/common/components/ui/input' import { TooltipInfoIcon } from '@/common/components/ui/tooltip-info-icon' import { zodV4Resolver } from '@/common/lib/zod-v4-resolver' -import { useForm } from 'react-hook-form' +import { useForm, useWatch } from 'react-hook-form' import { Select, SelectTrigger, @@ -19,7 +19,7 @@ import { import { FormFieldsAuth } from './form-fields-auth' import { useRunRemoteServer } from '../../hooks/use-run-remote-server' import log from 'electron-log/renderer' -import { useCallback, useState } from 'react' +import { useCallback, useState, useMemo } from 'react' import { useUpdateServer } from '../../hooks/use-update-server' import { getApiV1BetaSecretsDefaultKeysOptions, @@ -139,7 +139,7 @@ export function DialogFormRemoteMcp({ convertCreateRequestToFormData(existingServer, availableSecrets) const { data: groupsData } = useGroups() - const groups = groupsData?.groups ?? [] + const groups = useMemo(() => groupsData?.groups ?? [], [groupsData]) const form = useForm({ resolver: zodV4Resolver( @@ -220,7 +220,7 @@ export function DialogFormRemoteMcp({ } } - const authType = form.watch('auth_type') + const authType = useWatch({ control: form.control, name: 'auth_type' }) const isLoading = isSubmitting || (isEditing && isLoadingServer) const renderContent = useCallback(() => { diff --git a/renderer/src/features/mcp-servers/hooks/use-customize-tools-table.ts b/renderer/src/features/mcp-servers/hooks/use-customize-tools-table.ts index c15d06e7c..bdffbf06a 100644 --- a/renderer/src/features/mcp-servers/hooks/use-customize-tools-table.ts +++ b/renderer/src/features/mcp-servers/hooks/use-customize-tools-table.ts @@ -29,6 +29,7 @@ export function useCustomizeToolsTable({ }: UseCustomizeToolsTableProps) { const [enabledTools, setEnabledTools] = useState>({}) const [toolsOverride, setToolsOverride] = useState({}) + const [lastOverrideTools, setLastOverrideTools] = useState(overrideTools) const [editState, setEditState] = useState({ isOpen: false, tool: null, @@ -39,6 +40,14 @@ export function useCustomizeToolsTable({ const toolsInitializedRef = useRef(false) const previousToolNamesRef = useRef('') + // Sync toolsOverride with overrideTools when it changes + if (overrideTools !== lastOverrideTools) { + setLastOverrideTools(overrideTools) + if (overrideTools) { + setToolsOverride(overrideTools) + } + } + const isAllToolsEnabled = useMemo(() => { return Object.values(enabledTools).every((enabled) => enabled) }, [enabledTools]) @@ -129,12 +138,6 @@ export function useCustomizeToolsTable({ } }, [tools]) - useEffect(() => { - if (overrideTools) { - setToolsOverride(overrideTools) - } - }, [overrideTools]) - const handleToolToggle = (toolName: string, enabled: boolean) => { trackEvent('Customize Tools: toggle tool', { tool_name: toolName, diff --git a/renderer/src/features/mcp-servers/sub-pages/logs-page/index.tsx b/renderer/src/features/mcp-servers/sub-pages/logs-page/index.tsx index 2c844fd6b..001deb53c 100644 --- a/renderer/src/features/mcp-servers/sub-pages/logs-page/index.tsx +++ b/renderer/src/features/mcp-servers/sub-pages/logs-page/index.tsx @@ -15,23 +15,26 @@ import { highlight } from './search' import { MCP_OPTIMIZER_GROUP_NAME } from '@/common/lib/constants' import { Skeleton } from '@/common/components/ui/skeleton' +// Generate skeleton counts once to avoid calling Math.random during render +const SKELETON_COUNTS = Array.from( + { length: 20 }, + () => Math.floor(Math.random() * 6) + 1 +) + function SkeletonLogs() { return (
- {Array.from({ length: 20 }).map((_, i) => { - const numSkeletons = Math.floor(Math.random() * 6) + 1 - return ( -
- - {Array.from({ length: numSkeletons }).map((_, j) => ( - - ))} -
- ) - })} + {SKELETON_COUNTS.map((numSkeletons, i) => ( +
+ + {Array.from({ length: numSkeletons }).map((_, j) => ( + + ))} +
+ ))}
) } diff --git a/renderer/src/features/meta-mcp/components/group-selector-form.tsx b/renderer/src/features/meta-mcp/components/group-selector-form.tsx index b4ac39c21..1e904ba45 100644 --- a/renderer/src/features/meta-mcp/components/group-selector-form.tsx +++ b/renderer/src/features/meta-mcp/components/group-selector-form.tsx @@ -1,5 +1,5 @@ import { useTransition, type ReactElement } from 'react' -import { useForm, Controller } from 'react-hook-form' +import { useForm, Controller, useWatch } from 'react-hook-form' import { z } from 'zod' import { RadioGroup, RadioGroupItem } from '@/common/components/ui/radio-group' import { Label } from '@/common/components/ui/label' @@ -59,7 +59,10 @@ export function GroupSelectorForm({ }) const isDirty = form.formState.isDirty - const isSelectedGroup = form.watch('selectedGroup') + const isSelectedGroup = useWatch({ + control: form.control, + name: 'selectedGroup', + }) const onSubmit = async (data: FormSchema) => { const optimized_workloads = groups.flatMap((g) => g.servers) diff --git a/renderer/src/features/meta-mcp/hooks/use-mcp-optimizer-groups.ts b/renderer/src/features/meta-mcp/hooks/use-mcp-optimizer-groups.ts index f0c97b259..924716315 100644 --- a/renderer/src/features/meta-mcp/hooks/use-mcp-optimizer-groups.ts +++ b/renderer/src/features/meta-mcp/hooks/use-mcp-optimizer-groups.ts @@ -18,10 +18,10 @@ export function useMcpOptimizerGroups() { }), }) - const groups = groupsData?.groups ?? [] - const workloads = workloadsData?.workloads ?? [] - const groupsWithServers = useMemo(() => { + const groups = groupsData?.groups ?? [] + const workloads = workloadsData?.workloads ?? [] + const serversByGroup: Record = {} workloads @@ -40,7 +40,7 @@ export function useMcpOptimizerGroups() { ...group, servers: serversByGroup[group.name ?? ''] ?? [], })) - }, [groups, workloads]) + }, [groupsData, workloadsData]) return groupsWithServers } diff --git a/renderer/src/features/registry-servers/components/dialog-form-remote-registry-mcp.tsx b/renderer/src/features/registry-servers/components/dialog-form-remote-registry-mcp.tsx index 3195ac1d0..45cf7385c 100644 --- a/renderer/src/features/registry-servers/components/dialog-form-remote-registry-mcp.tsx +++ b/renderer/src/features/registry-servers/components/dialog-form-remote-registry-mcp.tsx @@ -1,5 +1,5 @@ import { useState } from 'react' -import { useForm } from 'react-hook-form' +import { useForm, useWatch } from 'react-hook-form' import { useQuery } from '@tanstack/react-query' import log from 'electron-log/renderer' import type { RegistryRemoteServerMetadata } from '@api/types.gen' @@ -180,11 +180,11 @@ export function DialogFormRemoteRegistryMcp({ ) } - if (!server) return null - - const authType = form.watch('auth_type') + const authType = useWatch({ control: form.control, name: 'auth_type' }) const isLoading = isSubmitting + if (!server) return null + return ( { + setWizardState({ currentIndex: 0, isGroupCreated: false }) + onClose() + }, [onClose]) + + // Create group when dialog opens and group hasn't been created yet useEffect(() => { - if (!isOpen) { - setWizardState({ currentIndex: 0, isGroupCreated: false }) - } else if (!wizardState.isGroupCreated && group?.name) { + if (isOpen && !wizardState.isGroupCreated && group?.name) { createGroupMutation .mutateAsync({ body: { @@ -54,10 +58,16 @@ export function MultiServerInstallWizard({ setWizardState((prev) => ({ ...prev, isGroupCreated: true })) }) .catch(() => { - onClose() + handleClose() }) } - }, [isOpen, wizardState.isGroupCreated, group?.name]) + }, [ + isOpen, + wizardState.isGroupCreated, + group?.name, + createGroupMutation, + handleClose, + ]) if (!isOpen || servers.length === 0 || !group?.name) return null @@ -74,7 +84,7 @@ export function MultiServerInstallWizard({ const handleNext = (closeDialog: () => void) => { if (!hasMoreServers) { closeDialog() - onClose() + handleClose() return } @@ -90,7 +100,7 @@ export function MultiServerInstallWizard({ key={currentServer.name} server={currentServer} isOpen={isOpen} - closeDialog={onClose} + closeDialog={handleClose} onSubmitSuccess={handleNext} hardcodedGroup={group.name} actionsSubmitLabel={hasMoreServers ? 'Next' : 'Finish'} @@ -113,7 +123,7 @@ export function MultiServerInstallWizard({ key={currentServer.name} server={currentServer} isOpen={isOpen} - onOpenChange={onClose} + onOpenChange={handleClose} onSubmitSuccess={handleNext} hardcodedGroup={group.name} actionsSubmitLabel={hasMoreServers ? 'Next' : 'Finish'} From b21b8913e3c0f2c887ca045718745fe3d063d7cc Mon Sep 17 00:00:00 2001 From: Giuseppe Scuglia Date: Mon, 24 Nov 2025 17:41:30 +0100 Subject: [PATCH 3/5] refactor: replace setTimouet with delay --- .../components/card-mcp-server/index.tsx | 71 ++++++++++++------- 1 file changed, 46 insertions(+), 25 deletions(-) diff --git a/renderer/src/features/mcp-servers/components/card-mcp-server/index.tsx b/renderer/src/features/mcp-servers/components/card-mcp-server/index.tsx index 5e5912e09..0017c3fa3 100644 --- a/renderer/src/features/mcp-servers/components/card-mcp-server/index.tsx +++ b/renderer/src/features/mcp-servers/components/card-mcp-server/index.tsx @@ -11,9 +11,10 @@ import { useMutationRestartServer } from '../../hooks/use-mutation-restart-serve import { useMutationStopServerList } from '../../hooks/use-mutation-stop-server' import { useSearch } from '@tanstack/react-router' -import { useRef, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import { twMerge } from 'tailwind-merge' import { trackEvent } from '@/common/lib/analytics' +import { delay } from '@utils/delay' import { Tooltip, TooltipTrigger, @@ -110,25 +111,29 @@ export function CardMcpServer({ strict: false, }) const [isNewServer, setIsNewServer] = useState(false) - const [lastNewServerName, setLastNewServerName] = useState( - null - ) - - // Check if the server is new by looking for a specific search parameter const searchNewServerName = 'newServerName' in search ? search.newServerName : null - if (searchNewServerName !== lastNewServerName) { - setLastNewServerName(searchNewServerName) + + useEffect(() => { + // Check if the server is new by looking for a specific search parameter if (searchNewServerName === name) { - setIsNewServer(true) - // clear state after 2 seconds - setTimeout(() => { - setIsNewServer(false) - }, 2000) - } else { - setIsNewServer(false) + let cancelled = false + + const showNewServerAnimation = async () => { + setIsNewServer(true) + await delay(2000) + if (!cancelled) { + setIsNewServer(false) + } + } + + showNewServerAnimation() + + return () => { + cancelled = true + } } - } + }, [name, searchNewServerName]) // Check if the server is in deleting state const isDeleting = status === 'deleting' @@ -136,16 +141,32 @@ export function CardMcpServer({ status === 'starting' || status === 'stopping' || status === 'restarting' const isStopped = status === 'stopped' || status === 'stopping' const [hadRecentStatusChange, setHadRecentStatusChange] = useState(false) - const [prevStatus, setPrevStatus] = useState(status) - - // show a brief animation for status transitions that are immediate - if (prevStatus !== status) { - setPrevStatus(status) - if (['running'].includes(status ?? '')) { - setHadRecentStatusChange(true) - setTimeout(() => setHadRecentStatusChange(false), 2500) + const prevStatusRef = useRef(status) + + useEffect(() => { + // Show a brief animation for status transitions + if ( + prevStatusRef.current !== status && + ['running'].includes(status ?? '') + ) { + let cancelled = false + + const showStatusChangeAnimation = async () => { + setHadRecentStatusChange(true) + await delay(2500) + if (!cancelled) { + setHadRecentStatusChange(false) + } + } + + showStatusChangeAnimation() + + return () => { + cancelled = true + } } - } + prevStatusRef.current = status + }, [status]) return ( Date: Wed, 26 Nov 2025 13:03:14 +0100 Subject: [PATCH 4/5] fix: group registry effect --- .../components/multi-server-install-wizard.tsx | 7 ++++++- .../src/routes/__tests__/registry-group_.$name.test.tsx | 6 +++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/renderer/src/features/registry-servers/components/multi-server-install-wizard.tsx b/renderer/src/features/registry-servers/components/multi-server-install-wizard.tsx index 447e912e4..0278efd11 100644 --- a/renderer/src/features/registry-servers/components/multi-server-install-wizard.tsx +++ b/renderer/src/features/registry-servers/components/multi-server-install-wizard.tsx @@ -47,7 +47,12 @@ export function MultiServerInstallWizard({ // Create group when dialog opens and group hasn't been created yet useEffect(() => { - if (isOpen && !wizardState.isGroupCreated && group?.name) { + if ( + isOpen && + !wizardState.isGroupCreated && + !createGroupMutation.isPending && + group?.name + ) { createGroupMutation .mutateAsync({ body: { diff --git a/renderer/src/routes/__tests__/registry-group_.$name.test.tsx b/renderer/src/routes/__tests__/registry-group_.$name.test.tsx index a70836cdb..36a7000eb 100644 --- a/renderer/src/routes/__tests__/registry-group_.$name.test.tsx +++ b/renderer/src/routes/__tests__/registry-group_.$name.test.tsx @@ -401,7 +401,7 @@ describe('Registry Group Detail Route', () => { name: /configure first-server/i, }) if (h) { - await waitForElementToBeRemoved(h, { timeout: 10000 }) + await waitForElementToBeRemoved(h) } } const secondServerHeading = await screen.findByRole('heading', { @@ -543,7 +543,7 @@ describe('Registry Group Detail Route', () => { name: /configure first-server/i, }) if (h2) { - await waitForElementToBeRemoved(h2, { timeout: 10000 }) + await waitForElementToBeRemoved(h2) } } const secondServerHeading2 = await screen.findByRole('heading', { @@ -562,7 +562,7 @@ describe('Registry Group Detail Route', () => { // The wizard closes the dialog before emitting readiness toasts const dialog = screen.queryByRole('dialog') if (dialog) { - await waitForElementToBeRemoved(dialog, { timeout: 10000 }) + await waitForElementToBeRemoved(dialog) } await waitFor(() => { From 09e3d022c4012a6e6f242fbe7eb6fb5a4d7169b1 Mon Sep 17 00:00:00 2001 From: Giuseppe Scuglia Date: Wed, 26 Nov 2025 13:12:17 +0100 Subject: [PATCH 5/5] fix: initialize tools override --- .../features/mcp-servers/hooks/use-customize-tools-table.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/renderer/src/features/mcp-servers/hooks/use-customize-tools-table.ts b/renderer/src/features/mcp-servers/hooks/use-customize-tools-table.ts index bdffbf06a..6e8b2c941 100644 --- a/renderer/src/features/mcp-servers/hooks/use-customize-tools-table.ts +++ b/renderer/src/features/mcp-servers/hooks/use-customize-tools-table.ts @@ -28,7 +28,9 @@ export function useCustomizeToolsTable({ onApply, }: UseCustomizeToolsTableProps) { const [enabledTools, setEnabledTools] = useState>({}) - const [toolsOverride, setToolsOverride] = useState({}) + const [toolsOverride, setToolsOverride] = useState( + overrideTools ?? {} + ) const [lastOverrideTools, setLastOverrideTools] = useState(overrideTools) const [editState, setEditState] = useState({ isOpen: false,