Skip to content

Commit f104e77

Browse files
committed
Prompt anonymous users to login if they want to use connectors in Ask Sourcebot
1 parent 358d481 commit f104e77

7 files changed

Lines changed: 150 additions & 82 deletions

File tree

packages/web/src/app/(app)/askgh/[owner]/[repo]/components/landingPage.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ export const LandingPage = ({
9595
onContextSelectorOpenChanged={setIsContextSelectorOpen}
9696
disabledMcpServerIds={disabledMcpServerIds}
9797
onDisabledMcpServerIdsChange={setDisabledMcpServerIds}
98+
isAuthenticated={isAuthenticated}
9899
/>
99100
<SearchModeSelector
100101
searchMode="agentic"

packages/web/src/app/(app)/chat/components/landingPageChatBox.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export const LandingPageChatBox = ({
6262
onContextSelectorOpenChanged={setIsContextSelectorOpen}
6363
disabledMcpServerIds={disabledMcpServerIds}
6464
onDisabledMcpServerIdsChange={setDisabledMcpServerIds}
65+
isAuthenticated={isAuthenticated}
6566
/>
6667
<SearchModeSelector
6768
searchMode="agentic"

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,7 @@ export const ChatThread = ({
496496
onContextSelectorOpenChanged={setIsContextSelectorOpen}
497497
disabledMcpServerIds={disabledMcpServerIds}
498498
onDisabledMcpServerIdsChange={onDisabledMcpServerIdsChange}
499+
isAuthenticated={isAuthenticated}
499500
/>
500501
</div>
501502
</CustomSlateEditor>

packages/web/src/ee/features/chat/mcp/components/connectorsMenu.test.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { describe, expect, test } from 'vitest';
2-
import { splitMcpServersForChatMenu } from './connectorsMenu';
2+
import { ErrorCode } from '@/lib/errorCodes';
3+
import { McpServersLoadError, shouldRetryMcpServersLoad, splitMcpServersForChatMenu } from './connectorsMenu';
34

45
describe('splitMcpServersForChatMenu', () => {
56
test('keeps connected and expired servers separate from connectable approved servers', () => {
@@ -15,3 +16,23 @@ describe('splitMcpServersForChatMenu', () => {
1516
expect(connectableServers.map((server) => server.id)).toEqual(['approved']);
1617
});
1718
});
19+
20+
describe('shouldRetryMcpServersLoad', () => {
21+
test('does not retry authentication failures', () => {
22+
const error = new McpServersLoadError({
23+
statusCode: 401,
24+
errorCode: ErrorCode.NOT_AUTHENTICATED,
25+
message: 'Not authenticated',
26+
});
27+
28+
expect(shouldRetryMcpServersLoad(0, error)).toBe(false);
29+
});
30+
31+
test('retries other failures up to the default react-query cap', () => {
32+
const error = new Error('network down');
33+
34+
expect(shouldRetryMcpServersLoad(0, error)).toBe(true);
35+
expect(shouldRetryMcpServersLoad(2, error)).toBe(true);
36+
expect(shouldRetryMcpServersLoad(3, error)).toBe(false);
37+
});
38+
});

packages/web/src/ee/features/chat/mcp/components/connectorsMenu.tsx

Lines changed: 121 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,13 @@ import { connectMcpToAsk, getMcpServersWithStatus } from "@/app/api/(client)/cli
1616
import { useToast } from "@/components/hooks/use-toast";
1717
import { McpFavicon } from "@/ee/features/chat/mcp/components/mcpFavicon";
1818
import { mcpQueryKeys } from "@/ee/features/chat/mcp/queryKeys";
19+
import { ErrorCode } from "@/lib/errorCodes";
20+
import type { ServiceError } from "@/lib/serviceError";
1921
import { isServiceError } from "@/lib/utils";
2022
import { useQuery, useQueryClient } from "@tanstack/react-query";
21-
import { AlertTriangleIcon, CableIcon, Loader2Icon, PlusCircleIcon, PlusIcon, RefreshCwIcon, SettingsIcon } from "lucide-react";
22-
import { useRouter } from "next/navigation";
23+
import { AlertTriangleIcon, CableIcon, Loader2Icon, LogInIcon, PlusCircleIcon, PlusIcon, RefreshCwIcon, SettingsIcon } from "lucide-react";
24+
import Link from "next/link";
25+
import { usePathname, useRouter } from "next/navigation";
2326
import { useEffect, useRef, useState } from "react";
2427
import { useSlate } from "slate-react";
2528
import { Editor } from "slate";
@@ -39,13 +42,28 @@ interface ConnectorsMenuProps {
3942
onSelectedSearchScopesChange: (items: SearchScope[]) => void;
4043
disabledMcpServerIds: string[];
4144
onDisabledMcpServerIdsChange: (ids: string[]) => void;
45+
isAuthenticated: boolean;
4246
}
4347

4448
interface ChatMenuMcpServer {
4549
isConnected: boolean;
4650
isAuthExpired: boolean;
4751
}
4852

53+
export class McpServersLoadError extends Error {
54+
constructor(public readonly serviceError: ServiceError) {
55+
super("Failed to load connectors");
56+
}
57+
}
58+
59+
export function shouldRetryMcpServersLoad(failureCount: number, error: Error) {
60+
if (error instanceof McpServersLoadError && error.serviceError.errorCode === ErrorCode.NOT_AUTHENTICATED) {
61+
return false;
62+
}
63+
64+
return failureCount < 3;
65+
}
66+
4967
export function splitMcpServersForChatMenu<T extends ChatMenuMcpServer>(servers: T[]) {
5068
return {
5169
connectedServers: servers.filter((server) => server.isConnected || server.isAuthExpired),
@@ -68,25 +86,30 @@ export const ConnectorsMenu = ({
6886
onSelectedSearchScopesChange,
6987
disabledMcpServerIds,
7088
onDisabledMcpServerIdsChange,
89+
isAuthenticated,
7190
}: ConnectorsMenuProps) => {
7291
const [connectingServerId, setConnectingServerId] = useState<string | null>(null);
7392
const editor = useSlate();
7493
const hasRestoredMcpOAuthDraft = useRef(false);
7594
const isMountedRef = useRef(false);
7695
const queryClient = useQueryClient();
7796
const router = useRouter();
97+
const pathname = usePathname();
7898
const { toast } = useToast();
7999
const isOwner = useRole() === OrgRole.OWNER;
100+
const loginHref = `/login?callbackUrl=${encodeURIComponent(pathname)}`;
80101

81-
const { data: servers = [], isError, isLoading, refetch } = useQuery({
102+
const { data: servers = [], error, isError, isLoading, refetch } = useQuery({
82103
queryKey: mcpQueryKeys.serversWithStatus,
83104
queryFn: async () => {
84105
const result = await getMcpServersWithStatus();
85106
if (isServiceError(result)) {
86-
throw new Error("Failed to load connectors");
107+
throw new McpServersLoadError(result);
87108
}
88109
return result;
89110
},
111+
retry: shouldRetryMcpServersLoad,
112+
enabled: isAuthenticated,
90113
});
91114

92115
useEffect(() => {
@@ -193,6 +216,8 @@ export const ConnectorsMenu = ({
193216

194217
const { connectedServers, connectableServers } = splitMcpServersForChatMenu(servers);
195218
const hasServers = connectedServers.length > 0 || connectableServers.length > 0;
219+
const isAuthenticationError = error instanceof McpServersLoadError && error.serviceError.errorCode === ErrorCode.NOT_AUTHENTICATED;
220+
const shouldShowLoginConnectorItem = !isAuthenticated || isAuthenticationError;
196221

197222
return (
198223
<DropdownMenu>
@@ -212,93 +237,108 @@ export const ConnectorsMenu = ({
212237
Connectors
213238
</DropdownMenuSubTrigger>
214239
<DropdownMenuSubContent className="w-56">
215-
{isError && !hasServers ? (
216-
<DropdownMenuItem
217-
onSelect={(e) => {
218-
e.preventDefault();
219-
refetch();
220-
}}
221-
className="gap-2 text-destructive"
222-
>
223-
<RefreshCwIcon className="w-4 h-4" />
224-
Failed to load. Retry?
225-
</DropdownMenuItem>
226-
) : isLoading ? (
227-
<DropdownMenuItem disabled>
228-
Loading connectors...
229-
</DropdownMenuItem>
230-
) : !hasServers ? (
231-
<DropdownMenuItem disabled>
232-
No connectors available
233-
</DropdownMenuItem>
234-
) : (
240+
{!shouldShowLoginConnectorItem && (
235241
<>
236-
{connectedServers.map((server) => {
237-
const isEnabled = !server.isAuthExpired && !disabledMcpServerIds.includes(server.id);
238-
return (
239-
<DropdownMenuItem
240-
key={server.id}
241-
onSelect={(e) => e.preventDefault()}
242-
disabled={server.isAuthExpired}
243-
className="flex items-center justify-between gap-2"
244-
>
245-
<div className="flex items-center gap-2 min-w-0">
246-
{server.isAuthExpired ? (
247-
<AlertTriangleIcon className="w-4 h-4 shrink-0 text-yellow-500" />
248-
) : (
249-
<McpFavicon faviconUrl={server.faviconUrl} className="w-4 h-4 rounded-sm" />
250-
)}
251-
<span className="truncate text-sm">{server.name}</span>
252-
</div>
253-
<Switch
254-
checked={isEnabled}
255-
onCheckedChange={(checked) => onToggle(server.id, checked)}
256-
disabled={server.isAuthExpired}
257-
className="scale-75"
258-
/>
259-
</DropdownMenuItem>
260-
);
261-
})}
262-
{connectedServers.length > 0 && connectableServers.length > 0 && <DropdownMenuSeparator />}
263-
{connectableServers.map((server) => (
242+
{isError && !hasServers ? (
264243
<DropdownMenuItem
265-
key={server.id}
266244
onSelect={(e) => {
267245
e.preventDefault();
268-
void handleConnect(server.id);
246+
refetch();
269247
}}
270-
disabled={connectingServerId !== null}
271-
className="group flex cursor-pointer items-center justify-between gap-2"
248+
className="gap-2 text-destructive"
272249
>
273-
<div className="flex items-center gap-2 min-w-0">
274-
<McpFavicon faviconUrl={server.faviconUrl} className="w-4 h-4 rounded-sm" />
275-
<span className="truncate text-sm">{server.name}</span>
276-
</div>
277-
{connectingServerId === server.id ? (
278-
<Loader2Icon className="w-4 h-4 shrink-0 animate-spin text-muted-foreground" />
279-
) : (
280-
<PlusCircleIcon className="w-4 h-4 shrink-0 text-green-600/80 transition-colors group-focus:text-green-500 group-hover:text-green-500 dark:text-green-400/80 dark:group-focus:text-green-400 dark:group-hover:text-green-400" />
281-
)}
250+
<RefreshCwIcon className="w-4 h-4" />
251+
Failed to load. Retry?
282252
</DropdownMenuItem>
283-
))}
253+
) : isLoading ? (
254+
<DropdownMenuItem disabled>
255+
Loading connectors...
256+
</DropdownMenuItem>
257+
) : !hasServers ? (
258+
<DropdownMenuItem disabled>
259+
No connectors available
260+
</DropdownMenuItem>
261+
) : (
262+
<>
263+
{connectedServers.map((server) => {
264+
const isEnabled = !server.isAuthExpired && !disabledMcpServerIds.includes(server.id);
265+
return (
266+
<DropdownMenuItem
267+
key={server.id}
268+
onSelect={(e) => e.preventDefault()}
269+
disabled={server.isAuthExpired}
270+
className="flex items-center justify-between gap-2"
271+
>
272+
<div className="flex items-center gap-2 min-w-0">
273+
{server.isAuthExpired ? (
274+
<AlertTriangleIcon className="w-4 h-4 shrink-0 text-yellow-500" />
275+
) : (
276+
<McpFavicon faviconUrl={server.faviconUrl} className="w-4 h-4 rounded-sm" />
277+
)}
278+
<span className="truncate text-sm">{server.name}</span>
279+
</div>
280+
<Switch
281+
checked={isEnabled}
282+
onCheckedChange={(checked) => onToggle(server.id, checked)}
283+
disabled={server.isAuthExpired}
284+
className="scale-75"
285+
/>
286+
</DropdownMenuItem>
287+
);
288+
})}
289+
{connectedServers.length > 0 && connectableServers.length > 0 && <DropdownMenuSeparator />}
290+
{connectableServers.map((server) => (
291+
<DropdownMenuItem
292+
key={server.id}
293+
onSelect={(e) => {
294+
e.preventDefault();
295+
void handleConnect(server.id);
296+
}}
297+
disabled={connectingServerId !== null}
298+
className="group flex cursor-pointer items-center justify-between gap-2"
299+
>
300+
<div className="flex items-center gap-2 min-w-0">
301+
<McpFavicon faviconUrl={server.faviconUrl} className="w-4 h-4 rounded-sm" />
302+
<span className="truncate text-sm">{server.name}</span>
303+
</div>
304+
{connectingServerId === server.id ? (
305+
<Loader2Icon className="w-4 h-4 shrink-0 animate-spin text-muted-foreground" />
306+
) : (
307+
<PlusCircleIcon className="w-4 h-4 shrink-0 text-green-600/80 transition-colors group-focus:text-green-500 group-hover:text-green-500 dark:text-green-400/80 dark:group-focus:text-green-400 dark:group-hover:text-green-400" />
308+
)}
309+
</DropdownMenuItem>
310+
))}
311+
</>
312+
)}
313+
<DropdownMenuSeparator />
284314
</>
285315
)}
286-
<DropdownMenuSeparator />
287-
<DropdownMenuItem
288-
className="gap-2 text-muted-foreground"
289-
onSelect={() => router.push(`/settings/accountAskAgent`)}
290-
>
291-
<CableIcon className="w-4 h-4" />
292-
My connectors
293-
</DropdownMenuItem>
294-
{isOwner && (
295-
<DropdownMenuItem
296-
className="gap-2 text-muted-foreground"
297-
onSelect={() => router.push(`/settings/workspaceAskAgent`)}
298-
>
299-
<SettingsIcon className="w-4 h-4" />
300-
Workspace connectors
316+
{shouldShowLoginConnectorItem ? (
317+
<DropdownMenuItem asChild className="gap-2 text-link focus:text-link">
318+
<Link href={loginHref}>
319+
<LogInIcon className="w-4 h-4" />
320+
Log in to use connectors
321+
</Link>
301322
</DropdownMenuItem>
323+
) : (
324+
<>
325+
<DropdownMenuItem
326+
className="gap-2 text-muted-foreground"
327+
onSelect={() => router.push(`/settings/accountAskAgent`)}
328+
>
329+
<CableIcon className="w-4 h-4" />
330+
My connectors
331+
</DropdownMenuItem>
332+
{isOwner && (
333+
<DropdownMenuItem
334+
className="gap-2 text-muted-foreground"
335+
onSelect={() => router.push(`/settings/workspaceAskAgent`)}
336+
>
337+
<SettingsIcon className="w-4 h-4" />
338+
Workspace connectors
339+
</DropdownMenuItem>
340+
)}
341+
</>
302342
)}
303343
</DropdownMenuSubContent>
304344
</DropdownMenuSub>

packages/web/src/features/chat/components/chatBox/chatBoxPlusButton.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ interface ChatBoxPlusButtonProps {
1010
onSelectedSearchScopesChange: (items: SearchScope[]) => void;
1111
disabledMcpServerIds: string[];
1212
onDisabledMcpServerIdsChange: (ids: string[]) => void;
13+
isAuthenticated: boolean;
1314
}
1415

1516
/**

0 commit comments

Comments
 (0)