Merge remote-tracking branch 'upstream/dev' into electon-desktop

This commit is contained in:
CREDO23 2026-03-29 10:41:05 +02:00
commit ab3c636bcd
85 changed files with 4642 additions and 414 deletions

View file

@ -40,6 +40,10 @@ import {
CreateGoogleDriveFileToolUI,
DeleteGoogleDriveFileToolUI,
} from "@/components/tool-ui/google-drive";
import {
CreateOneDriveFileToolUI,
DeleteOneDriveFileToolUI,
} from "@/components/tool-ui/onedrive";
import {
CreateJiraIssueToolUI,
DeleteJiraIssueToolUI,
@ -97,6 +101,8 @@ const AssistantMessageInner: FC = () => {
delete_linear_issue: DeleteLinearIssueToolUI,
create_google_drive_file: CreateGoogleDriveFileToolUI,
delete_google_drive_file: DeleteGoogleDriveFileToolUI,
create_onedrive_file: CreateOneDriveFileToolUI,
delete_onedrive_file: DeleteOneDriveFileToolUI,
create_calendar_event: CreateCalendarEventToolUI,
update_calendar_event: UpdateCalendarEventToolUI,
delete_calendar_event: DeleteCalendarEventToolUI,

View file

@ -340,10 +340,11 @@ export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, Connector
onBack={handleBackFromEdit}
onQuickIndex={(() => {
const cfg = connectorConfig || editingConnector.config;
const isDrive =
editingConnector.connector_type === "GOOGLE_DRIVE_CONNECTOR" ||
editingConnector.connector_type === "COMPOSIO_GOOGLE_DRIVE_CONNECTOR";
const hasDriveItems = isDrive
const isDriveOrOneDrive =
editingConnector.connector_type === "GOOGLE_DRIVE_CONNECTOR" ||
editingConnector.connector_type === "COMPOSIO_GOOGLE_DRIVE_CONNECTOR" ||
editingConnector.connector_type === "ONEDRIVE_CONNECTOR";
const hasDriveItems = isDriveOrOneDrive
? ((cfg?.selected_folders as unknown[]) ?? []).length > 0 ||
((cfg?.selected_files as unknown[]) ?? []).length > 0
: true;

View file

@ -13,7 +13,7 @@ import {
} from "lucide-react";
import type { FC } from "react";
import { useCallback, useEffect, useState } from "react";
import { ComposioDriveFolderTree } from "@/components/connectors/composio-drive-folder-tree";
import { DriveFolderTree, type SelectedFolder } from "@/components/connectors/drive-folder-tree";
import { Label } from "@/components/ui/label";
import {
Select,
@ -23,13 +23,9 @@ import {
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { connectorsApiService } from "@/lib/apis/connectors-api.service";
import type { ConnectorConfigProps } from "../index";
interface SelectedFolder {
id: string;
name: string;
}
interface IndexingOptions {
max_files_per_folder: number;
incremental_sync: boolean;
@ -102,6 +98,16 @@ export const ComposioDriveConfig: FC<ConnectorConfigProps> = ({ connector, onCon
setAuthError(true);
}, []);
const fetchItems = useCallback(
async (parentId?: string) => {
return connectorsApiService.listComposioDriveFolders({
connector_id: connector.id,
parent_id: parentId,
});
},
[connector.id]
);
const [isEditMode] = useState(() => existingFolders.length > 0 || existingFiles.length > 0);
const [isFolderTreeOpen, setIsFolderTreeOpen] = useState(!isEditMode);
@ -255,24 +261,28 @@ export const ComposioDriveConfig: FC<ConnectorConfigProps> = ({ connector, onCon
)}
</button>
{isFolderTreeOpen && (
<ComposioDriveFolderTree
connectorId={connector.id}
<DriveFolderTree
fetchItems={fetchItems}
selectedFolders={selectedFolders}
onSelectFolders={handleSelectFolders}
selectedFiles={selectedFiles}
onSelectFiles={handleSelectFiles}
onAuthError={handleAuthError}
rootLabel="My Drive"
providerName="Google Drive"
/>
)}
</div>
) : (
<ComposioDriveFolderTree
connectorId={connector.id}
<DriveFolderTree
fetchItems={fetchItems}
selectedFolders={selectedFolders}
onSelectFolders={handleSelectFolders}
selectedFiles={selectedFiles}
onSelectFiles={handleSelectFiles}
onAuthError={handleAuthError}
rootLabel="My Drive"
providerName="Google Drive"
/>
)}
</div>

View file

@ -242,8 +242,6 @@ export const GoogleDriveConfig: FC<ConnectorConfigProps> = ({ connector, onConfi
{totalSelected > 0 ? "Change Selection" : "Select from Google Drive"}
</Button>
{pickerError && !isAuthExpired && <p className="text-xs text-destructive">{pickerError}</p>}
{isAuthExpired && (
<p className="text-xs text-amber-600 dark:text-amber-500">
Your Google Drive authentication has expired. Please re-authenticate using the button

View file

@ -0,0 +1,350 @@
"use client";
import {
ChevronDown,
ChevronRight,
File,
FileSpreadsheet,
FileText,
FolderClosed,
Image,
Presentation,
X,
} from "lucide-react";
import type { FC } from "react";
import { useCallback, useEffect, useState } from "react";
import { DriveFolderTree, type SelectedFolder } from "@/components/connectors/drive-folder-tree";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { connectorsApiService } from "@/lib/apis/connectors-api.service";
import type { ConnectorConfigProps } from "../index";
interface IndexingOptions {
max_files_per_folder: number;
incremental_sync: boolean;
include_subfolders: boolean;
}
const DEFAULT_INDEXING_OPTIONS: IndexingOptions = {
max_files_per_folder: 100,
incremental_sync: true,
include_subfolders: true,
};
function getFileIconFromName(fileName: string, className: string = "size-3.5 shrink-0") {
const lowerName = fileName.toLowerCase();
if (lowerName.endsWith(".xlsx") || lowerName.endsWith(".xls") || lowerName.endsWith(".csv")) {
return <FileSpreadsheet className={`${className} text-muted-foreground`} />;
}
if (lowerName.endsWith(".pptx") || lowerName.endsWith(".ppt")) {
return <Presentation className={`${className} text-muted-foreground`} />;
}
if (lowerName.endsWith(".docx") || lowerName.endsWith(".doc") || lowerName.endsWith(".txt")) {
return <FileText className={`${className} text-muted-foreground`} />;
}
if (/\.(png|jpe?g|gif|webp|svg)$/.test(lowerName)) {
return <Image className={`${className} text-muted-foreground`} />;
}
return <File className={`${className} text-muted-foreground`} />;
}
export const OneDriveConfig: FC<ConnectorConfigProps> = ({ connector, onConfigChange }) => {
const existingFolders =
(connector.config?.selected_folders as SelectedFolder[] | undefined) || [];
const existingFiles = (connector.config?.selected_files as SelectedFolder[] | undefined) || [];
const existingIndexingOptions =
(connector.config?.indexing_options as IndexingOptions | undefined) || DEFAULT_INDEXING_OPTIONS;
const [selectedFolders, setSelectedFolders] = useState<SelectedFolder[]>(existingFolders);
const [selectedFiles, setSelectedFiles] = useState<SelectedFolder[]>(existingFiles);
const [indexingOptions, setIndexingOptions] = useState<IndexingOptions>(existingIndexingOptions);
const [authError, setAuthError] = useState(false);
const isAuthExpired = connector.config?.auth_expired === true || authError;
const handleAuthError = useCallback(() => {
setAuthError(true);
}, []);
const fetchItems = useCallback(
async (parentId?: string) => {
return connectorsApiService.listOneDriveFolders({
connector_id: connector.id,
parent_id: parentId,
});
},
[connector.id]
);
const [isEditMode] = useState(() => existingFolders.length > 0 || existingFiles.length > 0);
const [isFolderTreeOpen, setIsFolderTreeOpen] = useState(!isEditMode);
useEffect(() => {
const folders = (connector.config?.selected_folders as SelectedFolder[] | undefined) || [];
const files = (connector.config?.selected_files as SelectedFolder[] | undefined) || [];
const options =
(connector.config?.indexing_options as IndexingOptions | undefined) ||
DEFAULT_INDEXING_OPTIONS;
setSelectedFolders(folders);
setSelectedFiles(files);
setIndexingOptions(options);
}, [connector.config]);
const updateConfig = (
folders: SelectedFolder[],
files: SelectedFolder[],
options: IndexingOptions
) => {
if (onConfigChange) {
onConfigChange({
...connector.config,
selected_folders: folders,
selected_files: files,
indexing_options: options,
});
}
};
const handleSelectFolders = (folders: SelectedFolder[]) => {
setSelectedFolders(folders);
updateConfig(folders, selectedFiles, indexingOptions);
};
const handleSelectFiles = (files: SelectedFolder[]) => {
setSelectedFiles(files);
updateConfig(selectedFolders, files, indexingOptions);
};
const handleIndexingOptionChange = (key: keyof IndexingOptions, value: number | boolean) => {
const newOptions = { ...indexingOptions, [key]: value };
setIndexingOptions(newOptions);
updateConfig(selectedFolders, selectedFiles, newOptions);
};
const handleRemoveFolder = (folderId: string) => {
const newFolders = selectedFolders.filter((folder) => folder.id !== folderId);
setSelectedFolders(newFolders);
updateConfig(newFolders, selectedFiles, indexingOptions);
};
const handleRemoveFile = (fileId: string) => {
const newFiles = selectedFiles.filter((file) => file.id !== fileId);
setSelectedFiles(newFiles);
updateConfig(selectedFolders, newFiles, indexingOptions);
};
const totalSelected = selectedFolders.length + selectedFiles.length;
return (
<div className="space-y-6">
{/* Folder & File Selection */}
<div className="rounded-xl border border-border bg-slate-400/5 dark:bg-white/5 p-3 sm:p-6 space-y-3 sm:space-y-4">
<div className="space-y-1 sm:space-y-2">
<h3 className="font-medium text-sm sm:text-base">Folder & File Selection</h3>
<p className="text-xs sm:text-sm text-muted-foreground">
Select specific folders and/or individual files to index from your OneDrive.
</p>
</div>
{totalSelected > 0 && (
<div className="p-2 sm:p-3 bg-muted rounded-lg text-xs sm:text-sm space-y-1 sm:space-y-2">
<p className="font-medium">
Selected {totalSelected} item{totalSelected > 1 ? "s" : ""}: {(() => {
const parts: string[] = [];
if (selectedFolders.length > 0) {
parts.push(
`${selectedFolders.length} folder${selectedFolders.length > 1 ? "s" : ""}`
);
}
if (selectedFiles.length > 0) {
parts.push(`${selectedFiles.length} file${selectedFiles.length > 1 ? "s" : ""}`);
}
return parts.length > 0 ? `(${parts.join(", ")})` : "";
})()}
</p>
<div className="max-h-20 sm:max-h-24 overflow-y-auto space-y-1">
{selectedFolders.map((folder) => (
<div
key={folder.id}
className="text-xs sm:text-sm text-muted-foreground truncate flex items-center gap-1.5"
title={folder.name}
>
<FolderClosed className="size-3.5 shrink-0 text-muted-foreground" />
<span className="flex-1 truncate">{folder.name}</span>
<button
type="button"
onClick={() => handleRemoveFolder(folder.id)}
className="shrink-0 p-0.5 hover:bg-muted-foreground/20 rounded transition-colors"
aria-label={`Remove ${folder.name}`}
>
<X className="size-3.5" />
</button>
</div>
))}
{selectedFiles.map((file) => (
<div
key={file.id}
className="text-xs sm:text-sm text-muted-foreground truncate flex items-center gap-1.5"
title={file.name}
>
{getFileIconFromName(file.name)}
<span className="flex-1 truncate">{file.name}</span>
<button
type="button"
onClick={() => handleRemoveFile(file.id)}
className="shrink-0 p-0.5 hover:bg-muted-foreground/20 rounded transition-colors"
aria-label={`Remove ${file.name}`}
>
<X className="size-3.5" />
</button>
</div>
))}
</div>
</div>
)}
{isAuthExpired && (
<p className="text-xs text-amber-600 dark:text-amber-500">
Your OneDrive authentication has expired. Please re-authenticate using the button
below.
</p>
)}
{isEditMode ? (
<div className="space-y-2">
<button
type="button"
onClick={() => setIsFolderTreeOpen(!isFolderTreeOpen)}
className="flex items-center gap-2 text-xs sm:text-sm text-muted-foreground hover:text-foreground transition-colors w-fit"
>
Change Selection
{isFolderTreeOpen ? (
<ChevronDown className="size-4" />
) : (
<ChevronRight className="size-4" />
)}
</button>
{isFolderTreeOpen && (
<DriveFolderTree
fetchItems={fetchItems}
selectedFolders={selectedFolders}
onSelectFolders={handleSelectFolders}
selectedFiles={selectedFiles}
onSelectFiles={handleSelectFiles}
onAuthError={handleAuthError}
rootLabel="OneDrive"
providerName="OneDrive"
/>
)}
</div>
) : (
<DriveFolderTree
fetchItems={fetchItems}
selectedFolders={selectedFolders}
onSelectFolders={handleSelectFolders}
selectedFiles={selectedFiles}
onSelectFiles={handleSelectFiles}
onAuthError={handleAuthError}
rootLabel="OneDrive"
providerName="OneDrive"
/>
)}
</div>
{/* Indexing Options */}
<div className="rounded-xl border border-border bg-slate-400/5 dark:bg-white/5 p-3 sm:p-6 space-y-4">
<div className="space-y-1 sm:space-y-2">
<h3 className="font-medium text-sm sm:text-base">Indexing Options</h3>
<p className="text-xs sm:text-sm text-muted-foreground">
Configure how files are indexed from your OneDrive.
</p>
</div>
{/* Max files per folder */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="od-max-files" className="text-sm font-medium">
Max files per folder
</Label>
<p className="text-xs text-muted-foreground">
Maximum number of files to index from each folder
</p>
</div>
<Select
value={indexingOptions.max_files_per_folder.toString()}
onValueChange={(value) =>
handleIndexingOptionChange("max_files_per_folder", parseInt(value, 10))
}
>
<SelectTrigger
id="od-max-files"
className="w-[140px] bg-slate-400/5 dark:bg-slate-400/5 border-slate-400/20 text-xs sm:text-sm"
>
<SelectValue placeholder="Select limit" />
</SelectTrigger>
<SelectContent className="z-[100]">
<SelectItem value="50" className="text-xs sm:text-sm">
50 files
</SelectItem>
<SelectItem value="100" className="text-xs sm:text-sm">
100 files
</SelectItem>
<SelectItem value="250" className="text-xs sm:text-sm">
250 files
</SelectItem>
<SelectItem value="500" className="text-xs sm:text-sm">
500 files
</SelectItem>
<SelectItem value="1000" className="text-xs sm:text-sm">
1000 files
</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{/* Incremental sync toggle */}
<div className="flex items-center justify-between pt-2 border-t border-slate-400/20">
<div className="space-y-0.5">
<Label htmlFor="od-incremental-sync" className="text-sm font-medium">
Incremental sync
</Label>
<p className="text-xs text-muted-foreground">
Only sync changes since last index (faster). Disable for a full re-index.
</p>
</div>
<Switch
id="od-incremental-sync"
checked={indexingOptions.incremental_sync}
onCheckedChange={(checked) => handleIndexingOptionChange("incremental_sync", checked)}
/>
</div>
{/* Include subfolders toggle */}
<div className="flex items-center justify-between pt-2 border-t border-slate-400/20">
<div className="space-y-0.5">
<Label htmlFor="od-include-subfolders" className="text-sm font-medium">
Include subfolders
</Label>
<p className="text-xs text-muted-foreground">
Recursively index files in subfolders of selected folders
</p>
</div>
<Switch
id="od-include-subfolders"
checked={indexingOptions.include_subfolders}
onCheckedChange={(checked) => handleIndexingOptionChange("include_subfolders", checked)}
/>
</div>
</div>
</div>
);
};

View file

@ -21,6 +21,7 @@ import { MCPConfig } from "./components/mcp-config";
import { ObsidianConfig } from "./components/obsidian-config";
import { SlackConfig } from "./components/slack-config";
import { TavilyApiConfig } from "./components/tavily-api-config";
import { OneDriveConfig } from "./components/onedrive-config";
import { TeamsConfig } from "./components/teams-config";
import { WebcrawlerConfig } from "./components/webcrawler-config";
@ -58,6 +59,8 @@ export function getConnectorConfigComponent(
return DiscordConfig;
case "TEAMS_CONNECTOR":
return TeamsConfig;
case "ONEDRIVE_CONNECTOR":
return OneDriveConfig;
case "CONFLUENCE_CONNECTOR":
return ConfluenceConfig;
case "BOOKSTACK_CONNECTOR":

View file

@ -27,6 +27,7 @@ const REAUTH_ENDPOINTS: Partial<Record<string, string>> = {
[EnumConnectorName.COMPOSIO_GOOGLE_DRIVE_CONNECTOR]: "/api/v1/auth/composio/connector/reauth",
[EnumConnectorName.COMPOSIO_GMAIL_CONNECTOR]: "/api/v1/auth/composio/connector/reauth",
[EnumConnectorName.COMPOSIO_GOOGLE_CALENDAR_CONNECTOR]: "/api/v1/auth/composio/connector/reauth",
[EnumConnectorName.ONEDRIVE_CONNECTOR]: "/api/v1/auth/onedrive/connector/reauth",
};
interface ConnectorEditViewProps {

View file

@ -61,6 +61,13 @@ export const OAUTH_CONNECTORS = [
connectorType: EnumConnectorName.TEAMS_CONNECTOR,
authEndpoint: "/api/v1/auth/teams/connector/add/",
},
{
id: "onedrive-connector",
title: "OneDrive",
description: "Search your OneDrive files",
connectorType: EnumConnectorName.ONEDRIVE_CONNECTOR,
authEndpoint: "/api/v1/auth/onedrive/connector/add/",
},
{
id: "discord-connector",
title: "Discord",

View file

@ -729,10 +729,11 @@ export const useConnectorDialog = () => {
async (refreshConnectors: () => void) => {
if (!indexingConfig || !searchSpaceId) return;
// Validate date range (skip for Google Drive, Composio Drive, and Webcrawler)
// Validate date range (skip for Google Drive, Composio Drive, OneDrive, and Webcrawler)
if (
indexingConfig.connectorType !== "GOOGLE_DRIVE_CONNECTOR" &&
indexingConfig.connectorType !== "COMPOSIO_GOOGLE_DRIVE_CONNECTOR" &&
indexingConfig.connectorType !== "ONEDRIVE_CONNECTOR" &&
indexingConfig.connectorType !== "WEBCRAWLER_CONNECTOR"
) {
const dateRangeValidation = dateRangeSchema.safeParse({ startDate, endDate });
@ -778,10 +779,11 @@ export const useConnectorDialog = () => {
});
}
// Handle Google Drive folder selection (regular and Composio)
if (
(indexingConfig.connectorType === "GOOGLE_DRIVE_CONNECTOR" ||
indexingConfig.connectorType === "COMPOSIO_GOOGLE_DRIVE_CONNECTOR") &&
// Handle Google Drive / OneDrive folder selection (regular and Composio)
if (
(indexingConfig.connectorType === "GOOGLE_DRIVE_CONNECTOR" ||
indexingConfig.connectorType === "COMPOSIO_GOOGLE_DRIVE_CONNECTOR" ||
indexingConfig.connectorType === "ONEDRIVE_CONNECTOR") &&
indexingConnectorConfig
) {
const selectedFolders = indexingConnectorConfig.selected_folders as
@ -967,10 +969,11 @@ export const useConnectorDialog = () => {
async (refreshConnectors: () => void) => {
if (!editingConnector || !searchSpaceId || isSaving) return;
// Validate date range (skip for Google Drive which uses folder selection, Webcrawler which uses config, and non-indexable connectors)
// Validate date range (skip for Google Drive/OneDrive which uses folder selection, Webcrawler which uses config, and non-indexable connectors)
if (
editingConnector.is_indexable &&
editingConnector.connector_type !== "GOOGLE_DRIVE_CONNECTOR" &&
editingConnector.connector_type !== "ONEDRIVE_CONNECTOR" &&
editingConnector.connector_type !== "WEBCRAWLER_CONNECTOR"
) {
const dateRangeValidation = dateRangeSchema.safeParse({ startDate, endDate });
@ -986,11 +989,12 @@ export const useConnectorDialog = () => {
return;
}
// Prevent periodic indexing for Google Drive (regular or Composio) without folders/files selected
// Prevent periodic indexing for Google Drive / OneDrive (regular or Composio) without folders/files selected
if (
periodicEnabled &&
(editingConnector.connector_type === "GOOGLE_DRIVE_CONNECTOR" ||
editingConnector.connector_type === "COMPOSIO_GOOGLE_DRIVE_CONNECTOR")
editingConnector.connector_type === "COMPOSIO_GOOGLE_DRIVE_CONNECTOR" ||
editingConnector.connector_type === "ONEDRIVE_CONNECTOR")
) {
const selectedFolders = (connectorConfig || editingConnector.config)?.selected_folders as
| Array<{ id: string; name: string }>
@ -1043,7 +1047,8 @@ export const useConnectorDialog = () => {
indexingDescription = "Settings saved.";
} else if (
editingConnector.connector_type === "GOOGLE_DRIVE_CONNECTOR" ||
editingConnector.connector_type === "COMPOSIO_GOOGLE_DRIVE_CONNECTOR"
editingConnector.connector_type === "COMPOSIO_GOOGLE_DRIVE_CONNECTOR" ||
editingConnector.connector_type === "ONEDRIVE_CONNECTOR"
) {
// Google Drive (both regular and Composio) uses folder selection from config, not date ranges
const selectedFolders = (connectorConfig || editingConnector.config)?.selected_folders as

View file

@ -12,6 +12,7 @@ export const CONNECTOR_TO_DOCUMENT_TYPE: Record<string, string> = {
// Direct mappings (connector type matches document type)
SLACK_CONNECTOR: "SLACK_CONNECTOR",
TEAMS_CONNECTOR: "TEAMS_CONNECTOR",
ONEDRIVE_CONNECTOR: "ONEDRIVE_FILE",
NOTION_CONNECTOR: "NOTION_CONNECTOR",
GITHUB_CONNECTOR: "GITHUB_CONNECTOR",
LINEAR_CONNECTOR: "LINEAR_CONNECTOR",

View file

@ -25,6 +25,7 @@ const REAUTH_ENDPOINTS: Partial<Record<string, string>> = {
[EnumConnectorName.COMPOSIO_GOOGLE_DRIVE_CONNECTOR]: "/api/v1/auth/composio/connector/reauth",
[EnumConnectorName.COMPOSIO_GMAIL_CONNECTOR]: "/api/v1/auth/composio/connector/reauth",
[EnumConnectorName.COMPOSIO_GOOGLE_CALENDAR_CONNECTOR]: "/api/v1/auth/composio/connector/reauth",
[EnumConnectorName.ONEDRIVE_CONNECTOR]: "/api/v1/auth/onedrive/connector/reauth",
[EnumConnectorName.JIRA_CONNECTOR]: "/api/v1/auth/jira/connector/reauth",
[EnumConnectorName.CONFLUENCE_CONNECTOR]: "/api/v1/auth/confluence/connector/reauth",
};

View file

@ -394,7 +394,7 @@ const defaultComponents = memoizeMarkdownComponents({
if (!isCodeBlock) {
return (
<code
className={cn("aui-md-inline-code rounded border bg-muted font-semibold", className)}
className={cn("aui-md-inline-code rounded-md border bg-muted px-1.5 py-0.5 font-mono text-[0.9em] font-normal", className)}
{...props}
>
{children}

View file

@ -1267,6 +1267,12 @@ const TOOL_GROUPS: ToolGroup[] = [
connectorIcon: "google_drive",
tooltip: "Create and delete files in Google Drive.",
},
{
label: "OneDrive",
tools: ["create_onedrive_file", "delete_onedrive_file"],
connectorIcon: "onedrive",
tooltip: "Create and delete files in OneDrive.",
},
{
label: "Notion",
tools: ["create_notion_page", "update_notion_page", "delete_notion_page"],