Skip to content

Commit 8e11744

Browse files
committed
fix(web): remove connector reconnect continuation
1 parent 59d0e43 commit 8e11744

8 files changed

Lines changed: 35 additions & 172 deletions

File tree

packages/web/src/ee/features/chat/components/chatThread/chatThread.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -311,9 +311,6 @@ export const ChatThread = ({
311311
messages,
312312
isTurnInProgress,
313313
addToolApprovalResponse,
314-
sendMessage,
315-
selectedSearchScopes,
316-
disabledMcpServerIds,
317314
});
318315

319316
useEffect(() => {

packages/web/src/ee/features/chat/components/chatThread/mcpFailedServersBanner.test.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,7 @@ const createContext = (
2020
},
2121
},
2222
isReconnectAllowed,
23-
isContinueAllowed: false,
2423
reconnect,
25-
continueAfterReconnect: vi.fn(),
2624
});
2725

2826
const renderBanner = (context: McpReconnectContextValue) => render(

packages/web/src/ee/features/chat/components/chatThread/mcpReconnectBanner.test.tsx

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,7 @@ const createReconnectContext = (
2424
): McpReconnectContextValue => ({
2525
reconnectStates: { [state.serverId]: state },
2626
isReconnectAllowed: true,
27-
isContinueAllowed: false,
2827
reconnect: vi.fn(),
29-
continueAfterReconnect: vi.fn(),
3028
...overrides,
3129
});
3230

@@ -68,35 +66,18 @@ describe('McpReconnectBanner', () => {
6866
expect(button.disabled).toBe(true);
6967
});
7068

71-
test('shows the continue action after reconnecting', () => {
72-
const contextValue = createReconnectContext(
73-
createReconnectState({ status: 'reconnected' }),
74-
{ isContinueAllowed: true },
75-
);
76-
renderBanner(contextValue);
77-
78-
expect(screen.getByRole('status')).toBeTruthy();
79-
expect(screen.getByText('Linear reconnected')).toBeTruthy();
80-
81-
fireEvent.click(screen.getByRole('button', { name: 'Continue' }));
82-
expect(contextValue.continueAfterReconnect).toHaveBeenCalledWith('server-1');
83-
});
84-
85-
test('does not offer Continue when automatic continuation is unavailable', () => {
69+
test('does not render a success banner after reconnecting', () => {
8670
const contextValue = createReconnectContext(createReconnectState({ status: 'reconnected' }));
87-
renderBanner(contextValue);
71+
const { container } = renderBanner(contextValue);
8872

89-
expect(screen.getByText('Connection restored.')).toBeTruthy();
90-
expect(screen.queryByRole('button', { name: 'Continue' })).toBeNull();
73+
expect(container.childElementCount).toBe(0);
9174
});
9275

9376
test('does not render without a reconnect failure', () => {
9477
const contextValue: McpReconnectContextValue = {
9578
reconnectStates: {},
9679
isReconnectAllowed: true,
97-
isContinueAllowed: false,
9880
reconnect: vi.fn(),
99-
continueAfterReconnect: vi.fn(),
10081
};
10182
const { container } = renderBanner(contextValue);
10283

packages/web/src/ee/features/chat/components/chatThread/mcpReconnectBanner.tsx

Lines changed: 2 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22

33
import { Button } from '@/components/ui/button';
44
import { useMcpReconnect } from '@/ee/features/chat/mcpReconnectContext';
5-
import { AlertCircle, CheckCircle, Loader2 } from 'lucide-react';
5+
import { AlertCircle, Loader2 } from 'lucide-react';
66

77
export const McpReconnectBanner = () => {
88
const reconnectContext = useMcpReconnect();
99
const reconnectStates = Object.values(reconnectContext?.reconnectStates ?? {})
10-
.filter((state) => state.source !== 'tool-load');
10+
.filter((state) => state.source !== 'tool-load' && state.status !== 'reconnected');
1111

1212
if (!reconnectContext || reconnectStates.length === 0) {
1313
return null;
@@ -16,41 +16,6 @@ export const McpReconnectBanner = () => {
1616
return (
1717
<div>
1818
{reconnectStates.map((state) => {
19-
if (state.status === 'reconnected') {
20-
return (
21-
<div
22-
key={state.serverId}
23-
role="status"
24-
className="border-b border-green-200 bg-green-50 dark:border-green-800 dark:bg-green-950/20"
25-
>
26-
<div className="mx-auto flex max-w-3xl flex-col gap-3 px-4 py-3 sm:flex-row sm:items-center sm:justify-center sm:gap-6">
27-
<div className="flex min-w-0 items-start gap-2">
28-
<CheckCircle className="mt-0.5 h-4 w-4 flex-shrink-0 text-green-600 dark:text-green-400" />
29-
<div>
30-
<p className="text-sm font-medium text-green-800 dark:text-green-200">
31-
{state.serverName} reconnected
32-
</p>
33-
<p className="text-sm text-green-700 dark:text-green-300">
34-
{reconnectContext.isContinueAllowed
35-
? 'Continue to retry your request.'
36-
: 'Connection restored.'}
37-
</p>
38-
</div>
39-
</div>
40-
{reconnectContext.isContinueAllowed && (
41-
<Button
42-
size="sm"
43-
className="ml-6 self-start sm:ml-0 sm:self-auto"
44-
onClick={() => reconnectContext.continueAfterReconnect(state.serverId)}
45-
>
46-
Continue
47-
</Button>
48-
)}
49-
</div>
50-
</div>
51-
);
52-
}
53-
5419
const isReconnecting = state.status === 'reconnecting';
5520
return (
5621
<div

packages/web/src/ee/features/chat/components/chatThread/tools/mcpToolComponent.test.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,7 @@ const createReconnectContext = (
7171
): McpReconnectContextValue => ({
7272
reconnectStates: { [state.serverId]: state },
7373
isReconnectAllowed: true,
74-
isContinueAllowed: false,
7574
reconnect: vi.fn(),
76-
continueAfterReconnect: vi.fn(),
7775
...overrides,
7876
});
7977

packages/web/src/ee/features/chat/components/chatThread/useMcpReconnectController.test.tsx

Lines changed: 22 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { ReactNode } from 'react';
55
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
66
import { MCP_RECONNECT_SESSION_STORAGE_KEY } from '@/features/chat/constants';
77
import { SBChatMessage } from '@/features/chat/types';
8-
import { getUserMessageText } from '@/features/chat/utils';
98
import { useMcpReconnectController } from './useMcpReconnectController';
109

1110
const mocks = vi.hoisted(() => ({
@@ -44,16 +43,12 @@ const createWrapper = () => {
4443
};
4544

4645
const addToolApprovalResponse = vi.fn();
47-
const sendMessage = vi.fn();
4846

4947
const renderController = (initialProps: HookProps) =>
5048
renderHook(
5149
(props: HookProps) => useMcpReconnectController({
5250
...props,
5351
addToolApprovalResponse,
54-
sendMessage,
55-
selectedSearchScopes: [],
56-
disabledMcpServerIds: [],
5752
}),
5853
{ initialProps, wrapper: createWrapper() },
5954
);
@@ -189,6 +184,28 @@ describe('useMcpReconnectController', () => {
189184
expect(mocks.connectMcpToAsk).not.toHaveBeenCalled();
190185
});
191186

187+
test('shows a success toast when reconnection completes without an OAuth redirect', async () => {
188+
mocks.connectMcpToAsk.mockResolvedValue({ authorizationUrl: null });
189+
mocks.getMcpServersWithStatus.mockResolvedValue([
190+
{ id: 'server-1', name: 'Linear', isConnected: true, isAuthExpired: false },
191+
]);
192+
const { result } = renderController({ status: 'ready', messages: [], isTurnInProgress: false });
193+
194+
act(() => {
195+
result.current.onAuthRequired(AUTH_FAILURE);
196+
});
197+
198+
await act(async () => {
199+
result.current.contextValue.reconnect('server-1');
200+
});
201+
202+
await waitFor(() => {
203+
expect(mocks.toast).toHaveBeenCalledWith({
204+
description: 'Successfully reconnected to Linear.',
205+
});
206+
});
207+
});
208+
192209
test('tracks a failed tool load without treating it as a failed tool call', () => {
193210
const { result } = renderController({ status: 'ready', messages: [], isTurnInProgress: false });
194211

@@ -202,7 +219,6 @@ describe('useMcpReconnectController', () => {
202219
source: 'tool-load',
203220
status: 'authentication-required',
204221
});
205-
expect(result.current.contextValue.isContinueAllowed).toBe(false);
206222
expect(addToolApprovalResponse).not.toHaveBeenCalled();
207223
});
208224

@@ -224,7 +240,6 @@ describe('useMcpReconnectController', () => {
224240
expect(result.current.contextValue.reconnectStates['server-1']?.status).toBe('reconnected');
225241
});
226242
expect(window.sessionStorage.getItem(MCP_RECONNECT_SESSION_STORAGE_KEY)).toBeNull();
227-
expect(result.current.contextValue.isContinueAllowed).toBe(true);
228243
});
229244

230245
test('restores the source of a failed tool-load reconnect after OAuth', async () => {
@@ -245,7 +260,6 @@ describe('useMcpReconnectController', () => {
245260
expect(result.current.contextValue.reconnectStates['server-1']?.status).toBe('reconnected');
246261
});
247262
expect(result.current.contextValue.reconnectStates['server-1']?.source).toBe('tool-load');
248-
expect(result.current.contextValue.isContinueAllowed).toBe(false);
249263
});
250264

251265
test('falls back to authentication-required when the OAuth return did not reconnect the connector', async () => {
@@ -265,53 +279,5 @@ describe('useMcpReconnectController', () => {
265279
await waitFor(() => {
266280
expect(result.current.contextValue.reconnectStates['server-1']?.status).toBe('authentication-required');
267281
});
268-
expect(result.current.contextValue.isContinueAllowed).toBe(false);
269-
});
270-
271-
test('Continue sends a visible user turn naming the reconnected connector and resets the state', async () => {
272-
window.sessionStorage.setItem(MCP_RECONNECT_SESSION_STORAGE_KEY, JSON.stringify({
273-
serverId: 'server-1',
274-
serverName: 'Linear',
275-
toolCallId: 'tool-call-1',
276-
returnTo: '/chat/abc123',
277-
createdAt: Date.now(),
278-
}));
279-
mocks.getMcpServersWithStatus.mockResolvedValue([
280-
{ id: 'server-1', name: 'Linear', isConnected: true, isAuthExpired: false },
281-
]);
282-
283-
const { result } = renderController({ status: 'ready', messages: [], isTurnInProgress: false });
284-
await waitFor(() => {
285-
expect(result.current.contextValue.isContinueAllowed).toBe(true);
286-
});
287-
288-
act(() => {
289-
result.current.contextValue.continueAfterReconnect('server-1');
290-
});
291-
292-
expect(sendMessage).toHaveBeenCalledTimes(1);
293-
const sentMessage = sendMessage.mock.calls[0][0];
294-
expect(sentMessage.role).toBe('user');
295-
expect(getUserMessageText(sentMessage as SBChatMessage)).toBe(
296-
'Continue the previous request now that Linear is reconnected. Do not repeat operations that already completed.'
297-
);
298-
expect(result.current.contextValue.reconnectStates).toEqual({});
299-
});
300-
301-
test('Continue is unavailable when more than one connector failed authentication', () => {
302-
const { result, rerender } = renderController({ status: 'streaming', messages: [], isTurnInProgress: true });
303-
304-
act(() => {
305-
result.current.onAuthRequired(AUTH_FAILURE);
306-
result.current.onAuthRequired({ serverId: 'server-2', serverName: 'Jira', toolCallId: 'tool-call-3' });
307-
});
308-
rerender({ status: 'ready', messages: [], isTurnInProgress: false });
309-
310-
expect(result.current.contextValue.isContinueAllowed).toBe(false);
311-
312-
act(() => {
313-
result.current.contextValue.continueAfterReconnect('server-1');
314-
});
315-
expect(sendMessage).not.toHaveBeenCalled();
316282
});
317283
});

packages/web/src/ee/features/chat/components/chatThread/useMcpReconnectController.ts

Lines changed: 8 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ import {
1414
consumeMcpPendingReconnectForPath,
1515
saveMcpPendingReconnect,
1616
} from '@/features/chat/mcpReconnect';
17-
import { CreateUIMessage, ChatStatus, ChatAddToolApproveResponseFunction } from 'ai';
18-
import { SBChatMessage, SearchScope } from '@/features/chat/types';
19-
import { createUIMessage, getLastStepParts, isSBChatToolPart } from '@/features/chat/utils';
17+
import { ChatStatus, ChatAddToolApproveResponseFunction } from 'ai';
18+
import { SBChatMessage } from '@/features/chat/types';
19+
import { getLastStepParts, isSBChatToolPart } from '@/features/chat/utils';
2020
import { isServiceError } from '@/lib/utils';
2121
import { useQueryClient } from '@tanstack/react-query';
2222
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
@@ -37,25 +37,19 @@ interface UseMcpReconnectControllerOptions {
3737
messages: SBChatMessage[];
3838
isTurnInProgress: boolean;
3939
addToolApprovalResponse: ChatAddToolApproveResponseFunction;
40-
sendMessage: (message: CreateUIMessage<SBChatMessage>) => void;
41-
selectedSearchScopes: SearchScope[];
42-
disabledMcpServerIds: string[];
4340
}
4441

4542
// Orchestrates the client side of MCP connector reauthentication for a chat
4643
// thread: tracks per-connector reconnect state from tool-call authentication
4744
// failures and tool-load failures, automatically denies tool approvals still
4845
// pending in an interrupted response, gates the Reconnect action until the
4946
// response has settled, restores pending reconnect metadata after the OAuth
50-
// round trip, and sends the follow-up user turn on Continue when applicable.
47+
// round trip, and confirms that the connector is usable again.
5148
export function useMcpReconnectController({
5249
status,
5350
messages,
5451
isTurnInProgress,
5552
addToolApprovalResponse,
56-
sendMessage,
57-
selectedSearchScopes,
58-
disabledMcpServerIds,
5953
}: UseMcpReconnectControllerOptions): {
6054
contextValue: McpReconnectContextValue;
6155
onAuthRequired: (data: McpAuthRequiredData) => void;
@@ -168,7 +162,8 @@ export function useMcpReconnectController({
168162
// Restore pending reconnect metadata after returning from the OAuth
169163
// redirect, then confirm the connector actually reconnected via the
170164
// status endpoint. Runs once per mount; reads only sessionStorage, so it
171-
// does not race the OAuth status toast's query-parameter cleanup.
165+
// does not race the OAuth status toast's query-parameter cleanup. The
166+
// app-level OAuth status toast owns the success feedback for this path.
172167
useEffect(() => {
173168
if (hasRestoredPendingReconnect.current) {
174169
return;
@@ -278,6 +273,7 @@ export function useMcpReconnectController({
278273

279274
if (server?.isConnected && !server.isAuthExpired) {
280275
setReconnectStatus(serverId, 'reconnected');
276+
toast({ description: `Successfully reconnected to ${state.serverName}.` });
281277
} else {
282278
setReconnectStatus(serverId, 'authentication-required');
283279
toast({
@@ -287,45 +283,11 @@ export function useMcpReconnectController({
287283
}
288284
}, [queryClient, setReconnectStatus, toast]);
289285

290-
const stateList = useMemo(
291-
() => Object.values(reconnectStates).filter((state) => state.source !== 'tool-load'),
292-
[reconnectStates],
293-
);
294-
// Continue is only supported when exactly one connector failed
295-
// authentication in the response, and it has been reconnected.
296-
const isContinueAllowed =
297-
isReconnectAllowed &&
298-
stateList.length === 1 &&
299-
stateList[0].status === 'reconnected';
300-
const isContinueAllowedRef = useRef(isContinueAllowed);
301-
useEffect(() => {
302-
isContinueAllowedRef.current = isContinueAllowed;
303-
}, [isContinueAllowed]);
304-
305-
const continueAfterReconnect = useCallback((serverId: string) => {
306-
const state = reconnectStatesRef.current[serverId];
307-
if (!state || state.status !== 'reconnected' || !isContinueAllowedRef.current) {
308-
return;
309-
}
310-
311-
sendMessage(createUIMessage(
312-
`Continue the previous request now that ${state.serverName} is reconnected. Do not repeat operations that already completed.`,
313-
[],
314-
selectedSearchScopes,
315-
disabledMcpServerIds,
316-
));
317-
318-
setReconnectStates({});
319-
deniedApprovalIdsRef.current.clear();
320-
}, [sendMessage, selectedSearchScopes, disabledMcpServerIds]);
321-
322286
const contextValue = useMemo<McpReconnectContextValue>(() => ({
323287
reconnectStates,
324288
isReconnectAllowed,
325-
isContinueAllowed,
326289
reconnect,
327-
continueAfterReconnect,
328-
}), [reconnectStates, isReconnectAllowed, isContinueAllowed, reconnect, continueAfterReconnect]);
290+
}), [reconnectStates, isReconnectAllowed, reconnect]);
329291

330292
return { contextValue, onAuthRequired, onServerLoadFailed };
331293
}

packages/web/src/ee/features/chat/mcpReconnectContext.tsx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,7 @@ export interface McpReconnectContextValue {
2525
// (started tool calls finished, pending approvals denied, the final
2626
// tools-disabled step streamed and persisted).
2727
isReconnectAllowed: boolean;
28-
// True only in the supported case: exactly one failed connector, and it
29-
// has been reconnected.
30-
isContinueAllowed: boolean;
3128
reconnect: (serverId: string) => void;
32-
continueAfterReconnect: (serverId: string) => void;
3329
}
3430

3531
export const McpReconnectContext = createContext<McpReconnectContextValue | undefined>(undefined);

0 commit comments

Comments
 (0)