Merge remote-tracking branch 'upstream/dev' into fix/zero-cache-stale-replica-1355

This commit is contained in:
Anish Sarkar 2026-05-16 19:30:09 +05:30
commit af1d2fa430
601 changed files with 45027 additions and 4681 deletions

View file

@ -124,7 +124,6 @@ export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, Connector
handleStartEdit,
handleSaveConnector,
handleDisconnectConnector,
handleDisconnectFromList,
handleBackFromEdit,
handleBackFromConnect,
handleBackFromYouTube,
@ -233,9 +232,6 @@ export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, Connector
indexingConnectorIds={indexingConnectorIds}
onBack={handleBackFromMCPList}
onManage={handleStartEdit}
onDisconnect={(connector) =>
handleDisconnectFromList(connector, () => refreshConnectors())
}
onAddAccount={handleAddNewMCPFromList}
addButtonText="Add New MCP Server"
/>
@ -247,9 +243,6 @@ export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, Connector
indexingConnectorIds={indexingConnectorIds}
onBack={handleBackFromAccountsList}
onManage={handleStartEdit}
onDisconnect={(connector) =>
handleDisconnectFromList(connector, () => refreshConnectors())
}
onAddAccount={() => {
// Check both OAUTH_CONNECTORS and COMPOSIO_CONNECTORS
const oauthConnector =

View file

@ -3,6 +3,7 @@
import { CheckCircle2 } from "lucide-react";
import type { FC } from "react";
import type { ConnectorConfigProps } from "../index";
import { MCPTrustedTools } from "./mcp-trusted-tools";
export const MCPServiceConfig: FC<ConnectorConfigProps> = ({ connector }) => {
const serviceName = connector.config?.mcp_service as string | undefined;
@ -11,7 +12,7 @@ export const MCPServiceConfig: FC<ConnectorConfigProps> = ({ connector }) => {
: "this service";
return (
<div className="space-y-4">
<div className="space-y-6">
<div className="rounded-xl border border-border bg-emerald-500/5 p-4 flex items-start gap-3">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-emerald-500/10 shrink-0 mt-0.5">
<CheckCircle2 className="size-4 text-emerald-500" />
@ -23,6 +24,8 @@ export const MCPServiceConfig: FC<ConnectorConfigProps> = ({ connector }) => {
</p>
</div>
</div>
{connector.id > 0 && <MCPTrustedTools connector={connector} />}
</div>
);
};

View file

@ -0,0 +1,89 @@
"use client";
import { ShieldCheck, Trash2 } from "lucide-react";
import type { FC } from "react";
import { useState } from "react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
import { connectorsApiService } from "@/lib/apis/connectors-api.service";
interface MCPTrustedToolsProps {
connector: SearchSourceConnector;
}
/** Audit + revoke surface for tools promoted via in-chat "Always Allow". */
export const MCPTrustedTools: FC<MCPTrustedToolsProps> = ({ connector }) => {
const trustedTools = readTrustedTools(connector.config);
const [pending, setPending] = useState<Set<string>>(new Set());
const handleRevoke = async (toolName: string) => {
setPending((prev) => new Set(prev).add(toolName));
try {
await connectorsApiService.untrustMCPTool(connector.id, toolName);
toast.success(`Removed ${toolName} from trusted tools`);
} catch {
toast.error(`Failed to remove ${toolName} from trusted tools`);
} finally {
setPending((prev) => {
const next = new Set(prev);
next.delete(toolName);
return next;
});
}
};
return (
<div className="space-y-4">
<h3 className="font-medium text-sm sm:text-base flex items-center gap-2">
<ShieldCheck className="h-4 w-4" />
Trusted Tools
</h3>
<div className="rounded-xl border border-border bg-slate-400/5 dark:bg-white/5 p-3 sm:p-6 space-y-4">
<p className="text-[10px] sm:text-xs text-muted-foreground">
Tools listed here skip the approval prompt during chat. Trust is granted by clicking
"Always Allow" on an approval card; revoke it here to require approval again.
</p>
{trustedTools.length === 0 ? (
<p className="text-xs text-muted-foreground/70 italic">
No trusted tools yet for this connector.
</p>
) : (
<ul className="space-y-1">
{trustedTools.map((toolName) => {
const isPending = pending.has(toolName);
return (
<li
key={toolName}
className="flex items-center justify-between gap-3 rounded-lg px-3 py-2 hover:bg-muted/40 transition-colors"
>
<span className="text-xs font-mono break-all">{toolName}</span>
<Button
type="button"
variant="ghost"
size="sm"
className="h-7 px-2 text-muted-foreground hover:text-destructive shrink-0"
onClick={() => handleRevoke(toolName)}
disabled={isPending}
aria-label={`Revoke trust for ${toolName}`}
>
<Trash2 className="h-3.5 w-3.5" />
<span className="ml-1 hidden sm:inline">Revoke</span>
</Button>
</li>
);
})}
</ul>
)}
</div>
</div>
);
};
function readTrustedTools(config: Record<string, unknown> | undefined | null): string[] {
const raw = config?.trusted_tools;
if (!Array.isArray(raw)) return [];
return raw.filter((item): item is string => typeof item === "string");
}

View file

@ -1288,25 +1288,6 @@ export const useConnectorDialog = () => {
[editingConnector, searchSpaceId, deleteConnector, cameFromMCPList, setIsOpen]
);
const handleDisconnectFromList = useCallback(
async (connector: SearchSourceConnector, refreshConnectors: () => void) => {
if (!searchSpaceId) return;
try {
await deleteConnector({ id: connector.id });
trackConnectorDeleted(Number(searchSpaceId), connector.connector_type, connector.id);
toast.success(`${connector.name} disconnected successfully`);
refreshConnectors();
queryClient.invalidateQueries({
queryKey: cacheKeys.logs.summary(Number(searchSpaceId)),
});
} catch (error) {
console.error("Error disconnecting connector:", error);
toast.error("Failed to disconnect connector");
}
},
[searchSpaceId, deleteConnector]
);
// Handle quick index (index with selected date range, or backend defaults if none selected)
const handleQuickIndexConnector = useCallback(
async (
@ -1480,7 +1461,6 @@ export const useConnectorDialog = () => {
handleStartEdit,
handleSaveConnector,
handleDisconnectConnector,
handleDisconnectFromList,
handleBackFromEdit,
handleBackFromConnect,
handleBackFromYouTube,

View file

@ -1,7 +1,7 @@
"use client";
import { useAtomValue } from "jotai";
import { ArrowLeft, Plus, RefreshCw, Server, Trash2 } from "lucide-react";
import { ArrowLeft, Plus, RefreshCw, Server } from "lucide-react";
import { type FC, useCallback, useState } from "react";
import { toast } from "sonner";
import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms";
@ -24,7 +24,6 @@ interface ConnectorAccountsListViewProps {
indexingConnectorIds: Set<number>;
onBack: () => void;
onManage: (connector: SearchSourceConnector) => void;
onDisconnect?: (connector: SearchSourceConnector) => Promise<void> | void;
onAddAccount: () => void;
isConnecting?: boolean;
addButtonText?: string;
@ -37,15 +36,12 @@ export const ConnectorAccountsListView: FC<ConnectorAccountsListViewProps> = ({
indexingConnectorIds,
onBack,
onManage,
onDisconnect,
onAddAccount,
isConnecting = false,
addButtonText,
}) => {
const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom);
const [reauthingId, setReauthingId] = useState<number | null>(null);
const [confirmDisconnectId, setConfirmDisconnectId] = useState<number | null>(null);
const [disconnectingId, setDisconnectingId] = useState<number | null>(null);
// Get connector status
const { isConnectorEnabled, getConnectorStatusMessage } = useConnectorStatus();
@ -240,51 +236,6 @@ export const ConnectorAccountsListView: FC<ConnectorAccountsListViewProps> = ({
/>
Re-authenticate
</Button>
) : isLive && onDisconnect ? (
confirmDisconnectId === connector.id ? (
<div className="flex items-center gap-1.5 shrink-0">
<Button
variant="destructive"
size="sm"
className="h-8 text-[11px] px-3 rounded-lg font-medium shadow-xs"
onClick={async () => {
setDisconnectingId(connector.id);
setConfirmDisconnectId(null);
try {
await onDisconnect(connector);
} finally {
setDisconnectingId(null);
}
}}
disabled={disconnectingId === connector.id}
>
{disconnectingId === connector.id ? (
<RefreshCw className="size-3.5 animate-spin" />
) : (
"Confirm"
)}
</Button>
<Button
variant="ghost"
size="sm"
className="h-8 text-[11px] px-2 rounded-lg"
onClick={() => setConfirmDisconnectId(null)}
disabled={disconnectingId === connector.id}
>
Cancel
</Button>
</div>
) : (
<Button
variant="secondary"
size="sm"
className="h-8 text-[11px] px-3 rounded-lg font-medium bg-white text-slate-700 hover:bg-red-50 hover:text-red-700 border-0 shadow-xs dark:bg-secondary dark:text-secondary-foreground dark:hover:bg-red-950 dark:hover:text-red-400 shrink-0"
onClick={() => setConfirmDisconnectId(connector.id)}
>
<Trash2 className="size-3.5" />
Disconnect
</Button>
)
) : (
<Button
variant="secondary"

View file

@ -54,7 +54,10 @@ export interface InlineMentionEditorRef {
getText: () => string;
getMentionedDocuments: () => MentionedDocument[];
insertMentionChip: (mention: MentionChipInput, options?: { removeTriggerText?: boolean }) => void;
/** @deprecated Use ``insertMentionChip``. */
/**
* @deprecated Use ``insertMentionChip``. Kept for one transition
* cycle so we don't break ad-hoc callers; prefer the new name.
*/
insertDocumentChip: (
doc: Pick<Document, "id" | "title" | "document_type">,
options?: { removeTriggerText?: boolean }

View file

@ -33,8 +33,8 @@ import {
} from "@/components/ui/table";
import { useElectronAPI } from "@/hooks/use-platform";
import { documentsApiService } from "@/lib/apis/documents-api.service";
import { type CitationUrlMap, preprocessCitationMarkdown } from "@/lib/citations/citation-parser";
import { getVirtualPathDisplay } from "@/lib/chat/virtual-path-display";
import { type CitationUrlMap, preprocessCitationMarkdown } from "@/lib/citations/citation-parser";
import { cn } from "@/lib/utils";
function MarkdownCodeBlockSkeleton() {
@ -222,11 +222,7 @@ function FilePathLink({ path, className }: { path: string; className?: string })
: undefined;
const { displayName, isFolder } = getVirtualPathDisplay(path);
const icon = isFolder ? (
<FolderIcon className="size-3.5" />
) : (
<FileIcon className="size-3.5" />
);
const icon = isFolder ? <FolderIcon className="size-3.5" /> : <FileIcon className="size-3.5" />;
const handleClick = useCallback(
(event: React.MouseEvent<HTMLButtonElement>) => {

View file

@ -111,11 +111,7 @@ const UserTextPart: FC = () => {
icon={icon}
label={segment.doc.title}
tooltip={isFolder ? `Folder: ${segment.doc.title}` : segment.doc.title}
onClick={
isFolder
? undefined
: () => handleOpenDoc(segment.doc.id, segment.doc.title)
}
onClick={isFolder ? undefined : () => handleOpenDoc(segment.doc.id, segment.doc.title)}
className="mx-0.5"
/>
);

View file

@ -42,7 +42,7 @@ export interface PlateEditorProps {
editorVariant?: "default" | "demo" | "fullWidth" | "none";
/** Additional className for the container */
className?: string;
/** Save callback. When provided, ⌘+S / Ctrl+S shortcut is registered and save button appears. */
/** Save callback. When provided, ⌘+Shift+S / Ctrl+Shift+S shortcut is registered (avoiding the browser's ⌘+S / Ctrl+S "Save Page As" conflict) and a save button appears in the toolbar. */
onSave?: () => void;
/** Whether there are unsaved changes */
hasUnsavedChanges?: boolean;
@ -170,16 +170,10 @@ export function PlateEditor({
: markdown
? (editor) => {
if (!enableCitations) {
return safeDeserializeMarkdown(
editor,
escapeMdxExpressions(markdown)
) as Value;
return safeDeserializeMarkdown(editor, escapeMdxExpressions(markdown)) as Value;
}
const { content: rewritten, urlMap } = preprocessCitationMarkdown(markdown);
const value = safeDeserializeMarkdown(
editor,
escapeMdxExpressions(rewritten)
);
const value = safeDeserializeMarkdown(editor, escapeMdxExpressions(rewritten));
return injectCitationNodes(value, urlMap) as Value;
}
: undefined,
@ -203,10 +197,7 @@ export function PlateEditor({
let newValue: Descendant[];
if (enableCitations) {
const { content: rewritten, urlMap } = preprocessCitationMarkdown(markdown);
const deserialized = safeDeserializeMarkdown(
editor,
escapeMdxExpressions(rewritten)
);
const deserialized = safeDeserializeMarkdown(editor, escapeMdxExpressions(rewritten));
newValue = injectCitationNodes(deserialized, urlMap);
} else {
newValue = safeDeserializeMarkdown(editor, escapeMdxExpressions(markdown));

View file

@ -49,10 +49,7 @@ export function safeDeserializeMarkdown(
return api.deserialize(markdown, { remarkPlugins: STRICT_PLUGINS }) as Descendant[];
} catch (mdxError) {
if (process.env.NODE_ENV !== "production") {
console.warn(
"[plate-editor] MDX parse failed, retrying without remark-mdx:",
mdxError
);
console.warn("[plate-editor] MDX parse failed, retrying without remark-mdx:", mdxError);
}
try {
return api.deserialize(markdown, { remarkPlugins: LENIENT_PLUGINS }) as Descendant[];

View file

@ -37,7 +37,7 @@ export const Navbar = ({ scrolledBgClassName }: NavbarProps = {}) => {
const navItems = [
{ name: "Free\u00A0AI", link: "/free" },
{ name: "Pricing", link: "/pricing" },
// { name: "Blog", link: "/blog" },
{ name: "Blog", link: "/blog" },
{ name: "Changelog", link: "/changelog" },
{ name: "Docs", link: "/docs" },
{ name: "Contact\u00A0Us", link: "/contact" },

View file

@ -1,7 +1,6 @@
"use client";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { format } from "date-fns";
import { useSetAtom } from "jotai";
import {
ArchiveIcon,
@ -49,6 +48,7 @@ import {
searchThreads,
updateThread,
} from "@/lib/chat/thread-persistence";
import { formatThreadTimestamp } from "@/lib/format-date";
import { cn } from "@/lib/utils";
import { SidebarSlideOutPanel } from "./SidebarSlideOutPanel";
@ -389,8 +389,7 @@ export function AllPrivateChatsSidebarContent({
</TooltipTrigger>
<TooltipContent side="bottom" align="start">
<p>
{t("updated") || "Updated"}:{" "}
{format(new Date(thread.updatedAt), "MMM d, yyyy 'at' h:mm a")}
{t("updated") || "Updated"}: {formatThreadTimestamp(thread.updatedAt)}
</p>
</TooltipContent>
</Tooltip>

View file

@ -1,7 +1,6 @@
"use client";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { format } from "date-fns";
import { useSetAtom } from "jotai";
import {
ArchiveIcon,
@ -49,6 +48,7 @@ import {
searchThreads,
updateThread,
} from "@/lib/chat/thread-persistence";
import { formatThreadTimestamp } from "@/lib/format-date";
import { cn } from "@/lib/utils";
import { SidebarSlideOutPanel } from "./SidebarSlideOutPanel";
@ -388,8 +388,7 @@ export function AllSharedChatsSidebarContent({
</TooltipTrigger>
<TooltipContent side="bottom" align="start">
<p>
{t("updated") || "Updated"}:{" "}
{format(new Date(thread.updatedAt), "MMM d, yyyy 'at' h:mm a")}
{t("updated") || "Updated"}: {formatThreadTimestamp(thread.updatedAt)}
</p>
</TooltipContent>
</Tooltip>

View file

@ -24,10 +24,7 @@ import type React from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { toast } from "sonner";
import { agentFlagsAtom } from "@/atoms/agent/agent-flags-query.atom";
import {
makeFolderMention,
mentionedDocumentsAtom,
} from "@/atoms/chat/mentioned-documents.atom";
import { makeFolderMention, mentionedDocumentsAtom } from "@/atoms/chat/mentioned-documents.atom";
import { connectorDialogOpenAtom } from "@/atoms/connector-dialog/connector-dialog.atoms";
import { connectorsAtom } from "@/atoms/connectors/connector-query.atoms";
import { deleteDocumentMutationAtom } from "@/atoms/documents/document-mutation.atoms";

View file

@ -53,7 +53,7 @@ const THEMES = [
];
const LEARN_MORE_LINKS = [
{ key: "documentation" as const, href: "https://surfsense.com/docs" },
{ key: "documentation" as const, href: "https://www.surfsense.com/docs" },
{ key: "github" as const, href: "https://github.com/MODSetter/SurfSense" },
];

View file

@ -15,7 +15,7 @@ interface BreadcrumbNavProps {
export function BreadcrumbNav({ items, className }: BreadcrumbNavProps) {
const jsonLdItems = items.map((item) => ({
name: item.name,
url: `https://surfsense.com${item.href}`,
url: `https://www.surfsense.com${item.href}`,
}));
return (

View file

@ -16,8 +16,8 @@ export function OrganizationJsonLd() {
"@context": "https://schema.org",
"@type": "Organization",
name: "SurfSense",
url: "https://surfsense.com",
logo: "https://surfsense.com/logo.png",
url: "https://www.surfsense.com",
logo: "https://www.surfsense.com/logo.png",
description:
"Open source NotebookLM alternative for teams with no data limits. Use ChatGPT, Claude AI, and any AI model for free.",
sameAs: ["https://github.com/MODSetter/SurfSense", "https://discord.gg/Cg2M4GUJ"],
@ -38,14 +38,14 @@ export function WebSiteJsonLd() {
"@context": "https://schema.org",
"@type": "WebSite",
name: "SurfSense",
url: "https://surfsense.com",
url: "https://www.surfsense.com",
description:
"Open source NotebookLM alternative for teams with no data limits. Free ChatGPT, Claude AI, and any AI model.",
potentialAction: {
"@type": "SearchAction",
target: {
"@type": "EntryPoint",
urlTemplate: "https://surfsense.com/docs?search={search_term_string}",
urlTemplate: "https://www.surfsense.com/docs?search={search_term_string}",
},
"query-input": "required name=search_term_string",
},
@ -71,7 +71,7 @@ export function SoftwareApplicationJsonLd() {
},
description:
"Open source NotebookLM alternative with free access to ChatGPT, Claude AI, and any model. Connect Slack, Google Drive, Notion, Confluence, GitHub, and dozens more data sources.",
url: "https://surfsense.com",
url: "https://www.surfsense.com",
downloadUrl: "https://github.com/MODSetter/SurfSense/releases",
featureList: [
"Free access to ChatGPT, Claude AI, and any AI model",
@ -95,6 +95,7 @@ export function ArticleJsonLd({
description,
url,
datePublished,
dateModified,
author,
image,
}: {
@ -102,6 +103,7 @@ export function ArticleJsonLd({
description: string;
url: string;
datePublished: string;
dateModified?: string;
author: string;
image?: string;
}) {
@ -114,6 +116,7 @@ export function ArticleJsonLd({
description,
url,
datePublished,
...(dateModified ? { dateModified } : {}),
author: {
"@type": "Organization",
name: author,
@ -123,10 +126,10 @@ export function ArticleJsonLd({
name: "SurfSense",
logo: {
"@type": "ImageObject",
url: "https://surfsense.com/logo.png",
url: "https://www.surfsense.com/logo.png",
},
},
image: image || "https://surfsense.com/og-image.png",
image: image || "https://www.surfsense.com/og-image.png",
mainEntityOfPage: {
"@type": "WebPage",
"@id": url,