diff --git a/desktop/src/__tests__/mcpSettings.test.tsx b/desktop/src/__tests__/mcpSettings.test.tsx index f1e3df51..418dd5c3 100644 --- a/desktop/src/__tests__/mcpSettings.test.tsx +++ b/desktop/src/__tests__/mcpSettings.test.tsx @@ -224,4 +224,54 @@ describe('McpSettings', () => { expect(toggleServer).toHaveBeenCalledWith(server, '/workspace/project') }) + + it('shows reconnecting status immediately in the detail view', async () => { + let resolveReconnect: ((value: typeof server) => void) | null = null + const server = { + name: 'plugin:telegram:telegram', + scope: 'dynamic', + transport: 'stdio', + enabled: true, + status: 'failed' as 'connected' | 'needs-auth' | 'failed' | 'disabled' | 'checking', + statusLabel: 'Unavailable', + statusDetail: 'Timed out' as string | undefined, + configLocation: '/tmp/config', + summary: 'bun run start', + canEdit: false, + canRemove: false, + canReconnect: true, + canToggle: true, + config: { type: 'stdio' as const, command: 'bun', args: ['run', 'start'], env: {} }, + } + const reconnectServer = vi.fn().mockImplementation(() => new Promise((resolve) => { + resolveReconnect = resolve + })) + + useMcpStore.setState({ + servers: [server], + reconnectServer, + }) + + render() + + await act(async () => { + fireEvent.click(screen.getByRole('button', { name: 'Open plugin:telegram:telegram' })) + }) + + await act(async () => { + fireEvent.click(screen.getByRole('button', { name: /reconnect/i })) + }) + + expect(screen.getAllByText('Reconnecting...').length).toBeGreaterThan(0) + expect(reconnectServer).toHaveBeenCalledWith(server, '/workspace/project') + + await act(async () => { + resolveReconnect?.({ + ...server, + status: 'connected', + statusLabel: 'Connected', + statusDetail: undefined, + }) + }) + }) }) diff --git a/desktop/src/pages/McpSettings.tsx b/desktop/src/pages/McpSettings.tsx index a1087c36..a808e88b 100644 --- a/desktop/src/pages/McpSettings.tsx +++ b/desktop/src/pages/McpSettings.tsx @@ -229,6 +229,14 @@ function scopeLabel(server: McpServerRecord, t: ReturnType + {server.statusLabel} + + ) +} + function getServerIdentityKey(server: Pick) { if (server.scope === 'local' || server.scope === 'project') { return `${server.scope}:${server.projectPath ?? ''}:${server.name}` @@ -358,9 +366,7 @@ function ServerRow({
{server.name}
- - {server.statusLabel} - +
@@ -516,7 +522,19 @@ export function McpSettings() { } const handleReconnect = async (server: McpServerRecord) => { + const optimistic = { + ...server, + status: 'checking' as const, + statusLabel: t('status.reconnecting'), + statusDetail: undefined, + } + setBusyServerName(server.name) + setView((current) => { + if (current.type !== 'details' && current.type !== 'edit') return current + if (getServerIdentityKey(current.server) !== getServerIdentityKey(server)) return current + return { ...current, server: optimistic } + }) try { const updated = await reconnectServer(server, resolveOperationCwd(server)) addToast({ @@ -528,6 +546,11 @@ export function McpSettings() { if (view.type === 'edit') setView({ type: 'edit', server: updated }) if (view.type === 'details') setView({ type: 'details', server: updated }) } catch (error) { + setView((current) => { + if (current.type !== 'details' && current.type !== 'edit') return current + if (getServerIdentityKey(current.server) !== getServerIdentityKey(server)) return current + return { ...current, server } + }) addToast({ type: 'error', message: error instanceof Error ? error.message : t('settings.mcp.toast.reconnectFailed'), @@ -669,6 +692,12 @@ export function McpSettings() {

{server.name}

{server.summary}

+
+ + {server.statusDetail && ( + {server.statusDetail} + )} +
{server.canReconnect && (