From 4e39b411bef618b3b3009cc573204da664d0f476 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Thu, 23 Jul 2026 17:02:06 +0530 Subject: [PATCH 01/32] refactor: simplify docstring in capability registry for clarity --- surfsense_backend/app/capabilities/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/surfsense_backend/app/capabilities/__init__.py b/surfsense_backend/app/capabilities/__init__.py index 9c97ef59a..dfdab630b 100644 --- a/surfsense_backend/app/capabilities/__init__.py +++ b/surfsense_backend/app/capabilities/__init__.py @@ -1,4 +1,4 @@ -"""Scraper capability registry — typed, stateless verbs. See plans/backend/04-capabilities.md.""" +"""Scraper capability registry — typed, stateless verbs.""" from __future__ import annotations From 9c16b7f40748b105d81591fe3b24d69e11f5bcf6 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Thu, 23 Jul 2026 23:37:16 +0530 Subject: [PATCH 02/32] feat(connectors): add dedicated connectors route with sidebar panel --- .../[workspace_id]/connectors/page.tsx | 5 + .../connector-popup/connectors-panel.tsx | 548 ++++++++++++++++++ 2 files changed, 553 insertions(+) create mode 100644 surfsense_web/app/dashboard/[workspace_id]/connectors/page.tsx create mode 100644 surfsense_web/components/assistant-ui/connector-popup/connectors-panel.tsx diff --git a/surfsense_web/app/dashboard/[workspace_id]/connectors/page.tsx b/surfsense_web/app/dashboard/[workspace_id]/connectors/page.tsx new file mode 100644 index 000000000..d43a54d3d --- /dev/null +++ b/surfsense_web/app/dashboard/[workspace_id]/connectors/page.tsx @@ -0,0 +1,5 @@ +import { ConnectorsSection } from "@/components/assistant-ui/connector-popup/connectors-panel"; + +export default function ConnectorsPage() { + return ; +} diff --git a/surfsense_web/components/assistant-ui/connector-popup/connectors-panel.tsx b/surfsense_web/components/assistant-ui/connector-popup/connectors-panel.tsx new file mode 100644 index 000000000..4e0e8b4b1 --- /dev/null +++ b/surfsense_web/components/assistant-ui/connector-popup/connectors-panel.tsx @@ -0,0 +1,548 @@ +"use client"; + +import { useAtomValue } from "jotai"; +import { ChevronRight, LayoutGrid, Search, TriangleAlert, X } from "lucide-react"; +import { type ReactNode, useMemo, useState } from "react"; +import { statusInboxItemsAtom } from "@/atoms/inbox/status-inbox.atom"; +import { activeWorkspaceIdAtom } from "@/atoms/workspaces/workspace-query.atoms"; +import { Button } from "@/components/ui/button"; +import { + Drawer, + DrawerContent, + DrawerHandle, + DrawerTitle, + DrawerTrigger, +} from "@/components/ui/drawer"; +import { Separator } from "@/components/ui/separator"; +import { Spinner } from "@/components/ui/spinner"; +import { EnumConnectorName } from "@/contracts/enums/connector"; +import { getConnectorIcon } from "@/contracts/enums/connectorIcons"; +import type { SearchSourceConnector } from "@/contracts/types/connector.types"; +import { connectorIndexingMetadata } from "@/contracts/types/inbox.types"; +import { useConnectorsSync } from "@/hooks/use-connectors-sync"; +import { useZeroDocumentTypeCounts } from "@/hooks/use-zero-document-type-counts"; +import { cn } from "@/lib/utils"; +import { ConnectorConnectView } from "./connector-configs/views/connector-connect-view"; +import { ConnectorEditView } from "./connector-configs/views/connector-edit-view"; +import { IndexingConfigurationView } from "./connector-configs/views/indexing-configuration-view"; +import { COMPOSIO_CONNECTORS, getConnectorTitle, OAUTH_CONNECTORS } from "./constants/connector-constants"; +import { useConnectorDialog } from "./hooks/use-connector-dialog"; +import { useIndexingConnectors } from "./hooks/use-indexing-connectors"; +import { AllConnectorsTab } from "./tabs/all-connectors-tab"; +import { ConnectorAccountsListView } from "./views/connector-accounts-list-view"; +import { YouTubeCrawlerView } from "./views/youtube-crawler-view"; + +/** Health of a connected connector type, derived from indexing + inbox state. */ +type ConnectorHealth = "syncing" | "failed" | "ok"; + +interface ConnectedRow { + type: string; + title: string; + connectors: SearchSourceConnector[]; + accountCount: number; + health: ConnectorHealth; + errorMessage?: string; +} + +/** + * Connector management surface (single `/connectors` route) rendered inside the + * workspace panel. Stateful master–detail: + * - sub-rail: Overview + a flat "Your connectors" list (only what's connected, + * each with a live status glyph) + an "Add MCP server" action; + * - detail pane: the flat catalog (search + cards) OR one of the existing flow + * views (connect / edit / indexing / accounts / YouTube), reused verbatim + * from the former dialog. + * + * Unconnected connectors are one click from the catalog cards, so they never + * clutter the rail. The hook's internal `isOpen`/`connectorDialogOpenAtom` is + * inert on a route and ignored. + */ +export function ConnectorsSection() { + const workspaceId = useAtomValue(activeWorkspaceIdAtom); + const documentTypeCounts = useZeroDocumentTypeCounts(workspaceId); + const statusInboxItems = useAtomValue(statusInboxItemsAtom); + const [drawerOpen, setDrawerOpen] = useState(false); + + const inboxItems = useMemo( + () => statusInboxItems.filter((item) => item.type === "connector_indexing"), + [statusInboxItems] + ); + + const { + connectingId, + searchQuery, + indexingConfig, + indexingConnector, + indexingConnectorConfig, + editingConnector, + connectingConnectorType, + isCreatingConnector, + startDate, + endDate, + isStartingIndexing, + isSaving, + isDisconnecting, + periodicEnabled, + frequencyMinutes, + enableVisionLlm, + allConnectors, + viewingAccountsType, + viewingMCPList, + isYouTubeView, + isFromOAuth, + setSearchQuery, + setStartDate, + setEndDate, + setPeriodicEnabled, + setFrequencyMinutes, + setEnableVisionLlm, + handleOpenChange, + handleConnectOAuth, + handleConnectNonOAuth, + handleCreateWebcrawler, + handleCreateYouTubeCrawler, + handleSubmitConnectForm, + handleStartIndexing, + handleSkipIndexing, + handleStartEdit, + handleSaveConnector, + handleDisconnectConnector, + handleBackFromEdit, + handleBackFromConnect, + handleBackFromYouTube, + handleViewAccountsList, + handleBackFromAccountsList, + handleViewMCPList, + handleBackFromMCPList, + handleAddNewMCPFromList, + handleQuickIndexConnector, + connectorConfig, + setConnectorConfig, + setIndexingConnectorConfig, + setConnectorName, + } = useConnectorDialog(); + + const { + connectors: connectorsFromSync = [], + loading: connectorsLoading, + error: connectorsError, + refreshConnectors: refreshConnectorsSync, + } = useConnectorsSync(workspaceId); + + const useSyncData = connectorsFromSync.length > 0 || (connectorsLoading && !connectorsError); + const connectors = (useSyncData ? connectorsFromSync : allConnectors || []) as SearchSourceConnector[]; + + const refreshConnectors = async () => { + if (useSyncData) { + await refreshConnectorsSync(); + } + }; + + const { indexingConnectorIds, startIndexing, stopIndexing } = useIndexingConnectors( + connectors, + inboxItems + ); + + const connectedTypes = useMemo( + () => new Set(connectors.map((c) => c.connector_type)), + [connectors] + ); + + // Latest indexing status per connector id, parsed from the status inbox. Used + // to drive the "Your connectors" status glyphs (syncing / failed). + const statusByConnectorId = useMemo(() => { + const map = new Map(); + for (const item of inboxItems) { + const parsed = connectorIndexingMetadata.safeParse(item.metadata); + if (!parsed.success) continue; + const at = item.updated_at ?? item.created_at; + const prev = map.get(parsed.data.connector_id); + if (!prev || at > prev.at) { + map.set(parsed.data.connector_id, { + status: parsed.data.status, + error: parsed.data.error_message ?? undefined, + at, + }); + } + } + return map; + }, [inboxItems]); + + // Flat "Your connectors" rows: one per connected type, alphabetized, with a + // derived health (syncing > failed > ok) and account count. + const connectedRows = useMemo(() => { + const byType = new Map(); + for (const c of connectors) { + const arr = byType.get(c.connector_type) ?? []; + arr.push(c); + byType.set(c.connector_type, arr); + } + return [...byType.entries()] + .map(([type, list]) => { + const ids = list.map((c) => c.id); + const syncing = ids.some( + (id) => indexingConnectorIds.has(id) || statusByConnectorId.get(id)?.status === "in_progress" + ); + const failedEntry = syncing + ? undefined + : ids.map((id) => statusByConnectorId.get(id)).find((s) => s?.status === "failed"); + return { + type, + title: + type === EnumConnectorName.MCP_CONNECTOR ? "MCP Servers" : getConnectorTitle(type), + connectors: list, + accountCount: list.length, + health: syncing ? "syncing" : failedEntry ? "failed" : "ok", + errorMessage: failedEntry?.error, + } satisfies ConnectedRow; + }) + .sort((a, b) => a.title.localeCompare(b.title)); + }, [connectors, indexingConnectorIds, statusByConnectorId]); + + if (!workspaceId) return null; + + const activeConnectorType = editingConnector + ? editingConnector.connector_type + : connectingConnectorType || + viewingAccountsType?.connectorType || + (viewingMCPList ? EnumConnectorName.MCP_CONNECTOR : null); + + const flowView = ((): ReactNode => { + if (isYouTubeView) { + return ; + } + if (viewingMCPList) { + return ( + + ); + } + if (viewingAccountsType) { + return ( + { + const oauthConnector = + OAUTH_CONNECTORS.find( + (c) => c.connectorType === viewingAccountsType.connectorType + ) || + COMPOSIO_CONNECTORS.find( + (c) => c.connectorType === viewingAccountsType.connectorType + ); + if (oauthConnector) { + handleConnectOAuth(oauthConnector); + } + }} + isConnecting={connectingId !== null} + /> + ); + } + if (connectingConnectorType) { + return ( + handleSubmitConnectForm(formData, startIndexing)} + onBack={handleBackFromConnect} + isSubmitting={isCreatingConnector} + /> + ); + } + if (editingConnector) { + return ( + c.id === editingConnector.id)?.last_indexed_at ?? + editingConnector.last_indexed_at, + }} + startDate={startDate} + endDate={endDate} + periodicEnabled={periodicEnabled} + frequencyMinutes={frequencyMinutes} + enableVisionLlm={enableVisionLlm} + isSaving={isSaving} + isDisconnecting={isDisconnecting} + isIndexing={indexingConnectorIds.has(editingConnector.id)} + workspaceId={workspaceId?.toString()} + onStartDateChange={setStartDate} + onEndDateChange={setEndDate} + onPeriodicEnabledChange={setPeriodicEnabled} + onFrequencyChange={setFrequencyMinutes} + onEnableVisionLlmChange={setEnableVisionLlm} + onSave={() => { + startIndexing(editingConnector.id); + handleSaveConnector(() => refreshConnectors()); + }} + onDisconnect={() => handleDisconnectConnector(() => refreshConnectors())} + onBack={handleBackFromEdit} + onQuickIndex={(() => { + const cfg = connectorConfig || editingConnector.config; + const isDriveOrOneDrive = + editingConnector.connector_type === "GOOGLE_DRIVE_CONNECTOR" || + editingConnector.connector_type === "COMPOSIO_GOOGLE_DRIVE_CONNECTOR" || + editingConnector.connector_type === "ONEDRIVE_CONNECTOR" || + editingConnector.connector_type === "DROPBOX_CONNECTOR"; + const hasDriveItems = isDriveOrOneDrive + ? ((cfg?.selected_folders as unknown[]) ?? []).length > 0 || + ((cfg?.selected_files as unknown[]) ?? []).length > 0 + : true; + if (!hasDriveItems) return undefined; + return () => { + startIndexing(editingConnector.id); + handleQuickIndexConnector( + editingConnector.id, + editingConnector.connector_type, + stopIndexing, + startDate, + endDate + ); + }; + })()} + onConfigChange={setConnectorConfig} + onNameChange={setConnectorName} + /> + ); + } + if (indexingConfig) { + return ( + { + if (indexingConfig.connectorId) { + startIndexing(indexingConfig.connectorId); + } + handleStartIndexing(() => refreshConnectors()); + }} + onSkip={handleSkipIndexing} + /> + ); + } + return null; + })(); + + const isFlowActive = flowView !== null; + + const detailTitle = + isFlowActive && activeConnectorType ? getConnectorTitle(activeConnectorType) : "Overview"; + + const hasConnected = connectedRows.length > 0; + + // Rail navigation: clear any open flow first (handleOpenChange(false) is the + // hook's full reset), then apply the target management action. + const resetFlow = () => handleOpenChange(false); + const goOverview = () => { + setDrawerOpen(false); + resetFlow(); + }; + const manageRow = (row: ConnectedRow) => { + setDrawerOpen(false); + resetFlow(); + if (row.type === EnumConnectorName.MCP_CONNECTOR) { + handleViewMCPList(); + return; + } + if (row.accountCount > 1) { + handleViewAccountsList(row.type, row.title); + return; + } + handleStartEdit(row.connectors[0]); + }; + const addMcpServer = () => { + setDrawerOpen(false); + resetFlow(); + handleConnectNonOAuth?.(EnumConnectorName.MCP_CONNECTOR); + }; + + const navBtnClass = (active: boolean) => + cn( + "inline-flex w-full items-center justify-start gap-3 rounded-md px-3 py-2.5 text-left text-sm font-medium transition-colors duration-150 focus:outline-none", + active + ? "bg-accent text-accent-foreground" + : "text-muted-foreground hover:bg-accent hover:text-accent-foreground" + ); + + const railLabel = (text: string) => ( +

{text}

+ ); + + const renderNav = () => ( + <> + + + {hasConnected && ( + <> + + {railLabel("Your connectors")} +
+ {connectedRows.map((row) => { + const isActive = isFlowActive && activeConnectorType === row.type; + return ( + + ); + })} +
+ + )} + + + + + ); + + const catalog = ( +
+
+
+ + setSearchQuery(e.target.value)} + /> + {searchQuery && ( + + )} +
+
+ + +
+ ); + + return ( +
+
+

+ Connectors +

+ + + + + + + + Connectors navigation + + + +
+ +
+
+

{detailTitle}

+ +
+
{isFlowActive ? flowView : catalog}
+
+
+ ); +} From e705a73ae952f25744e24b805f482ebbfb0d57c5 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Thu, 23 Jul 2026 23:37:26 +0530 Subject: [PATCH 03/32] feat(sidebar): add Connectors nav item to workspace sidebar --- .../layout/providers/LayoutDataProvider.tsx | 14 ++++++++++++-- .../components/layout/ui/sidebar/Sidebar.tsx | 15 +++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/surfsense_web/components/layout/providers/LayoutDataProvider.tsx b/surfsense_web/components/layout/providers/LayoutDataProvider.tsx index 511c095e5..cfd970f13 100644 --- a/surfsense_web/components/layout/providers/LayoutDataProvider.tsx +++ b/surfsense_web/components/layout/providers/LayoutDataProvider.tsx @@ -2,7 +2,7 @@ import { useQuery } from "@tanstack/react-query"; import { useAtomValue, useSetAtom } from "jotai"; -import { AlarmClock, AlertTriangle, Shapes, SquareTerminal } from "lucide-react"; +import { AlarmClock, AlertTriangle, Shapes, SquareTerminal, Unplug } from "lucide-react"; import { useParams, usePathname, useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; import { useTheme } from "next-themes"; @@ -300,6 +300,7 @@ export function LayoutDataProvider({ const isAutomationsActive = pathname?.includes("/automations") === true; const isArtifactsActive = pathname?.endsWith("/artifacts") === true; const isPlaygroundRoute = pathname?.includes("/playground") === true; + const isConnectorsRoute = pathname?.includes("/connectors") === true; const navItems: NavItem[] = useMemo( () => ( @@ -316,6 +317,12 @@ export function LayoutDataProvider({ icon: Shapes, isActive: isArtifactsActive, }, + { + title: "Connectors", + url: `/dashboard/${workspaceId}/connectors`, + icon: Unplug, + isActive: isConnectorsRoute, + }, { title: "Playground", url: `/dashboard/${workspaceId}/playground`, @@ -324,7 +331,7 @@ export function LayoutDataProvider({ }, ] as (NavItem | null)[] ).filter((item): item is NavItem => item !== null), - [workspaceId, isAutomationsActive, isArtifactsActive, isPlaygroundRoute] + [workspaceId, isAutomationsActive, isArtifactsActive, isConnectorsRoute, isPlaygroundRoute] ); // Handlers @@ -631,6 +638,7 @@ export function LayoutDataProvider({ const isAutomationsPage = pathname?.includes("/automations") === true; const isArtifactsPage = pathname?.endsWith("/artifacts") === true; const isPlaygroundPage = pathname?.includes("/playground") === true; + const isConnectorsPage = pathname?.includes("/connectors") === true; const isAllChatsPage = pathname?.endsWith("/chats") === true; const handleChatsClick = useCallback(() => { router.push(`/dashboard/${workspaceId}/chats`); @@ -650,6 +658,7 @@ export function LayoutDataProvider({ isAutomationsPage || isArtifactsPage || isPlaygroundPage || + isConnectorsPage || isAllChatsPage; return ( @@ -702,6 +711,7 @@ export function LayoutDataProvider({ isAutomationsPage || isArtifactsPage || isPlaygroundPage || + isConnectorsPage || isAllChatsPage ? "items-start justify-center px-6 py-8 md:px-10 md:pb-10 md:pt-16" : undefined diff --git a/surfsense_web/components/layout/ui/sidebar/Sidebar.tsx b/surfsense_web/components/layout/ui/sidebar/Sidebar.tsx index e24523448..0d280dc28 100644 --- a/surfsense_web/components/layout/ui/sidebar/Sidebar.tsx +++ b/surfsense_web/components/layout/ui/sidebar/Sidebar.tsx @@ -140,6 +140,10 @@ export function Sidebar({ () => navItems.find((item) => item.url.endsWith("/artifacts")), [navItems] ); + const connectorsItem = useMemo( + () => navItems.find((item) => item.url.endsWith("/connectors")), + [navItems] + ); const playgroundItem = useMemo( () => navItems.find((item) => item.url.endsWith("/playground")), [navItems] @@ -150,6 +154,7 @@ export function Sidebar({ (item) => !item.url.endsWith("/automations") && !item.url.endsWith("/artifacts") && + !item.url.endsWith("/connectors") && !item.url.endsWith("/playground") ), [navItems] @@ -254,6 +259,16 @@ export function Sidebar({ tooltipContent={isCollapsed ? artifactsItem.title : undefined} /> )} + {connectorsItem && ( + onNavItemClick?.(connectorsItem)} + isCollapsed={isCollapsed} + isActive={connectorsItem.isActive} + tooltipContent={isCollapsed ? connectorsItem.title : undefined} + /> + )} {playgroundItem && ( Date: Thu, 23 Jul 2026 23:37:30 +0530 Subject: [PATCH 04/32] fix(connectors): land OAuth callback on connectors panel to resume flow --- .../app/dashboard/[workspace_id]/connectors/callback/route.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/surfsense_web/app/dashboard/[workspace_id]/connectors/callback/route.ts b/surfsense_web/app/dashboard/[workspace_id]/connectors/callback/route.ts index 09a2c2f2c..857075e58 100644 --- a/surfsense_web/app/dashboard/[workspace_id]/connectors/callback/route.ts +++ b/surfsense_web/app/dashboard/[workspace_id]/connectors/callback/route.ts @@ -19,7 +19,9 @@ export async function GET( const response = new NextResponse(null, { status: 302, headers: { - Location: `/dashboard/${workspace_id}/new-chat`, + // Land on the connectors panel so `useConnectorDialog` (mounted there) + // consumes the result cookie and continues the indexing/edit flow. + Location: `/dashboard/${workspace_id}/connectors`, }, }); response.cookies.set(OAUTH_RESULT_COOKIE, result, { From 132fd1e5d63a379062fee838f87cec46e20d6572 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Thu, 23 Jul 2026 23:37:40 +0530 Subject: [PATCH 05/32] refactor(connectors): remove legacy ConnectorIndicator modal --- .../[workspace_id]/client-layout.tsx | 2 - .../assistant-ui/connector-popup.tsx | 388 ------------------ .../assistant-ui/connector-popup/index.ts | 3 - 3 files changed, 393 deletions(-) delete mode 100644 surfsense_web/components/assistant-ui/connector-popup.tsx diff --git a/surfsense_web/app/dashboard/[workspace_id]/client-layout.tsx b/surfsense_web/app/dashboard/[workspace_id]/client-layout.tsx index 54fc9d000..fb76af227 100644 --- a/surfsense_web/app/dashboard/[workspace_id]/client-layout.tsx +++ b/surfsense_web/app/dashboard/[workspace_id]/client-layout.tsx @@ -8,7 +8,6 @@ import { useEffect } from "react"; import { pendingUserImageDataUrlsAtom } from "@/atoms/chat/pending-user-images.atom"; import { llmSetupStatusAtomFamily } from "@/atoms/model-connections/model-connections-query.atoms"; import { activeWorkspaceIdAtom } from "@/atoms/workspaces/workspace-query.atoms"; -import { ConnectorIndicator } from "@/components/assistant-ui/connector-popup"; import { DocumentUploadDialogProvider } from "@/components/assistant-ui/document-upload-popup"; import { LayoutDataProvider } from "@/components/layout"; import { OnboardingTour } from "@/components/onboarding-tour"; @@ -169,7 +168,6 @@ export function DashboardClientLayout({ initialPlaygroundSidebarCollapsed={initialPlaygroundSidebarCollapsed} > {children} - ); diff --git a/surfsense_web/components/assistant-ui/connector-popup.tsx b/surfsense_web/components/assistant-ui/connector-popup.tsx deleted file mode 100644 index a8bfc29bd..000000000 --- a/surfsense_web/components/assistant-ui/connector-popup.tsx +++ /dev/null @@ -1,388 +0,0 @@ -"use client"; - -import { useAtomValue } from "jotai"; -import { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from "react"; -import { createPortal } from "react-dom"; -import { statusInboxItemsAtom } from "@/atoms/inbox/status-inbox.atom"; -import { activeWorkspaceIdAtom } from "@/atoms/workspaces/workspace-query.atoms"; -import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog"; -import { Tabs, TabsContent } from "@/components/ui/tabs"; -import type { SearchSourceConnector } from "@/contracts/types/connector.types"; -import { useConnectorsSync } from "@/hooks/use-connectors-sync"; -import { PICKER_CLOSE_EVENT, PICKER_OPEN_EVENT } from "@/hooks/use-google-picker"; -import { useZeroDocumentTypeCounts } from "@/hooks/use-zero-document-type-counts"; -import { ConnectorDialogHeader } from "./connector-popup/components/connector-dialog-header"; -import { ConnectorConnectView } from "./connector-popup/connector-configs/views/connector-connect-view"; -import { ConnectorEditView } from "./connector-popup/connector-configs/views/connector-edit-view"; -import { IndexingConfigurationView } from "./connector-popup/connector-configs/views/indexing-configuration-view"; -import { - COMPOSIO_CONNECTORS, - OAUTH_CONNECTORS, -} from "./connector-popup/constants/connector-constants"; -import { useConnectorDialog } from "./connector-popup/hooks/use-connector-dialog"; -import { useIndexingConnectors } from "./connector-popup/hooks/use-indexing-connectors"; -import { ActiveConnectorsTab } from "./connector-popup/tabs/active-connectors-tab"; -import { AllConnectorsTab } from "./connector-popup/tabs/all-connectors-tab"; -import { ConnectorAccountsListView } from "./connector-popup/views/connector-accounts-list-view"; -import { YouTubeCrawlerView } from "./connector-popup/views/youtube-crawler-view"; - -export interface ConnectorIndicatorHandle { - open: () => void; -} - -interface ConnectorIndicatorProps { - showTrigger?: boolean; -} - -export const ConnectorIndicator = forwardRef( - (_props, ref) => { - const workspaceId = useAtomValue(activeWorkspaceIdAtom); - - // Real-time document type counts via Zero (updates instantly as docs are indexed) - const documentTypeCounts = useZeroDocumentTypeCounts(workspaceId); - // Read status inbox items from shared atom (populated by LayoutDataProvider) - // instead of creating a duplicate useInbox("status") hook. - const statusInboxItems = useAtomValue(statusInboxItemsAtom); - const inboxItems = useMemo( - () => statusInboxItems.filter((item) => item.type === "connector_indexing"), - [statusInboxItems] - ); - - // Use the custom hook for dialog state management - const { - isOpen, - activeTab, - connectingId, - isScrolled, - searchQuery, - indexingConfig, - indexingConnector, - indexingConnectorConfig, - editingConnector, - connectingConnectorType, - isCreatingConnector, - startDate, - endDate, - isStartingIndexing, - isSaving, - isDisconnecting, - periodicEnabled, - frequencyMinutes, - enableVisionLlm, - allConnectors, - viewingAccountsType, - viewingMCPList, - isYouTubeView, - isFromOAuth, - setSearchQuery, - setStartDate, - setEndDate, - setPeriodicEnabled, - setFrequencyMinutes, - setEnableVisionLlm, - handleOpenChange, - handleTabChange, - handleScroll, - handleConnectOAuth, - handleConnectNonOAuth, - handleCreateWebcrawler, - handleCreateYouTubeCrawler, - handleSubmitConnectForm, - handleStartIndexing, - handleSkipIndexing, - handleStartEdit, - handleSaveConnector, - handleDisconnectConnector, - handleBackFromEdit, - handleBackFromConnect, - handleBackFromYouTube, - handleViewAccountsList, - handleBackFromAccountsList, - handleBackFromMCPList, - handleAddNewMCPFromList, - handleQuickIndexConnector, - connectorConfig, - setConnectorConfig, - setIndexingConnectorConfig, - setConnectorName, - } = useConnectorDialog(); - - const [pickerOpen, setPickerOpen] = useState(false); - useEffect(() => { - const onOpen = () => setPickerOpen(true); - const onClose = () => setPickerOpen(false); - window.addEventListener(PICKER_OPEN_EVENT, onOpen); - window.addEventListener(PICKER_CLOSE_EVENT, onClose); - return () => { - window.removeEventListener(PICKER_OPEN_EVENT, onOpen); - window.removeEventListener(PICKER_CLOSE_EVENT, onClose); - }; - }, []); - - const { - connectors: connectorsFromSync = [], - loading: connectorsLoading, - error: connectorsError, - refreshConnectors: refreshConnectorsSync, - } = useConnectorsSync(workspaceId); - - const useSyncData = connectorsFromSync.length > 0 || (connectorsLoading && !connectorsError); - const connectors = useSyncData ? connectorsFromSync : allConnectors || []; - - const refreshConnectors = async () => { - if (useSyncData) { - await refreshConnectorsSync(); - } - }; - - // Track indexing state locally - clears automatically when last_indexed_at changes via real-time sync - // Also clears when failed notifications are detected - const { indexingConnectorIds, startIndexing, stopIndexing } = useIndexingConnectors( - connectors as SearchSourceConnector[], - inboxItems - ); - - // Get document types that have documents in the workspace - const activeDocumentTypes = documentTypeCounts - ? Object.entries(documentTypeCounts).filter(([, count]) => count > 0) - : []; - - const hasConnectors = connectors.length > 0; - const hasSources = hasConnectors || activeDocumentTypes.length > 0; - const totalSourceCount = connectors.length + activeDocumentTypes.length; - - const activeConnectorsCount = connectors.length; - - // Check which connectors are already connected - // Real-time connector updates via Zero sync - const connectedTypes = new Set( - (connectors || []).map((c: SearchSourceConnector) => c.connector_type) - ); - - useImperativeHandle(ref, () => ({ - open: () => handleOpenChange(true), - })); - - if (!workspaceId) return null; - - return ( - - {isOpen && - createPortal( - - ); - } -); - -ConnectorIndicator.displayName = "ConnectorIndicator"; diff --git a/surfsense_web/components/assistant-ui/connector-popup/index.ts b/surfsense_web/components/assistant-ui/connector-popup/index.ts index f3209a777..8fd895e71 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/index.ts +++ b/surfsense_web/components/assistant-ui/connector-popup/index.ts @@ -1,6 +1,3 @@ -// Main component export -export { ConnectorIndicator } from "../connector-popup"; - // Sub-components (if needed for external use) export { ConnectorCard } from "./components/connector-card"; export { ConnectorDialogHeader } from "./components/connector-dialog-header"; From 915ebcf2e74d2e3e9d521ed4357af54200e35309 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Thu, 23 Jul 2026 23:37:48 +0530 Subject: [PATCH 06/32] refactor(chat): open connectors page from composer/banner and restyle tool-group headings --- .../components/assistant-ui/thread.tsx | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/surfsense_web/components/assistant-ui/thread.tsx b/surfsense_web/components/assistant-ui/thread.tsx index 73d31e11d..120af4bfe 100644 --- a/surfsense_web/components/assistant-ui/thread.tsx +++ b/surfsense_web/components/assistant-ui/thread.tsx @@ -44,7 +44,6 @@ import { clearPremiumAlertForThreadAtom, premiumAlertByThreadAtom, } from "@/atoms/chat/premium-alert.atom"; -import { connectorDialogOpenAtom } from "@/atoms/connector-dialog/connector-dialog.atoms"; import { connectorsAtom } from "@/atoms/connectors/connector-query.atoms"; import { membersAtom } from "@/atoms/members/members-query.atoms"; import { llmSetupStatusAtomFamily } from "@/atoms/model-connections/model-connections-query.atoms"; @@ -328,7 +327,9 @@ const ConnectToolsBanner: FC<{ onVisibleChange?: (visible: boolean) => void; }> = ({ isThreadEmpty, onVisibleChange }) => { const { data: connectors } = useAtomValue(connectorsAtom); - const setConnectorDialogOpen = useSetAtom(connectorDialogOpenAtom); + const router = useRouter(); + const params = useParams(); + const bannerWorkspaceId = getWorkspaceIdNumber(params); const [dismissed, setDismissed] = useState(() => { if (typeof window === "undefined") return false; return localStorage.getItem(BANNER_DISMISSED_KEY) === "true"; @@ -371,7 +372,9 @@ const ConnectToolsBanner: FC<{ variant="ghost" size="sm" className="h-7 min-w-0 cursor-pointer justify-start gap-2 rounded-md px-0 text-[13px] font-normal text-muted-foreground select-none hover:bg-transparent hover:text-foreground" - onClick={() => setConnectorDialogOpen(true)} + onClick={() => { + if (bannerWorkspaceId) router.push(`/dashboard/${bannerWorkspaceId}/connectors`); + }} > Connect your tools @@ -1152,7 +1155,11 @@ const ComposerAction: FC = ({ onChatModelSelected, }) => { const mentionedDocuments = useAtomValue(mentionedDocumentsAtom); - const setConnectorDialogOpen = useSetAtom(connectorDialogOpenAtom); + const router = useRouter(); + const openConnectors = useCallback( + () => router.push(`/dashboard/${workspaceId}/connectors`), + [router, workspaceId] + ); const [toolsPopoverOpen, setToolsPopoverOpen] = useState(false); const [openConnectorSubmenu, setOpenConnectorSubmenu] = useState(null); const [expandedConnectorGroups, setExpandedConnectorGroups] = useState>( @@ -1302,7 +1309,7 @@ const ComposerAction: FC = ({ Upload Files - setConnectorDialogOpen(true)}> + MCP Connectors @@ -1327,7 +1334,7 @@ const ComposerAction: FC = ({
{regularToolGroups.map((group) => (
-
+
{group.label}
{group.tools.map((tool) => { @@ -1354,7 +1361,7 @@ const ComposerAction: FC = ({ ))} {connectorToolGroups.length > 0 && (
-
+
Connector Actions
{connectorToolGroups.map((group) => { @@ -1435,7 +1442,7 @@ const ComposerAction: FC = ({ )} {otherToolGroup && (
-
+
{otherToolGroup.label}
{otherToolGroup.tools.map((tool) => { @@ -1522,7 +1529,7 @@ const ComposerAction: FC = ({ Take a screenshot - setConnectorDialogOpen(true)}> + MCP Connectors @@ -1546,7 +1553,7 @@ const ComposerAction: FC = ({ > {regularToolGroups.map((group) => (
-
+
{group.label}
{group.tools.map((tool) => { @@ -1581,7 +1588,7 @@ const ComposerAction: FC = ({ ))} {connectorToolGroups.length > 0 && (
-
+
Connector Actions
{connectorToolGroups.map((group) => { @@ -1667,7 +1674,7 @@ const ComposerAction: FC = ({ )} {otherToolGroup && (
-
+
{otherToolGroup.label}
{otherToolGroup.tools.map((tool) => { From 6c1c75a3e1fc5a869a56c595db4e2b5d68f25394 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Thu, 23 Jul 2026 23:37:51 +0530 Subject: [PATCH 07/32] feat(connector-icon): mask MCP icon to inherit currentColor --- surfsense_web/contracts/enums/connectorIcons.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/surfsense_web/contracts/enums/connectorIcons.tsx b/surfsense_web/contracts/enums/connectorIcons.tsx index 81696729d..1e76d7e45 100644 --- a/surfsense_web/contracts/enums/connectorIcons.tsx +++ b/surfsense_web/contracts/enums/connectorIcons.tsx @@ -74,7 +74,17 @@ export const getConnectorIcon = (connectorType: EnumConnectorName | string, clas case EnumConnectorName.CIRCLEBACK_CONNECTOR: return Circleback; case EnumConnectorName.MCP_CONNECTOR: - return MCP; + // Masked so the black glyph inherits currentColor (white in dark mode). + return ( +