feat: enhance Local Folder connector with version history and folder sync capabilities

This commit is contained in:
Anish Sarkar 2026-04-02 11:40:04 +05:30
parent e2f946b7c0
commit 5eeee99bb1
14 changed files with 742 additions and 1 deletions

View file

@ -35,6 +35,7 @@ export function getDocumentTypeLabel(type: string): string {
BOOKSTACK_CONNECTOR: "BookStack",
CIRCLEBACK: "Circleback",
OBSIDIAN_CONNECTOR: "Obsidian",
LOCAL_FOLDER_FILE: "Local Folder",
SURFSENSE_DOCS: "SurfSense Docs",
NOTE: "Note",
COMPOSIO_GOOGLE_DRIVE_CONNECTOR: "Composio Google Drive",

View file

@ -0,0 +1,272 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { FolderSync, Info } from "lucide-react";
import type { FC } from "react";
import { useRef } from "react";
import { useForm } from "react-hook-form";
import * as z from "zod";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { EnumConnectorName } from "@/contracts/enums/connector";
import { getConnectorBenefits } from "../connector-benefits";
import type { ConnectFormProps } from "../index";
const localFolderFormSchema = z.object({
name: z.string().min(3, {
message: "Connector name must be at least 3 characters.",
}),
folder_path: z.string().min(1, {
message: "Folder path is required.",
}),
folder_name: z.string().min(1, {
message: "Folder name is required.",
}),
exclude_patterns: z.string().optional(),
file_extensions: z.string().optional(),
});
type LocalFolderFormValues = z.infer<typeof localFolderFormSchema>;
export const LocalFolderConnectForm: FC<ConnectFormProps> = ({ onSubmit, isSubmitting }) => {
const isSubmittingRef = useRef(false);
const isElectron = typeof window !== "undefined" && !!window.electronAPI;
const form = useForm<LocalFolderFormValues>({
resolver: zodResolver(localFolderFormSchema),
defaultValues: {
name: "Local Folder",
folder_path: "",
folder_name: "",
exclude_patterns: "node_modules,.git,.DS_Store",
file_extensions: "",
},
});
const handleBrowse = async () => {
if (!isElectron) return;
const selected = await window.electronAPI!.selectFolder();
if (selected) {
form.setValue("folder_path", selected);
const folderName = selected.split(/[\\/]/).pop() || "folder";
if (!form.getValues("folder_name")) {
form.setValue("folder_name", folderName);
}
if (form.getValues("name") === "Local Folder") {
form.setValue("name", folderName);
}
}
};
const handleSubmit = async (values: LocalFolderFormValues) => {
if (isSubmittingRef.current || isSubmitting) return;
isSubmittingRef.current = true;
try {
const excludePatterns = values.exclude_patterns
? values.exclude_patterns
.split(",")
.map((p) => p.trim())
.filter(Boolean)
: [];
const fileExtensions = values.file_extensions
? values.file_extensions
.split(",")
.map((e) => {
const ext = e.trim();
return ext.startsWith(".") ? ext : `.${ext}`;
})
.filter(Boolean)
: null;
await onSubmit({
name: values.name,
connector_type: EnumConnectorName.LOCAL_FOLDER_CONNECTOR,
config: {
folder_path: values.folder_path,
folder_name: values.folder_name,
exclude_patterns: excludePatterns,
file_extensions: fileExtensions,
},
is_indexable: true,
is_active: true,
last_indexed_at: null,
periodic_indexing_enabled: false,
indexing_frequency_minutes: null,
next_scheduled_at: null,
});
} finally {
isSubmittingRef.current = false;
}
};
return (
<div className="space-y-6 pb-6">
<Alert className="bg-blue-500/10 dark:bg-blue-500/10 border-blue-500/30 p-2 sm:p-3">
<Info className="size-4 shrink-0 text-blue-500" />
<AlertTitle className="text-xs sm:text-sm">Desktop App Required</AlertTitle>
<AlertDescription className="text-[10px] sm:text-xs">
Real-time file watching is powered by the SurfSense desktop app. Files are
automatically synced whenever changes are detected.
</AlertDescription>
</Alert>
<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">
<Form {...form}>
<form
id="local-folder-connect-form"
onSubmit={form.handleSubmit(handleSubmit)}
className="space-y-4 sm:space-y-6"
>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel className="text-xs sm:text-sm">Connector Name</FormLabel>
<FormControl>
<Input
placeholder="My Documents"
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
disabled={isSubmitting}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="folder_path"
render={({ field }) => (
<FormItem>
<FormLabel className="text-xs sm:text-sm">Folder Path</FormLabel>
<div className="flex gap-2">
<FormControl>
<Input
placeholder="/path/to/your/folder"
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40 font-mono flex-1"
disabled={isSubmitting}
{...field}
/>
</FormControl>
{isElectron && (
<Button
type="button"
variant="outline"
size="sm"
onClick={handleBrowse}
disabled={isSubmitting}
className="shrink-0"
>
<FolderSync className="h-4 w-4 mr-1" />
Browse
</Button>
)}
</div>
<FormDescription className="text-[10px] sm:text-xs">
The absolute path to the folder to watch and sync.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="folder_name"
render={({ field }) => (
<FormItem>
<FormLabel className="text-xs sm:text-sm">Display Name</FormLabel>
<FormControl>
<Input
placeholder="My Notes"
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
disabled={isSubmitting}
{...field}
/>
</FormControl>
<FormDescription className="text-[10px] sm:text-xs">
A friendly name shown in the documents sidebar.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="exclude_patterns"
render={({ field }) => (
<FormItem>
<FormLabel className="text-xs sm:text-sm">Exclude Patterns</FormLabel>
<FormControl>
<Input
placeholder="node_modules,.git,.DS_Store"
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40 font-mono"
disabled={isSubmitting}
{...field}
/>
</FormControl>
<FormDescription className="text-[10px] sm:text-xs">
Comma-separated patterns of directories/files to exclude.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="file_extensions"
render={({ field }) => (
<FormItem>
<FormLabel className="text-xs sm:text-sm">File Extensions (optional)</FormLabel>
<FormControl>
<Input
placeholder=".md,.txt,.rst"
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40 font-mono"
disabled={isSubmitting}
{...field}
/>
</FormControl>
<FormDescription className="text-[10px] sm:text-xs">
Leave empty to index all supported files, or specify comma-separated extensions.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</div>
{getConnectorBenefits(EnumConnectorName.LOCAL_FOLDER_CONNECTOR) && (
<div className="rounded-xl border border-border bg-slate-400/5 dark:bg-white/5 px-3 sm:px-6 py-4 space-y-2">
<h4 className="text-xs sm:text-sm font-medium">
What you get with Local Folder sync:
</h4>
<ul className="list-disc pl-5 text-[10px] sm:text-xs text-muted-foreground space-y-1">
{getConnectorBenefits(EnumConnectorName.LOCAL_FOLDER_CONNECTOR)?.map(
(benefit) => <li key={benefit}>{benefit}</li>
)}
</ul>
</div>
)}
</div>
);
};

View file

@ -111,6 +111,14 @@ export function getConnectorBenefits(connectorType: string): string[] | null {
"Incremental sync - only changed files are re-indexed",
"Full support for your vault's folder structure",
],
LOCAL_FOLDER_CONNECTOR: [
"Watch local folders for real-time changes via the desktop app",
"Automatic change detection — only modified files are re-indexed",
"Version history with up to 20 snapshots per document",
"Mirrors your folder structure in the SurfSense sidebar",
"Supports any text-based file format",
"Works as a periodic sync fallback when the desktop app is not running",
],
};
return benefits[connectorType] || null;

View file

@ -7,6 +7,7 @@ import { GithubConnectForm } from "./components/github-connect-form";
import { LinkupApiConnectForm } from "./components/linkup-api-connect-form";
import { LumaConnectForm } from "./components/luma-connect-form";
import { MCPConnectForm } from "./components/mcp-connect-form";
import { LocalFolderConnectForm } from "./components/local-folder-connect-form";
import { ObsidianConnectForm } from "./components/obsidian-connect-form";
import { TavilyApiConnectForm } from "./components/tavily-api-connect-form";
@ -58,7 +59,8 @@ export function getConnectFormComponent(connectorType: string): ConnectFormCompo
return MCPConnectForm;
case "OBSIDIAN_CONNECTOR":
return ObsidianConnectForm;
// Add other connector types here as needed
case "LOCAL_FOLDER_CONNECTOR":
return LocalFolderConnectForm;
default:
return null;
}

View file

@ -0,0 +1,163 @@
"use client";
import type { FC } from "react";
import { useState } from "react";
import { FolderSync } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import type { ConnectorConfigProps } from "../index";
export const LocalFolderConfig: FC<ConnectorConfigProps> = ({
connector,
onConfigChange,
onNameChange,
}) => {
const isElectron = typeof window !== "undefined" && !!window.electronAPI;
const [folderPath, setFolderPath] = useState<string>(
(connector.config?.folder_path as string) || ""
);
const [folderName, setFolderName] = useState<string>(
(connector.config?.folder_name as string) || ""
);
const [excludePatterns, setExcludePatterns] = useState<string>(() => {
const patterns = connector.config?.exclude_patterns;
if (Array.isArray(patterns)) {
return patterns.join(", ");
}
return (patterns as string) || "node_modules, .git, .DS_Store";
});
const [fileExtensions, setFileExtensions] = useState<string>(() => {
const exts = connector.config?.file_extensions;
if (Array.isArray(exts)) {
return exts.join(", ");
}
return (exts as string) || "";
});
const [name, setName] = useState<string>(connector.name || "");
const handleFolderPathChange = (value: string) => {
setFolderPath(value);
onConfigChange?.({ ...connector.config, folder_path: value });
};
const handleFolderNameChange = (value: string) => {
setFolderName(value);
onConfigChange?.({ ...connector.config, folder_name: value });
};
const handleExcludePatternsChange = (value: string) => {
setExcludePatterns(value);
const arr = value
.split(",")
.map((p) => p.trim())
.filter(Boolean);
onConfigChange?.({ ...connector.config, exclude_patterns: arr });
};
const handleFileExtensionsChange = (value: string) => {
setFileExtensions(value);
const arr = value
? value
.split(",")
.map((e) => {
const ext = e.trim();
return ext.startsWith(".") ? ext : `.${ext}`;
})
.filter(Boolean)
: null;
onConfigChange?.({ ...connector.config, file_extensions: arr });
};
const handleNameChange = (value: string) => {
setName(value);
onNameChange?.(value);
};
const handleBrowse = async () => {
if (!isElectron) return;
const selected = await window.electronAPI!.selectFolder();
if (selected) {
handleFolderPathChange(selected);
const autoName = selected.split(/[\\/]/).pop() || "folder";
if (!folderName) handleFolderNameChange(autoName);
}
};
return (
<div className="space-y-6">
<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-2">
<Label className="text-xs sm:text-sm">Connector Name</Label>
<Input
value={name}
onChange={(e) => handleNameChange(e.target.value)}
placeholder="Local Folder"
className="border-slate-400/20 focus-visible:border-slate-400/40"
/>
</div>
</div>
<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">
<h3 className="font-medium text-sm sm:text-base">Folder Configuration</h3>
<div className="space-y-4">
<div className="space-y-2">
<Label className="text-xs sm:text-sm">Folder Path</Label>
<div className="flex gap-2">
<Input
value={folderPath}
onChange={(e) => handleFolderPathChange(e.target.value)}
placeholder="/path/to/your/folder"
className="border-slate-400/20 focus-visible:border-slate-400/40 font-mono flex-1"
/>
{isElectron && (
<Button type="button" variant="outline" size="sm" onClick={handleBrowse} className="shrink-0">
<FolderSync className="h-4 w-4 mr-1" />
Browse
</Button>
)}
</div>
</div>
<div className="space-y-2">
<Label className="text-xs sm:text-sm">Display Name</Label>
<Input
value={folderName}
onChange={(e) => handleFolderNameChange(e.target.value)}
placeholder="My Notes"
className="border-slate-400/20 focus-visible:border-slate-400/40"
/>
</div>
<div className="space-y-2">
<Label className="text-xs sm:text-sm">Exclude Patterns</Label>
<Input
value={excludePatterns}
onChange={(e) => handleExcludePatternsChange(e.target.value)}
placeholder="node_modules, .git, .DS_Store"
className="border-slate-400/20 focus-visible:border-slate-400/40 font-mono"
/>
<p className="text-[10px] sm:text-xs text-muted-foreground">
Comma-separated patterns of directories/files to exclude.
</p>
</div>
<div className="space-y-2">
<Label className="text-xs sm:text-sm">File Extensions (optional)</Label>
<Input
value={fileExtensions}
onChange={(e) => handleFileExtensionsChange(e.target.value)}
placeholder=".md, .txt, .rst"
className="border-slate-400/20 focus-visible:border-slate-400/40 font-mono"
/>
<p className="text-[10px] sm:text-xs text-muted-foreground">
Leave empty to index all supported files.
</p>
</div>
</div>
</div>
</div>
);
};

View file

@ -19,6 +19,7 @@ import { JiraConfig } from "./components/jira-config";
import { LinkupApiConfig } from "./components/linkup-api-config";
import { LumaConfig } from "./components/luma-config";
import { MCPConfig } from "./components/mcp-config";
import { LocalFolderConfig } from "./components/local-folder-config";
import { ObsidianConfig } from "./components/obsidian-config";
import { OneDriveConfig } from "./components/onedrive-config";
import { SlackConfig } from "./components/slack-config";
@ -82,6 +83,8 @@ export function getConnectorConfigComponent(
return MCPConfig;
case "OBSIDIAN_CONNECTOR":
return ObsidianConfig;
case "LOCAL_FOLDER_CONNECTOR":
return LocalFolderConfig;
case "COMPOSIO_GOOGLE_DRIVE_CONNECTOR":
return ComposioDriveConfig;
case "COMPOSIO_GMAIL_CONNECTOR":

View file

@ -29,6 +29,7 @@ export const CONNECTOR_TO_DOCUMENT_TYPE: Record<string, string> = {
BOOKSTACK_CONNECTOR: "BOOKSTACK_CONNECTOR",
CIRCLEBACK_CONNECTOR: "CIRCLEBACK",
OBSIDIAN_CONNECTOR: "OBSIDIAN_CONNECTOR",
LOCAL_FOLDER_CONNECTOR: "LOCAL_FOLDER_FILE",
// Special mappings (connector type differs from document type)
GOOGLE_DRIVE_CONNECTOR: "GOOGLE_DRIVE_FILE",

View file

@ -0,0 +1,185 @@
"use client";
import { useCallback, useEffect, useState } from "react";
import { Clock, RotateCcw } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
import { Spinner } from "@/components/ui/spinner";
import { documentsApiService } from "@/lib/apis/documents-api.service";
import { toast } from "sonner";
interface DocumentVersionSummary {
version_number: number;
title: string;
content_hash: string;
created_at: string | null;
}
interface VersionHistoryProps {
documentId: number;
documentType: string;
}
export function VersionHistoryButton({ documentId, documentType }: VersionHistoryProps) {
const showVersionHistory = documentType === "LOCAL_FOLDER_FILE" || documentType === "OBSIDIAN_CONNECTOR";
if (!showVersionHistory) return null;
return (
<Sheet>
<SheetTrigger asChild>
<Button variant="ghost" size="sm" className="gap-1.5 text-xs">
<Clock className="h-3.5 w-3.5" />
Versions
</Button>
</SheetTrigger>
<SheetContent className="w-[400px] sm:w-[540px]">
<SheetHeader>
<SheetTitle>Version History</SheetTitle>
</SheetHeader>
<VersionHistoryPanel documentId={documentId} />
</SheetContent>
</Sheet>
);
}
function VersionHistoryPanel({ documentId }: { documentId: number }) {
const [versions, setVersions] = useState<DocumentVersionSummary[]>([]);
const [loading, setLoading] = useState(true);
const [selectedVersion, setSelectedVersion] = useState<number | null>(null);
const [versionContent, setVersionContent] = useState<string>("");
const [contentLoading, setContentLoading] = useState(false);
const [restoring, setRestoring] = useState(false);
const loadVersions = useCallback(async () => {
setLoading(true);
try {
const data = await documentsApiService.listDocumentVersions(documentId);
setVersions(data as DocumentVersionSummary[]);
} catch {
toast.error("Failed to load version history");
} finally {
setLoading(false);
}
}, [documentId]);
useEffect(() => {
loadVersions();
}, [loadVersions]);
const handleSelectVersion = async (versionNumber: number) => {
setSelectedVersion(versionNumber);
setContentLoading(true);
try {
const data = (await documentsApiService.getDocumentVersion(
documentId,
versionNumber
)) as { source_markdown: string };
setVersionContent(data.source_markdown || "");
} catch {
toast.error("Failed to load version content");
} finally {
setContentLoading(false);
}
};
const handleRestore = async (versionNumber: number) => {
setRestoring(true);
try {
await documentsApiService.restoreDocumentVersion(documentId, versionNumber);
toast.success(`Restored version ${versionNumber}`);
await loadVersions();
} catch {
toast.error("Failed to restore version");
} finally {
setRestoring(false);
}
};
if (loading) {
return (
<div className="flex items-center justify-center py-12">
<Spinner size="lg" className="text-muted-foreground" />
</div>
);
}
if (versions.length === 0) {
return (
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground">
<Clock className="h-8 w-8 mb-2 opacity-50" />
<p className="text-sm">No version history available yet.</p>
<p className="text-xs mt-1">Versions are created when file content changes.</p>
</div>
);
}
return (
<div className="flex flex-col gap-4 pt-4 h-full">
<div className="flex-1 overflow-y-auto space-y-2">
{versions.map((v) => (
<div
key={v.version_number}
className={`rounded-lg border p-3 cursor-pointer transition-colors ${
selectedVersion === v.version_number
? "border-primary bg-primary/5"
: "border-border hover:border-primary/50"
}`}
onClick={() => handleSelectVersion(v.version_number)}
>
<div className="flex items-center justify-between">
<div className="space-y-1">
<p className="text-sm font-medium">Version {v.version_number}</p>
{v.created_at && (
<p className="text-xs text-muted-foreground">
{new Date(v.created_at).toLocaleString()}
</p>
)}
{v.title && (
<p className="text-xs text-muted-foreground truncate max-w-[200px]">
{v.title}
</p>
)}
</div>
<Button
variant="outline"
size="sm"
className="shrink-0 gap-1"
disabled={restoring}
onClick={(e) => {
e.stopPropagation();
handleRestore(v.version_number);
}}
>
<RotateCcw className="h-3 w-3" />
Restore
</Button>
</div>
</div>
))}
</div>
{selectedVersion !== null && (
<div className="border-t pt-4 max-h-[40vh] overflow-y-auto">
<h4 className="text-sm font-medium mb-2">
Preview Version {selectedVersion}
</h4>
{contentLoading ? (
<div className="flex items-center justify-center py-6">
<Spinner size="sm" />
</div>
) : (
<pre className="text-xs whitespace-pre-wrap font-mono bg-muted/50 rounded-lg p-3 max-h-[30vh] overflow-y-auto">
{versionContent || "(empty)"}
</pre>
)}
</div>
)}
</div>
);
}

View file

@ -26,6 +26,7 @@ export const documentTypeEnum = z.enum([
"BOOKSTACK_CONNECTOR",
"CIRCLEBACK",
"OBSIDIAN_CONNECTOR",
"LOCAL_FOLDER_FILE",
"SURFSENSE_DOCS",
"NOTE",
"COMPOSIO_GOOGLE_DRIVE_CONNECTOR",

View file

@ -0,0 +1,41 @@
"use client";
import { useEffect, useRef } from "react";
import { connectorsApiService } from "@/lib/apis/connectors-api.service";
const DEBOUNCE_MS = 2000;
export function useFolderSync() {
const pendingRef = useRef<Map<string, ReturnType<typeof setTimeout>>>(new Map());
useEffect(() => {
const api = typeof window !== "undefined" ? window.electronAPI : null;
if (!api?.onFileChanged) return;
const cleanup = api.onFileChanged((event) => {
const key = `${event.connectorId}:${event.fullPath}`;
const existing = pendingRef.current.get(key);
if (existing) clearTimeout(existing);
const timeout = setTimeout(async () => {
pendingRef.current.delete(key);
try {
await connectorsApiService.indexFile(event.connectorId, event.fullPath);
} catch (err) {
console.error("[FolderSync] Failed to trigger re-index:", err);
}
}, DEBOUNCE_MS);
pendingRef.current.set(key, timeout);
});
return () => {
cleanup();
for (const timeout of pendingRef.current.values()) {
clearTimeout(timeout);
}
pendingRef.current.clear();
};
}, []);
}

View file

@ -404,6 +404,18 @@ class ConnectorsApiService {
listDiscordChannelsResponse
);
};
// =============================================================================
// Local Folder Connector Methods
// =============================================================================
indexFile = async (connectorId: number, filePath: string) => {
return baseApiService.post(
`/api/v1/search-source-connectors/${connectorId}/index-file`,
undefined,
{ body: { file_path: filePath } }
);
};
}
export type { SlackChannel, DiscordChannel };

View file

@ -379,6 +379,22 @@ class DocumentsApiService {
});
};
listDocumentVersions = async (documentId: number) => {
return baseApiService.get(`/api/v1/documents/${documentId}/versions`);
};
getDocumentVersion = async (documentId: number, versionNumber: number) => {
return baseApiService.get(
`/api/v1/documents/${documentId}/versions/${versionNumber}`
);
};
restoreDocumentVersion = async (documentId: number, versionNumber: number) => {
return baseApiService.post(
`/api/v1/documents/${documentId}/versions/${versionNumber}/restore`
);
};
/**
* Delete a document
*/

View file

@ -30,6 +30,7 @@ export const getConnectorTypeDisplay = (type: string): string => {
YOUTUBE_CONNECTOR: "YouTube",
CIRCLEBACK_CONNECTOR: "Circleback",
OBSIDIAN_CONNECTOR: "Obsidian",
LOCAL_FOLDER_CONNECTOR: "Local Folder",
DROPBOX_CONNECTOR: "Dropbox",
MCP_CONNECTOR: "MCP Server",
};

View file

@ -1,5 +1,30 @@
import type { PostHog } from "posthog-js";
interface WatchedFolderConfig {
path: string;
name: string;
excludePatterns: string[];
fileExtensions: string[] | null;
connectorId: number;
searchSpaceId: number;
active: boolean;
}
interface FolderSyncFileChangedEvent {
connectorId: number;
searchSpaceId: number;
folderPath: string;
relativePath: string;
fullPath: string;
action: "add" | "change" | "unlink";
timestamp: number;
}
interface FolderSyncWatcherReadyEvent {
connectorId: number;
folderPath: string;
}
interface ElectronAPI {
versions: {
electron: string;
@ -14,6 +39,16 @@ interface ElectronAPI {
setQuickAskMode: (mode: string) => Promise<void>;
getQuickAskMode: () => Promise<string>;
replaceText: (text: string) => Promise<void>;
// Folder sync
selectFolder: () => Promise<string | null>;
addWatchedFolder: (config: WatchedFolderConfig) => Promise<WatchedFolderConfig[]>;
removeWatchedFolder: (folderPath: string) => Promise<WatchedFolderConfig[]>;
getWatchedFolders: () => Promise<WatchedFolderConfig[]>;
getWatcherStatus: () => Promise<{ path: string; active: boolean; watching: boolean }[]>;
onFileChanged: (callback: (data: FolderSyncFileChangedEvent) => void) => () => void;
onWatcherReady: (callback: (data: FolderSyncWatcherReadyEvent) => void) => () => void;
pauseWatcher: () => Promise<void>;
resumeWatcher: () => Promise<void>;
}
declare global {