diff --git a/surfsense_desktop/src/ipc/handlers.ts b/surfsense_desktop/src/ipc/handlers.ts index cc84a46e0..247d171f5 100644 --- a/surfsense_desktop/src/ipc/handlers.ts +++ b/surfsense_desktop/src/ipc/handlers.ts @@ -228,7 +228,7 @@ export function registerIpcHandlers(): void { ipcMain.handle( IPC_CHANNELS.AGENT_FILESYSTEM_SET_SETTINGS, - (_event, settings: { mode?: 'cloud' | 'desktop_local_folder'; localRootPath?: string | null }) => + (_event, settings: { mode?: 'cloud' | 'desktop_local_folder'; localRootPaths?: string[] | null }) => setAgentFilesystemSettings(settings) ); diff --git a/surfsense_desktop/src/modules/agent-filesystem.ts b/surfsense_desktop/src/modules/agent-filesystem.ts index 9dfe79fb0..afad98f24 100644 --- a/surfsense_desktop/src/modules/agent-filesystem.ts +++ b/surfsense_desktop/src/modules/agent-filesystem.ts @@ -1,16 +1,17 @@ import { app, dialog } from "electron"; -import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { access, mkdir, readFile, writeFile } from "node:fs/promises"; import { dirname, isAbsolute, join, relative, resolve } from "node:path"; export type AgentFilesystemMode = "cloud" | "desktop_local_folder"; export interface AgentFilesystemSettings { mode: AgentFilesystemMode; - localRootPath: string | null; + localRootPaths: string[]; updatedAt: string; } const SETTINGS_FILENAME = "agent-filesystem-settings.json"; +const MAX_LOCAL_ROOTS = 5; function getSettingsPath(): string { return join(app.getPath("userData"), SETTINGS_FILENAME); @@ -19,11 +20,28 @@ function getSettingsPath(): string { function getDefaultSettings(): AgentFilesystemSettings { return { mode: "cloud", - localRootPath: null, + localRootPaths: [], updatedAt: new Date().toISOString(), }; } +function normalizeLocalRootPaths(paths: unknown): string[] { + if (!Array.isArray(paths)) { + return []; + } + const uniquePaths = new Set(); + for (const path of paths) { + if (typeof path !== "string") continue; + const trimmed = path.trim(); + if (!trimmed) continue; + uniquePaths.add(trimmed); + if (uniquePaths.size >= MAX_LOCAL_ROOTS) { + break; + } + } + return [...uniquePaths]; +} + export async function getAgentFilesystemSettings(): Promise { try { const raw = await readFile(getSettingsPath(), "utf8"); @@ -33,7 +51,7 @@ export async function getAgentFilesystemSettings(): Promise> + settings: { + mode?: AgentFilesystemMode; + localRootPaths?: string[] | null; + } ): Promise { const current = await getAgentFilesystemSettings(); const nextMode = @@ -51,8 +72,10 @@ export async function setAgentFilesystemSettings( : current.mode; const next: AgentFilesystemSettings = { mode: nextMode, - localRootPath: - settings.localRootPath === undefined ? current.localRootPath : settings.localRootPath, + localRootPaths: + settings.localRootPaths === undefined + ? current.localRootPaths + : normalizeLocalRootPaths(settings.localRootPaths ?? []), updatedAt: new Date().toISOString(), }; @@ -101,20 +124,45 @@ function toVirtualPath(rootPath: string, absolutePath: string): string { async function resolveCurrentRootPath(): Promise { const settings = await getAgentFilesystemSettings(); - if (!settings.localRootPath) { - throw new Error("No local filesystem root selected"); + if (settings.localRootPaths.length === 0) { + throw new Error("No local filesystem roots selected"); } - return settings.localRootPath; + return settings.localRootPaths[0]; +} + +async function resolveCurrentRootPaths(): Promise { + const settings = await getAgentFilesystemSettings(); + if (settings.localRootPaths.length === 0) { + throw new Error("No local filesystem roots selected"); + } + return settings.localRootPaths; } export async function readAgentLocalFileText( virtualPath: string ): Promise<{ path: string; content: string }> { - const rootPath = await resolveCurrentRootPath(); - const absolutePath = resolveVirtualPath(rootPath, virtualPath); - const content = await readFile(absolutePath, "utf8"); + const rootPaths = await resolveCurrentRootPaths(); + for (const rootPath of rootPaths) { + const absolutePath = resolveVirtualPath(rootPath, virtualPath); + try { + const content = await readFile(absolutePath, "utf8"); + return { + path: toVirtualPath(rootPath, absolutePath), + content, + }; + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") { + continue; + } + throw error; + } + } + // Keep the same relative virtual path in the error context. + const fallbackRootPath = await resolveCurrentRootPath(); + const fallbackAbsolutePath = resolveVirtualPath(fallbackRootPath, virtualPath); + const content = await readFile(fallbackAbsolutePath, "utf8"); return { - path: toVirtualPath(rootPath, absolutePath), + path: toVirtualPath(fallbackRootPath, fallbackAbsolutePath), content, }; } @@ -123,11 +171,25 @@ export async function writeAgentLocalFileText( virtualPath: string, content: string ): Promise<{ path: string }> { - const rootPath = await resolveCurrentRootPath(); - const absolutePath = resolveVirtualPath(rootPath, virtualPath); - await mkdir(dirname(absolutePath), { recursive: true }); - await writeFile(absolutePath, content, "utf8"); + const rootPaths = await resolveCurrentRootPaths(); + let selectedRootPath = rootPaths[0]; + let selectedAbsolutePath = resolveVirtualPath(selectedRootPath, virtualPath); + + for (const rootPath of rootPaths) { + const absolutePath = resolveVirtualPath(rootPath, virtualPath); + try { + await access(absolutePath); + selectedRootPath = rootPath; + selectedAbsolutePath = absolutePath; + break; + } catch { + // Keep searching for an existing file path across selected roots. + } + } + + await mkdir(dirname(selectedAbsolutePath), { recursive: true }); + await writeFile(selectedAbsolutePath, content, "utf8"); return { - path: toVirtualPath(rootPath, absolutePath), + path: toVirtualPath(selectedRootPath, selectedAbsolutePath), }; } diff --git a/surfsense_desktop/src/preload.ts b/surfsense_desktop/src/preload.ts index 9fc213bfa..f7aaf9633 100644 --- a/surfsense_desktop/src/preload.ts +++ b/surfsense_desktop/src/preload.ts @@ -110,7 +110,7 @@ contextBridge.exposeInMainWorld('electronAPI', { ipcRenderer.invoke(IPC_CHANNELS.AGENT_FILESYSTEM_GET_SETTINGS), setAgentFilesystemSettings: (settings: { mode?: "cloud" | "desktop_local_folder"; - localRootPath?: string | null; + localRootPaths?: string[] | null; }) => ipcRenderer.invoke(IPC_CHANNELS.AGENT_FILESYSTEM_SET_SETTINGS, settings), pickAgentFilesystemRoot: () => ipcRenderer.invoke(IPC_CHANNELS.AGENT_FILESYSTEM_PICK_ROOT), }); diff --git a/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx index bdb77ade2..616637a49 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx @@ -660,7 +660,7 @@ export default function NewChatPage() { const selection = await getAgentFilesystemSelection(); if ( selection.filesystem_mode === "desktop_local_folder" && - !selection.local_filesystem_root + (!selection.local_filesystem_roots || selection.local_filesystem_roots.length === 0) ) { toast.error("Select a local folder before using Local Folder mode."); return; @@ -702,7 +702,7 @@ export default function NewChatPage() { search_space_id: searchSpaceId, filesystem_mode: selection.filesystem_mode, client_platform: selection.client_platform, - local_filesystem_root: selection.local_filesystem_root, + local_filesystem_roots: selection.local_filesystem_roots, messages: messageHistory, mentioned_document_ids: hasDocumentIds ? mentionedDocumentIds.document_ids : undefined, mentioned_surfsense_doc_ids: hasSurfsenseDocIds @@ -1098,7 +1098,7 @@ export default function NewChatPage() { decisions, filesystem_mode: selection.filesystem_mode, client_platform: selection.client_platform, - local_filesystem_root: selection.local_filesystem_root, + local_filesystem_roots: selection.local_filesystem_roots, }), signal: controller.signal, }); @@ -1435,7 +1435,7 @@ export default function NewChatPage() { disabled_tools: disabledTools.length > 0 ? disabledTools : undefined, filesystem_mode: selection.filesystem_mode, client_platform: selection.client_platform, - local_filesystem_root: selection.local_filesystem_root, + local_filesystem_roots: selection.local_filesystem_roots, }), signal: controller.signal, }); diff --git a/surfsense_web/lib/agent-filesystem.ts b/surfsense_web/lib/agent-filesystem.ts index 6bfb5d131..c9096a294 100644 --- a/surfsense_web/lib/agent-filesystem.ts +++ b/surfsense_web/lib/agent-filesystem.ts @@ -4,7 +4,7 @@ export type ClientPlatform = "web" | "desktop"; export interface AgentFilesystemSelection { filesystem_mode: AgentFilesystemMode; client_platform: ClientPlatform; - local_filesystem_root?: string; + local_filesystem_roots?: string[]; } const DEFAULT_SELECTION: AgentFilesystemSelection = { @@ -24,11 +24,12 @@ export async function getAgentFilesystemSelection(): Promise Promise; setAgentFilesystemSettings: (settings: { mode?: AgentFilesystemMode; - localRootPath?: string | null; + localRootPaths?: string[] | null; }) => Promise; pickAgentFilesystemRoot: () => Promise; }