diff --git a/src/main/services/extensions/install/McpInstallService.ts b/src/main/services/extensions/install/McpInstallService.ts index 3c83c0fe..687c904a 100644 --- a/src/main/services/extensions/install/McpInstallService.ts +++ b/src/main/services/extensions/install/McpInstallService.ts @@ -37,6 +37,10 @@ const HEADER_KEY_RE = /^[A-Za-z][\w-]{0,100}$/; const TIMEOUT_MS = 30_000; +function scopeRequiresProjectPath(scope?: string): boolean { + return scope === 'local' || scope === 'project'; +} + export class McpInstallService { constructor(private readonly aggregator: McpCatalogAggregator) {} @@ -59,6 +63,13 @@ export class McpInstallService { }; } + if (scopeRequiresProjectPath(scope) && !projectPath) { + return { + state: 'error', + error: `projectPath is required for ${scope} scope`, + }; + } + // 3. Validate env var keys (prevent command injection) for (const key of Object.keys(envValues)) { if (!ENV_KEY_RE.test(key)) { @@ -212,6 +223,10 @@ export class McpInstallService { return { state: 'error', error: `Invalid scope: "${scope}".` }; } + if (scopeRequiresProjectPath(scope) && !projectPath) { + return { state: 'error', error: `projectPath is required for ${scope} scope` }; + } + for (const key of Object.keys(envValues)) { if (!ENV_KEY_RE.test(key)) { return { state: 'error', error: `Invalid env var name: "${key}".` }; @@ -319,6 +334,13 @@ export class McpInstallService { }; } + if (scopeRequiresProjectPath(scope) && !projectPath) { + return { + state: 'error', + error: `projectPath is required for ${scope} scope`, + }; + } + if (projectPath && !path.isAbsolute(projectPath)) { return { state: 'error', diff --git a/src/main/services/extensions/state/McpInstallationStateService.ts b/src/main/services/extensions/state/McpInstallationStateService.ts index f947f190..a550237b 100644 --- a/src/main/services/extensions/state/McpInstallationStateService.ts +++ b/src/main/services/extensions/state/McpInstallationStateService.ts @@ -3,8 +3,8 @@ * * Sources: * - User scope: ~/.claude.json → mcpServers + * - Local scope: ~/.claude.json → projects[projectPath].mcpServers * - Project scope: .mcp.json in project root - * - Local scope: determined by Claude CLI (may also be in ~/.claude.json) * * Both files are managed by the Claude CLI. This service is read-only. */ @@ -27,30 +27,30 @@ interface TimedCache { } export class McpInstallationStateService { - private cache: TimedCache | null = null; + private cache = new Map>(); /** - * Get all installed MCP servers across user and project scopes. + * Get all installed MCP servers across user, local, and project scopes. */ async getInstalled(projectPath?: string): Promise { - // Cache is project-path-dependent, so invalidate on path change - if (this.cache && Date.now() - this.cache.fetchedAt < CACHE_TTL_MS) { - return this.cache.data; + const cacheKey = projectPath ?? '__user__'; + const cached = this.cache.get(cacheKey); + if (cached && Date.now() - cached.fetchedAt < CACHE_TTL_MS) { + return cached.data; } const entries: InstalledMcpEntry[] = []; + const claudeConfig = await this.readClaudeConfig(); // User scope: ~/.claude.json - const userEntries = await this.readUserMcpServers(); - entries.push(...userEntries); + entries.push(...this.readUserMcpServers(claudeConfig)); - // Project scope: .mcp.json if (projectPath) { - const projectEntries = await this.readProjectMcpServers(projectPath); - entries.push(...projectEntries); + entries.push(...this.readLocalMcpServers(claudeConfig, projectPath)); + entries.push(...(await this.readProjectMcpServers(projectPath))); } - this.cache = { data: entries, fetchedAt: Date.now() }; + this.cache.set(cacheKey, { data: entries, fetchedAt: Date.now() }); return entries; } @@ -58,14 +58,42 @@ export class McpInstallationStateService { * Invalidate cache. Call after install/uninstall operations. */ invalidateCache(): void { - this.cache = null; + this.cache.clear(); } // ── Private ──────────────────────────────────────────────────────────── - private async readUserMcpServers(): Promise { + private async readClaudeConfig(): Promise | null> { const configPath = path.join(getHomeDir(), '.claude.json'); - return this.readMcpServersFromFile(configPath, 'user'); + try { + const raw = await fs.readFile(configPath, 'utf-8'); + return JSON.parse(raw) as Record; + } catch (err) { + if ((err as NodeJS.ErrnoException).code === 'ENOENT') { + return null; + } + logger.error(`Failed to read MCP servers from ${configPath}:`, err); + return null; + } + } + + private readUserMcpServers(config: Record | null): InstalledMcpEntry[] { + return this.readMcpServersFromConfig(config?.mcpServers, 'user'); + } + + private readLocalMcpServers( + config: Record | null, + projectPath: string + ): InstalledMcpEntry[] { + const projects = + config && typeof config.projects === 'object' && config.projects + ? (config.projects as Record) + : null; + const projectConfig = + projects && typeof projects[projectPath] === 'object' && projects[projectPath] + ? (projects[projectPath] as Record) + : null; + return this.readMcpServersFromConfig(projectConfig?.mcpServers, 'local'); } private async readProjectMcpServers(projectPath: string): Promise { @@ -73,6 +101,27 @@ export class McpInstallationStateService { return this.readMcpServersFromFile(configPath, 'project'); } + private readMcpServersFromConfig( + value: unknown, + scope: 'user' | 'project' | 'local' + ): InstalledMcpEntry[] { + const mcpServers = + value && typeof value === 'object' + ? (value as Record) + : null; + if (!mcpServers) { + return []; + } + + return Object.entries(mcpServers).map(([name, config]): InstalledMcpEntry => { + let transport: string | undefined; + if (config.command) transport = 'stdio'; + else if (config.url) transport = 'http'; + + return { name, scope, transport }; + }); + } + private async readMcpServersFromFile( filePath: string, scope: 'user' | 'project' @@ -80,21 +129,7 @@ export class McpInstallationStateService { try { const raw = await fs.readFile(filePath, 'utf-8'); const json = JSON.parse(raw) as Record; - const mcpServers = json.mcpServers as - | Record - | undefined; - - if (!mcpServers || typeof mcpServers !== 'object') { - return []; - } - - return Object.entries(mcpServers).map(([name, config]): InstalledMcpEntry => { - let transport: string | undefined; - if (config.command) transport = 'stdio'; - else if (config.url) transport = 'http'; - - return { name, scope, transport }; - }); + return this.readMcpServersFromConfig(json.mcpServers, scope); } catch (err) { if ((err as NodeJS.ErrnoException).code === 'ENOENT') { return []; diff --git a/src/renderer/components/extensions/ExtensionStoreView.tsx b/src/renderer/components/extensions/ExtensionStoreView.tsx index 7b736687..7f1331c3 100644 --- a/src/renderer/components/extensions/ExtensionStoreView.tsx +++ b/src/renderer/components/extensions/ExtensionStoreView.tsx @@ -323,6 +323,7 @@ export const ExtensionStoreView = (): React.JSX.Element => { { { setCustomMcpDialogOpen(false)} + projectPath={projectPath} /> diff --git a/src/renderer/components/extensions/mcp/CustomMcpServerDialog.tsx b/src/renderer/components/extensions/mcp/CustomMcpServerDialog.tsx index 35624aad..7cb8e740 100644 --- a/src/renderer/components/extensions/mcp/CustomMcpServerDialog.tsx +++ b/src/renderer/components/extensions/mcp/CustomMcpServerDialog.tsx @@ -37,14 +37,16 @@ const SERVER_NAME_RE = /^[\w.-]{1,100}$/; interface CustomMcpServerDialogProps { open: boolean; onClose: () => void; + projectPath: string | null; } type TransportMode = 'stdio' | 'http'; type HttpTransport = 'streamable-http' | 'sse' | 'http'; -type Scope = 'local' | 'user'; +type Scope = 'local' | 'user' | 'project'; const SCOPE_OPTIONS: { value: Scope; label: string }[] = [ { value: 'user', label: 'User (global)' }, + { value: 'project', label: 'Project' }, { value: 'local', label: 'Local' }, ]; @@ -62,6 +64,7 @@ interface EnvEntry { export const CustomMcpServerDialog = ({ open, onClose, + projectPath, }: CustomMcpServerDialogProps): React.JSX.Element => { const installCustomMcpServer = useStore((s) => s.installCustomMcpServer); @@ -101,6 +104,12 @@ export const CustomMcpServerDialog = ({ } }, [open]); + useEffect(() => { + if (open && scope !== 'user' && !projectPath) { + setScope('user'); + } + }, [open, projectPath, scope]); + // Auto-fill env vars from saved API keys useEffect(() => { if (!open || envVars.length === 0 || !api.apiKeys) return; @@ -168,6 +177,7 @@ export const CustomMcpServerDialog = ({ const request: McpCustomInstallRequest = { serverName, scope, + projectPath: scope !== 'user' ? (projectPath ?? undefined) : undefined, installSpec, envValues, headers: headers.filter((h) => h.key.trim() && h.value.trim()), @@ -197,6 +207,7 @@ export const CustomMcpServerDialog = ({ const canSubmit = serverName.trim() && (transportMode === 'stdio' ? npmPackage.trim() : httpUrl.trim()) && + !(scope !== 'user' && !projectPath) && !installing; return ( @@ -372,7 +383,11 @@ export const CustomMcpServerDialog = ({ {SCOPE_OPTIONS.map((opt) => ( - + {opt.label} ))} diff --git a/src/renderer/components/extensions/mcp/McpServerCard.tsx b/src/renderer/components/extensions/mcp/McpServerCard.tsx index e2f04f87..afae2142 100644 --- a/src/renderer/components/extensions/mcp/McpServerCard.tsx +++ b/src/renderer/components/extensions/mcp/McpServerCard.tsx @@ -11,18 +11,28 @@ import { Button } from '@renderer/components/ui/button'; import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip'; import { useStore } from '@renderer/store'; import { formatCompactNumber, formatRelativeTime } from '@renderer/utils/formatters'; -import { sanitizeMcpServerName } from '@shared/utils/extensionNormalizers'; +import { + getMcpInstallationSummaryLabel, + getMcpOperationKey, + sanitizeMcpServerName, +} from '@shared/utils/extensionNormalizers'; import { Clock, Cloud, Globe, KeyRound, Lock, Monitor, Star, Tag, Wrench } from 'lucide-react'; import { Github as GithubIcon } from 'lucide-react'; import { InstallButton } from '../common/InstallButton'; import { SourceBadge } from '../common/SourceBadge'; -import type { McpCatalogItem, McpServerDiagnostic } from '@shared/types/extensions'; +import type { + InstalledMcpEntry, + McpCatalogItem, + McpServerDiagnostic, +} from '@shared/types/extensions'; interface McpServerCardProps { server: McpCatalogItem; isInstalled: boolean; + installedEntry?: InstalledMcpEntry | null; + installedEntries?: InstalledMcpEntry[]; diagnostic?: McpServerDiagnostic | null; diagnosticsLoading?: boolean; onClick: (serverId: string) => void; @@ -31,23 +41,42 @@ interface McpServerCardProps { export const McpServerCard = ({ server, isInstalled, + installedEntry, + installedEntries = [], diagnostic, diagnosticsLoading, onClick, }: McpServerCardProps): React.JSX.Element => { - const installProgress = useStore((s) => s.mcpInstallProgress[server.id] ?? 'idle'); + const operationKey = getMcpOperationKey(server.id, 'user'); + const installProgress = useStore((s) => s.mcpInstallProgress[operationKey] ?? 'idle'); const installMcpServer = useStore((s) => s.installMcpServer); const uninstallMcpServer = useStore((s) => s.uninstallMcpServer); - const installError = useStore((s) => s.installErrors[server.id]); + const installError = useStore((s) => s.installErrors[operationKey]); const stars = useStore((s) => server.repositoryUrl ? s.mcpGitHubStars[server.repositoryUrl] : undefined ); const canAutoInstall = !!server.installSpec; + const normalizedInstalledEntries = installedEntries.length + ? installedEntries + : installedEntry + ? [installedEntry] + : []; const requiresConfiguration = server.installSpec?.type === 'http' || server.envVars.length > 0 || server.requiresAuth || (server.authHeaders?.length ?? 0) > 0; + const defaultServerName = sanitizeMcpServerName(server.name); + const userInstallEntry = + normalizedInstalledEntries.find((entry) => entry.scope === 'user') ?? null; + const installSummaryLabel = getMcpInstallationSummaryLabel(normalizedInstalledEntries); + const supportsDirectInstalledAction = + isInstalled && + normalizedInstalledEntries.length === 1 && + userInstallEntry?.name === defaultServerName && + !requiresConfiguration; + const shouldShowDirectInstallButton = + canAutoInstall && (!isInstalled ? !requiresConfiguration : supportsDirectInstalledAction); const [imgError, setImgError] = useState(false); const hasIcon = !!server.iconUrl && !imgError; const diagnosticBadgeClass = @@ -103,7 +132,7 @@ export const McpServerCard = ({ className="border-emerald-500/30 bg-emerald-500/10 text-emerald-400" variant="outline" > - Installed + {installSummaryLabel ?? 'Installed'} )} {isInstalled && diagnosticsLoading && !diagnostic && ( @@ -224,7 +253,7 @@ export const McpServerCard = ({ )} - {canAutoInstall && !requiresConfiguration && ( + {shouldShowDirectInstallButton && (
installMcpServer({ registryId: server.id, - serverName: sanitizeMcpServerName(server.name), + serverName: defaultServerName, scope: 'user', envValues: {}, headers: [], }) } - onUninstall={() => uninstallMcpServer(server.id, sanitizeMcpServerName(server.name))} + onUninstall={() => + uninstallMcpServer(server.id, userInstallEntry?.name ?? defaultServerName, 'user') + } size="sm" errorMessage={installError} />
)} - {canAutoInstall && requiresConfiguration && ( + {canAutoInstall && (!shouldShowDirectInstallButton || requiresConfiguration) && (
)} - {(isInstalled || diagnosticsLoading) && ( + {isInstalledForScope && (
Claude Status @@ -366,7 +389,7 @@ export const McpServerDetailDialog = ({ {canAutoInstall && (

- {isInstalled ? 'Manage Installation' : 'Install Server'} + {isInstalledForScope ? 'Manage Installation' : 'Install Server'}

{/* Server name */} @@ -380,6 +403,7 @@ export const McpServerDetailDialog = ({ onChange={(e) => setServerName(e.target.value)} placeholder="my-server" className="h-8 text-sm" + disabled={isInstalledForScope} />
@@ -392,7 +416,11 @@ export const McpServerDetailDialog = ({ {SCOPE_OPTIONS.map((opt) => ( - + {opt.label} ))} @@ -499,7 +527,7 @@ export const McpServerDetailDialog = ({
void; mcpSearchResults: McpCatalogItem[]; @@ -65,6 +69,7 @@ interface McpServersPanelProps { } export const McpServersPanel = ({ + projectPath, mcpSearchQuery, mcpSearch, mcpSearchResults, @@ -107,10 +112,10 @@ export const McpServersPanel = ({ // Load initial browse data useEffect(() => { - if (browseCatalog.length === 0 && !browseLoading) { + if (browseCatalog.length === 0 && !browseLoading && !browseError) { void mcpBrowse(); } - }, [browseCatalog.length, browseLoading, mcpBrowse]); + }, [browseCatalog.length, browseError, browseLoading, mcpBrowse]); useEffect(() => { void runMcpDiagnostics(); @@ -136,17 +141,24 @@ export const McpServersPanel = ({ [installedServers] ); - const installedEntriesByName = useMemo( - () => new Map(installedServers.map((entry) => [entry.name.toLowerCase(), entry] as const)), - [installedServers] - ); + const installedEntriesByName = useMemo(() => { + const entriesByName = new Map(); + for (const entry of installedServers) { + const key = entry.name.toLowerCase(); + entriesByName.set(key, [...(entriesByName.get(key) ?? []), entry]); + } + return entriesByName; + }, [installedServers]); /** Check if a catalog server is installed by comparing sanitized names */ const isServerInstalled = (server: McpCatalogItem): boolean => installedNames.has(sanitizeMcpServerName(server.name)); + const getInstalledEntries = (server: McpCatalogItem): InstalledMcpEntry[] => + installedEntriesByName.get(sanitizeMcpServerName(server.name)) ?? []; + const getInstalledEntry = (server: McpCatalogItem): InstalledMcpEntry | null => - installedEntriesByName.get(sanitizeMcpServerName(server.name)) ?? null; + getPreferredMcpInstallationEntry(getInstalledEntries(server)); const getDiagnostic = (server: McpCatalogItem): McpServerDiagnostic | null => { const installedEntry = getInstalledEntry(server); @@ -374,6 +386,8 @@ export const McpServersPanel = ({ key={server.id} server={server} isInstalled={isServerInstalled(server)} + installedEntry={getInstalledEntry(server)} + installedEntries={getInstalledEntries(server)} diagnostic={getDiagnostic(server)} diagnosticsLoading={mcpDiagnosticsLoading} onClick={setSelectedMcpServerId} @@ -400,8 +414,11 @@ export const McpServersPanel = ({ setSelectedMcpServerId(null)} /> diff --git a/src/renderer/components/extensions/plugins/PluginCard.tsx b/src/renderer/components/extensions/plugins/PluginCard.tsx index 9ffe21d5..1b7818bf 100644 --- a/src/renderer/components/extensions/plugins/PluginCard.tsx +++ b/src/renderer/components/extensions/plugins/PluginCard.tsx @@ -7,6 +7,7 @@ import { useStore } from '@renderer/store'; import { getCapabilityLabel, getInstallationSummaryLabel, + getPluginOperationKey, hasInstallationInScope, inferCapabilities, normalizeCategory, @@ -27,10 +28,11 @@ interface PluginCardProps { export const PluginCard = ({ plugin, index, onClick }: PluginCardProps): React.JSX.Element => { const capabilities = inferCapabilities(plugin); const category = normalizeCategory(plugin.category); - const installProgress = useStore((s) => s.pluginInstallProgress[plugin.pluginId] ?? 'idle'); + const operationKey = getPluginOperationKey(plugin.pluginId, 'user'); + const installProgress = useStore((s) => s.pluginInstallProgress[operationKey] ?? 'idle'); const installPlugin = useStore((s) => s.installPlugin); const uninstallPlugin = useStore((s) => s.uninstallPlugin); - const installError = useStore((s) => s.installErrors[plugin.pluginId]); + const installError = useStore((s) => s.installErrors[operationKey]); const isUserInstalled = hasInstallationInScope(plugin.installations, 'user'); const installSummaryLabel = getInstallationSummaryLabel(plugin.installations); const baseStriped = index % 2 === 0; diff --git a/src/renderer/components/extensions/plugins/PluginDetailDialog.tsx b/src/renderer/components/extensions/plugins/PluginDetailDialog.tsx index f0860b5e..46295c2b 100644 --- a/src/renderer/components/extensions/plugins/PluginDetailDialog.tsx +++ b/src/renderer/components/extensions/plugins/PluginDetailDialog.tsx @@ -27,6 +27,7 @@ import { useStore } from '@renderer/store'; import { getCapabilityLabel, getInstallationSummaryLabel, + getPluginOperationKey, hasInstallationInScope, inferCapabilities, normalizeCategory, @@ -44,6 +45,7 @@ interface PluginDetailDialogProps { plugin: EnrichedPlugin | null; open: boolean; onClose: () => void; + projectPath: string | null; } const SCOPE_OPTIONS: { value: InstallScope; label: string }[] = [ @@ -56,31 +58,20 @@ export const PluginDetailDialog = ({ plugin, open, onClose, + projectPath, }: PluginDetailDialogProps): React.JSX.Element => { - const { - fetchPluginReadme, - readmes, - readmeLoading, - installPlugin, - uninstallPlugin, - pluginCatalogProjectPath, - } = useStore( + const { fetchPluginReadme, readmes, readmeLoading, installPlugin, uninstallPlugin } = useStore( useShallow((s) => ({ fetchPluginReadme: s.fetchPluginReadme, readmes: s.pluginReadmes, readmeLoading: s.pluginReadmeLoading, installPlugin: s.installPlugin, uninstallPlugin: s.uninstallPlugin, - pluginCatalogProjectPath: s.pluginCatalogProjectPath, })) ); - const installProgress = useStore( - (s) => (plugin ? s.pluginInstallProgress[plugin.pluginId] : undefined) ?? 'idle' - ); - const installError = useStore((s) => (plugin ? s.installErrors[plugin.pluginId] : undefined)); const [scope, setScope] = useState('user'); - const projectScopeAvailable = Boolean(pluginCatalogProjectPath); + const projectScopeAvailable = Boolean(projectPath); useEffect(() => { if (plugin && open) { @@ -100,6 +91,12 @@ export const PluginDetailDialog = ({ } }, [projectScopeAvailable, scope]); + const operationKey = plugin ? getPluginOperationKey(plugin.pluginId, scope) : null; + const installProgress = useStore( + (s) => (operationKey ? s.pluginInstallProgress[operationKey] : undefined) ?? 'idle' + ); + const installError = useStore((s) => (operationKey ? s.installErrors[operationKey] : undefined)); + if (!plugin) return <>; const capabilities = inferCapabilities(plugin); @@ -202,16 +199,14 @@ export const PluginDetailDialog = ({ installPlugin({ pluginId: plugin.pluginId, scope, - ...(scope !== 'user' && pluginCatalogProjectPath - ? { projectPath: pluginCatalogProjectPath } - : {}), + ...(scope !== 'user' && projectPath ? { projectPath } : {}), }) } onUninstall={() => uninstallPlugin( plugin.pluginId, scope, - scope !== 'user' ? (pluginCatalogProjectPath ?? undefined) : undefined + scope !== 'user' ? (projectPath ?? undefined) : undefined ) } size="default" diff --git a/src/renderer/components/extensions/plugins/PluginsPanel.tsx b/src/renderer/components/extensions/plugins/PluginsPanel.tsx index 5480b7c8..4fe38a2e 100644 --- a/src/renderer/components/extensions/plugins/PluginsPanel.tsx +++ b/src/renderer/components/extensions/plugins/PluginsPanel.tsx @@ -35,6 +35,7 @@ import type { } from '@shared/types/extensions'; interface PluginsPanelProps { + projectPath: string | null; pluginFilters: PluginFilters; pluginSort: { field: PluginSortField; order: 'asc' | 'desc' }; selectedPluginId: string | null; @@ -111,6 +112,7 @@ function selectFilteredPlugins( } export const PluginsPanel = ({ + projectPath, pluginFilters, pluginSort, selectedPluginId, @@ -395,6 +397,7 @@ export const PluginsPanel = ({ plugin={selectedPlugin} open={selectedPluginId !== null} onClose={() => setSelectedPluginId(null)} + projectPath={projectPath} />
); diff --git a/src/renderer/components/extensions/skills/SkillDetailDialog.tsx b/src/renderer/components/extensions/skills/SkillDetailDialog.tsx index d70238d6..2b57a9ca 100644 --- a/src/renderer/components/extensions/skills/SkillDetailDialog.tsx +++ b/src/renderer/components/extensions/skills/SkillDetailDialog.tsx @@ -26,6 +26,8 @@ import { useStore } from '@renderer/store'; import { AlertTriangle, ExternalLink, FolderOpen, Pencil, Trash2 } from 'lucide-react'; import { useShallow } from 'zustand/react/shallow'; +import { resolveSkillProjectPath } from './skillProjectUtils'; + interface SkillDetailDialogProps { skillId: string | null; open: boolean; @@ -58,8 +60,13 @@ export const SkillDetailDialog = ({ useEffect(() => { if (!open || !skillId) return; - void fetchSkillDetail(skillId, projectPath ?? undefined).catch(() => undefined); - }, [fetchSkillDetail, open, projectPath, skillId]); + void fetchSkillDetail( + skillId, + detail?.item.scope + ? resolveSkillProjectPath(detail.item.scope, projectPath, detail.item.projectRoot) + : (projectPath ?? undefined) + ).catch(() => undefined); + }, [detail?.item.projectRoot, detail?.item.scope, fetchSkillDetail, open, projectPath, skillId]); useEffect(() => { if (!open) { @@ -70,6 +77,9 @@ export const SkillDetailDialog = ({ }, [open]); const item = detail?.item; + const effectiveProjectPath = item + ? resolveSkillProjectPath(item.scope, projectPath, item.projectRoot) + : (projectPath ?? undefined); function formatRootKind(rootKind: 'claude' | 'cursor' | 'agents'): string { return `.${rootKind}`; @@ -92,7 +102,7 @@ export const SkillDetailDialog = ({ try { await deleteSkill({ skillId: item.id, - projectPath: projectPath ?? undefined, + projectPath: effectiveProjectPath, }); setDeleteConfirmOpen(false); onDeleted(); @@ -125,7 +135,7 @@ export const SkillDetailDialog = ({ variant="outline" size="sm" onClick={() => { - void fetchSkillDetail(skillId, projectPath ?? undefined).catch(() => undefined); + void fetchSkillDetail(skillId, effectiveProjectPath).catch(() => undefined); }} > Retry @@ -288,7 +298,7 @@ export const SkillDetailDialog = ({
@@ -260,7 +307,7 @@ export const SkillImportDialog = ({