mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-17 13:13:35 +08:00
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
This commit is contained in:
parent
49e1367659
commit
c4772a097e
@ -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<typeof server>((resolve) => {
|
||||
resolveReconnect = resolve
|
||||
}))
|
||||
|
||||
useMcpStore.setState({
|
||||
servers: [server],
|
||||
reconnectServer,
|
||||
})
|
||||
|
||||
render(<McpSettings />)
|
||||
|
||||
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,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -229,6 +229,14 @@ function scopeLabel(server: McpServerRecord, t: ReturnType<typeof useTranslation
|
||||
return t(`settings.mcp.scope.${group}`)
|
||||
}
|
||||
|
||||
function StatusBadge({ server }: { server: McpServerRecord }) {
|
||||
return (
|
||||
<span className={`inline-flex items-center rounded-full border px-2.5 py-1 text-[11px] font-semibold ${STATUS_TONE[server.status]}`}>
|
||||
{server.statusLabel}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function getServerIdentityKey(server: Pick<McpServerRecord, 'name' | 'scope' | 'projectPath'>) {
|
||||
if (server.scope === 'local' || server.scope === 'project') {
|
||||
return `${server.scope}:${server.projectPath ?? ''}:${server.name}`
|
||||
@ -358,9 +366,7 @@ function ServerRow({
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-3 mb-2 min-w-0">
|
||||
<div className="text-[1.05rem] font-semibold text-[var(--color-text-primary)] truncate">{server.name}</div>
|
||||
<span className={`inline-flex items-center rounded-full border px-2.5 py-1 text-[11px] font-semibold ${STATUS_TONE[server.status]}`}>
|
||||
{server.statusLabel}
|
||||
</span>
|
||||
<StatusBadge server={server} />
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2 text-xs text-[var(--color-text-tertiary)]">
|
||||
<span className="rounded-full bg-[var(--color-surface-hover)] px-2 py-1 font-medium text-[var(--color-text-secondary)]">
|
||||
@ -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() {
|
||||
<div>
|
||||
<h2 className="text-[2.2rem] font-semibold tracking-[-0.03em] text-[var(--color-text-primary)]">{server.name}</h2>
|
||||
<p className="mt-3 text-base text-[var(--color-text-secondary)]">{server.summary}</p>
|
||||
<div className="mt-4 flex flex-wrap items-center gap-3">
|
||||
<StatusBadge server={server} />
|
||||
{server.statusDetail && (
|
||||
<span className="text-sm text-[var(--color-text-tertiary)]">{server.statusDetail}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{server.canReconnect && (
|
||||
<Button variant="secondary" onClick={() => handleReconnect(server)} loading={busyServerName === server.name}>
|
||||
@ -724,6 +753,14 @@ export function McpSettings() {
|
||||
<p className="mt-3 text-base text-[var(--color-text-secondary)]">
|
||||
{editing ? t('settings.mcp.form.editHint') : t('settings.mcp.form.createHint')}
|
||||
</p>
|
||||
{editing && targetServer && (
|
||||
<div className="mt-4 flex flex-wrap items-center gap-3">
|
||||
<StatusBadge server={targetServer} />
|
||||
{targetServer.statusDetail && (
|
||||
<span className="text-sm text-[var(--color-text-tertiary)]">{targetServer.statusDetail}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user