From c4772a097e5c3a10e177a086c2b6d46b275a6f59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Wed, 22 Apr 2026 15:53:27 +0800 Subject: [PATCH] Make MCP reconnect progress visible in the detail view The reconnect action could succeed, but the detail and edit views gave almost no page-level feedback beyond the button spinner. Mirror the reconnect lifecycle into the visible status surfaces so users can tell immediately that the request is in flight and whether it settled. Constraint: Reconnect feedback must be visible without waiting for the list row to refresh Rejected: Rely on toast only | too late and too easy to miss while staying on the detail page Rejected: Add a separate transient banner | duplicates the existing status surfaces instead of updating them Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep reconnect state reflected in the page-level status badge and status field, not just the action button Tested: cd desktop && bun run test -- mcpSettings.test.tsx; cd desktop && bun x tsc --noEmit --ignoreDeprecations 5.0 Not-tested: Manual desktop click-through against a live MCP reconnect --- desktop/src/__tests__/mcpSettings.test.tsx | 50 ++++++++++++++++++++++ desktop/src/pages/McpSettings.tsx | 43 +++++++++++++++++-- 2 files changed, 90 insertions(+), 3 deletions(-) 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 && (