diff --git a/src/renderer/components/extensions/mcp/McpServersPanel.tsx b/src/renderer/components/extensions/mcp/McpServersPanel.tsx index 46249a10..33419f5e 100644 --- a/src/renderer/components/extensions/mcp/McpServersPanel.tsx +++ b/src/renderer/components/extensions/mcp/McpServersPanel.tsx @@ -100,6 +100,7 @@ export const McpServersPanel = ({ mcpDiagnosticsLastCheckedAtFallback, runMcpDiagnostics, cliStatus, + cliStatusLoading, } = useStore( useShallow((s) => ({ browseCatalog: s.mcpBrowseCatalog, @@ -120,6 +121,7 @@ export const McpServersPanel = ({ mcpDiagnosticsLastCheckedAtFallback: s.mcpDiagnosticsLastCheckedAt, runMcpDiagnostics: s.runMcpDiagnostics, cliStatus: s.cliStatus, + cliStatusLoading: s.cliStatusLoading, })) ); const installedServers = @@ -144,9 +146,27 @@ export const McpServersPanel = ({ } }, [browseCatalog.length, browseError, browseLoading, mcpBrowse]); + const diagnosticsDisableReason = useMemo(() => { + if (cliStatusLoading) { + return 'Checking runtime status...'; + } + + if (cliStatus?.installed === false) { + if (cliStatus.binaryPath && cliStatus.launchError) { + return 'The configured runtime was found but failed to start. Open the Dashboard to repair or reinstall it.'; + } + return 'The configured runtime is required. Install or repair it from the Dashboard.'; + } + + return null; + }, [cliStatus, cliStatusLoading]); + useEffect(() => { + if (diagnosticsDisableReason) { + return; + } void runMcpDiagnostics(projectPath ?? undefined); - }, [projectPath, runMcpDiagnostics]); + }, [diagnosticsDisableReason, projectPath, runMcpDiagnostics]); // Fetch GitHub stars after catalog loads (fire-and-forget) useEffect(() => { @@ -240,6 +260,8 @@ export const McpServersPanel = ({

{mcpDiagnosticsLoading ? ( <>Checking installed MCP servers via {runtimeLabel} ... + ) : diagnosticsDisableReason ? ( + diagnosticsDisableReason ) : mcpDiagnosticsLastCheckedAt ? ( `Last checked ${formatRelativeTime(new Date(mcpDiagnosticsLastCheckedAt).toISOString())}` ) : ( @@ -251,7 +273,7 @@ export const McpServersPanel = ({ variant="outline" size="sm" onClick={() => void runMcpDiagnostics(projectPath ?? undefined)} - disabled={mcpDiagnosticsLoading} + disabled={mcpDiagnosticsLoading || Boolean(diagnosticsDisableReason)} className="whitespace-nowrap" > ) : ( -

- Waiting for {diagnosticsCommand} results... -

+

Waiting for diagnostics results...

)} )} diff --git a/test/renderer/components/extensions/mcp/McpServersPanel.test.ts b/test/renderer/components/extensions/mcp/McpServersPanel.test.ts index 8b86b574..0281c102 100644 --- a/test/renderer/components/extensions/mcp/McpServersPanel.test.ts +++ b/test/renderer/components/extensions/mcp/McpServersPanel.test.ts @@ -34,8 +34,12 @@ interface StoreState { mcpDiagnosticsLastCheckedAt: number | null; mcpDiagnosticsLastCheckedAtByProjectPath?: Record; runMcpDiagnostics: ReturnType; + cliStatusLoading: boolean; cliStatus?: { flavor?: 'claude' | 'agent_teams_orchestrator'; + installed?: boolean; + binaryPath?: string | null; + launchError?: string | null; }; } @@ -144,6 +148,7 @@ describe('McpServersPanel initial browse loading', () => { storeState.mcpDiagnosticsLastCheckedAt = null; storeState.mcpDiagnosticsLastCheckedAtByProjectPath = undefined; storeState.runMcpDiagnostics = vi.fn(); + storeState.cliStatusLoading = false; storeState.cliStatus = undefined; }); @@ -309,4 +314,79 @@ describe('McpServersPanel initial browse loading', () => { await Promise.resolve(); }); }); + + it('does not auto-run diagnostics when the configured runtime is unavailable', async () => { + storeState.cliStatus = { + flavor: 'agent_teams_orchestrator', + installed: false, + binaryPath: null, + launchError: null, + }; + + const host = document.createElement('div'); + document.body.appendChild(host); + const root = createRoot(host); + + await act(async () => { + root.render( + React.createElement(McpServersPanel, { + projectPath: null, + mcpSearchQuery: '', + mcpSearch: vi.fn(), + mcpSearchResults: [], + mcpSearchLoading: false, + mcpSearchWarnings: [], + selectedMcpServerId: null, + setSelectedMcpServerId: vi.fn(), + }) + ); + await Promise.resolve(); + }); + + expect(storeState.runMcpDiagnostics).not.toHaveBeenCalled(); + expect(host.textContent).toContain( + 'The configured runtime is required. Install or repair it from the Dashboard.' + ); + const checkStatusButton = Array.from(host.querySelectorAll('button')).find((button) => + button.textContent?.includes('Check Status') + ); + expect(checkStatusButton).toBeDefined(); + expect((checkStatusButton as HTMLButtonElement).disabled).toBe(true); + + await act(async () => { + root.unmount(); + await Promise.resolve(); + }); + }); + + it('renders provider-neutral waiting copy while diagnostics are still running', async () => { + storeState.mcpDiagnosticsLoading = true; + + const host = document.createElement('div'); + document.body.appendChild(host); + const root = createRoot(host); + + await act(async () => { + root.render( + React.createElement(McpServersPanel, { + projectPath: null, + mcpSearchQuery: '', + mcpSearch: vi.fn(), + mcpSearchResults: [], + mcpSearchLoading: false, + mcpSearchWarnings: [], + selectedMcpServerId: null, + setSelectedMcpServerId: vi.fn(), + }) + ); + await Promise.resolve(); + }); + + expect(host.textContent).toContain('Waiting for diagnostics results...'); + + await act(async () => { + root.unmount(); + await Promise.resolve(); + }); + }); });