diff --git a/src/renderer/components/extensions/ExtensionStoreView.tsx b/src/renderer/components/extensions/ExtensionStoreView.tsx index 7f1331c3..1f9ea351 100644 --- a/src/renderer/components/extensions/ExtensionStoreView.tsx +++ b/src/renderer/components/extensions/ExtensionStoreView.tsx @@ -7,6 +7,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { api } from '@renderer/api'; +import { Badge } from '@renderer/components/ui/badge'; import { Button } from '@renderer/components/ui/button'; import { Tabs, TabsContent, TabsList } from '@renderer/components/ui/tabs'; import { @@ -164,14 +165,20 @@ export const ExtensionStoreView = (): React.JSX.Element => { const isRefreshing = cliStatusLoading || apiKeysLoading || pluginCatalogLoading || mcpBrowseLoading || skillsLoading; const cliStatusBanner = useMemo(() => { + const providers = cliStatus?.providers ?? []; + const isMultimodel = cliStatus?.flavor === 'agent_teams_orchestrator' && providers.length > 0; + if (cliStatusLoading || cliStatus === null) { return (
-

Checking Claude CLI availability

+

+ Checking extensions runtime availability +

- Extensions need Claude CLI to install plugins, run MCP servers, and validate auth. + Extensions need the configured runtime to manage plugins, MCP servers, skills, and + provider connections.

@@ -186,13 +193,13 @@ export const ExtensionStoreView = (): React.JSX.Element => {

{cliLaunchIssue - ? 'Claude CLI was found but failed to start' - : 'Claude CLI is not available'} + ? 'The configured runtime was found but failed to start' + : 'The configured runtime is not available'}

{cliLaunchIssue - ? 'Plugin installs are disabled until Claude CLI passes its startup health check. Open the Dashboard to repair or reinstall it.' - : 'Plugin installs are disabled until Claude CLI is installed. Open the Dashboard to install it and retry.'} + ? 'Extensions are disabled until the runtime passes its startup health check. Open the Dashboard to repair or reinstall it.' + : 'Extensions are disabled until the runtime is installed. Open the Dashboard to install it and retry.'}

{cliLaunchIssue && cliStatus.launchError && (

@@ -207,7 +214,7 @@ export const ExtensionStoreView = (): React.JSX.Element => { ); } - if (!cliStatus.authLoggedIn) { + if (!isMultimodel && !cliStatus.authLoggedIn) { return (

@@ -226,6 +233,68 @@ export const ExtensionStoreView = (): React.JSX.Element => { ); } + if (isMultimodel) { + return ( +
+
+ +
+

Multimodel runtime capabilities

+

+ Provider support can differ by section. Plugins are shown only where the runtime + explicitly declares support. +

+
+
+
+ {providers.map((provider) => { + const statusTone = provider.authenticated + ? 'border-emerald-500/30 bg-emerald-500/5 text-emerald-300' + : provider.supported + ? 'border-amber-500/30 bg-amber-500/5 text-amber-300' + : 'border-border bg-surface-raised text-text-muted'; + const statusLabel = provider.authenticated + ? 'Connected' + : provider.supported + ? 'Needs setup' + : 'Unsupported'; + const pluginStatus = provider.capabilities.extensions.plugins.status; + + return ( +
+
+
+

{provider.displayName}

+

+ {provider.statusMessage ?? provider.backend?.label ?? 'Ready to configure'} +

+
+ + {statusLabel} + +
+
+ + Plugins: {pluginStatus === 'supported' ? 'supported' : 'limited'} + + + MCP: {provider.capabilities.extensions.mcp.status} + + + Skills: {provider.capabilities.extensions.skills.ownership} + +
+
+ ); + })} +
+
+ ); + } + return (
@@ -280,7 +349,8 @@ export const ExtensionStoreView = (): React.JSX.Element => { {!cliInstalled && (
- Claude CLI is required to install or uninstall extensions. Install it from Settings. + The configured runtime is required to install or uninstall extensions. Install or + repair it from the Dashboard.
)} {/* Active sessions warning */} diff --git a/src/renderer/components/extensions/apikeys/ApiKeysPanel.tsx b/src/renderer/components/extensions/apikeys/ApiKeysPanel.tsx index 951aecb0..a45b0e58 100644 --- a/src/renderer/components/extensions/apikeys/ApiKeysPanel.tsx +++ b/src/renderer/components/extensions/apikeys/ApiKeysPanel.tsx @@ -2,7 +2,7 @@ * ApiKeysPanel — grid of saved API keys with add button and empty state. */ -import { useEffect, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { Button } from '@renderer/components/ui/button'; import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip'; @@ -16,15 +16,17 @@ import { ApiKeyFormDialog } from './ApiKeyFormDialog'; import type { ApiKeyEntry } from '@shared/types/extensions'; export const ApiKeysPanel = (): React.JSX.Element => { - const { apiKeys, apiKeysLoading, apiKeysError, storageStatus, fetchStorageStatus } = useStore( - useShallow((s) => ({ - apiKeys: s.apiKeys, - apiKeysLoading: s.apiKeysLoading, - apiKeysError: s.apiKeysError, - storageStatus: s.apiKeyStorageStatus, - fetchStorageStatus: s.fetchApiKeyStorageStatus, - })) - ); + const { apiKeys, apiKeysLoading, apiKeysError, storageStatus, fetchStorageStatus, cliStatus } = + useStore( + useShallow((s) => ({ + apiKeys: s.apiKeys, + apiKeysLoading: s.apiKeysLoading, + apiKeysError: s.apiKeysError, + storageStatus: s.apiKeyStorageStatus, + fetchStorageStatus: s.fetchApiKeyStorageStatus, + cliStatus: s.cliStatus, + })) + ); const [dialogOpen, setDialogOpen] = useState(false); const [editingKey, setEditingKey] = useState(null); @@ -49,9 +51,82 @@ export const ApiKeysPanel = (): React.JSX.Element => { }; const isOsKeychain = storageStatus?.encryptionMethod === 'os-keychain'; + const providerKeyCards = useMemo(() => { + if (!cliStatus?.providers?.length) { + return []; + } + + return ( + [ + { + providerId: 'anthropic', + label: 'Anthropic runtime', + envVar: 'ANTHROPIC_API_KEY', + }, + { + providerId: 'codex', + label: 'Codex runtime', + envVar: 'OPENAI_API_KEY', + }, + ] as const + ).flatMap((item) => { + const provider = cliStatus.providers.find((entry) => entry.providerId === item.providerId); + if (!provider) { + return []; + } + + return [ + { + ...item, + authenticated: provider.authenticated, + apiKeyConfigured: provider.connection?.apiKeyConfigured ?? false, + sourceLabel: provider.connection?.apiKeySourceLabel ?? null, + statusMessage: provider.statusMessage ?? null, + }, + ]; + }); + }, [cliStatus]); return (
+ {providerKeyCards.length > 0 && ( +
+ {providerKeyCards.map((provider) => ( +
+
+
+

{provider.label}

+

{provider.envVar}

+
+ + {provider.authenticated + ? 'Connected' + : provider.apiKeyConfigured + ? 'Key configured' + : 'Key missing'} + +
+

+ {provider.sourceLabel + ? `Current source: ${provider.sourceLabel}.` + : 'No stored or environment key detected for this provider.'} + {provider.statusMessage ? ` ${provider.statusMessage}` : ''} +

+
+ ))} +
+ )} {/* Header row */}

diff --git a/src/renderer/components/extensions/common/InstallButton.tsx b/src/renderer/components/extensions/common/InstallButton.tsx index 2bc48112..78930ad7 100644 --- a/src/renderer/components/extensions/common/InstallButton.tsx +++ b/src/renderer/components/extensions/common/InstallButton.tsx @@ -24,6 +24,7 @@ interface InstallButtonProps { isInstalled: boolean; onInstall: () => void; onUninstall: () => void; + section?: 'plugins' | 'mcp'; disabled?: boolean; size?: 'sm' | 'default'; errorMessage?: string; @@ -34,6 +35,7 @@ export const InstallButton = ({ isInstalled, onInstall, onUninstall, + section = 'plugins', disabled, size = 'sm', errorMessage, @@ -48,6 +50,7 @@ export const InstallButton = ({ isInstalled, cliStatus, cliStatusLoading, + section, }); const isDisabled = disabled || Boolean(disableReason); const [lastAction, setLastAction] = useState<'install' | 'uninstall' | null>(null); diff --git a/src/renderer/components/extensions/mcp/McpServerCard.tsx b/src/renderer/components/extensions/mcp/McpServerCard.tsx index afae2142..4f58b343 100644 --- a/src/renderer/components/extensions/mcp/McpServerCard.tsx +++ b/src/renderer/components/extensions/mcp/McpServerCard.tsx @@ -258,6 +258,7 @@ export const McpServerCard = ({ installMcpServer({ registryId: server.id, diff --git a/src/renderer/components/extensions/mcp/McpServerDetailDialog.tsx b/src/renderer/components/extensions/mcp/McpServerDetailDialog.tsx index d16e0885..412be5a8 100644 --- a/src/renderer/components/extensions/mcp/McpServerDetailDialog.tsx +++ b/src/renderer/components/extensions/mcp/McpServerDetailDialog.tsx @@ -528,6 +528,7 @@ export const McpServerDetailDialog = ({ ({ browseCatalog: s.mcpBrowseCatalog, @@ -105,6 +106,7 @@ export const McpServersPanel = ({ mcpDiagnosticsError: s.mcpDiagnosticsError, mcpDiagnosticsLastCheckedAt: s.mcpDiagnosticsLastCheckedAt, runMcpDiagnostics: s.runMcpDiagnostics, + cliStatus: s.cliStatus, })) ); @@ -118,8 +120,8 @@ export const McpServersPanel = ({ }, [browseCatalog.length, browseError, browseLoading, mcpBrowse]); useEffect(() => { - void runMcpDiagnostics(); - }, [runMcpDiagnostics]); + void runMcpDiagnostics(projectPath ?? undefined); + }, [projectPath, runMcpDiagnostics]); // Fetch GitHub stars after catalog loads (fire-and-forget) useEffect(() => { @@ -185,6 +187,12 @@ export const McpServersPanel = ({ // Sort displayed servers const displayServers = useMemo(() => sortMcpServers(rawServers, mcpSort), [rawServers, mcpSort]); + const runtimeLabel = + cliStatus?.flavor === 'agent_teams_orchestrator' ? 'multimodel runtime' : 'Claude CLI'; + const diagnosticsCommand = + cliStatus?.flavor === 'agent_teams_orchestrator' + ? 'claude-multimodel mcp diagnose' + : 'claude mcp list'; // Find selected server (search in both lists to avoid losing selection during search toggle) const selectedServer = useMemo(() => { @@ -205,14 +213,12 @@ export const McpServersPanel = ({

MCP Health Status

{mcpDiagnosticsLoading ? ( - <> - Checking installed MCP servers via Claude CLI (claude mcp list) ... - + <>Checking installed MCP servers via {runtimeLabel} ... ) : mcpDiagnosticsLastCheckedAt ? ( `Last checked ${formatRelativeTime(new Date(mcpDiagnosticsLastCheckedAt).toISOString())}` ) : ( <> - Run diagnostics (claude mcp list) to verify installed MCP + Run diagnostics ({diagnosticsCommand}) to verify installed MCP connectivity. )} @@ -221,7 +227,7 @@ export const McpServersPanel = ({