feat(filesystem): propagate localRootPaths across desktop and web API

This commit is contained in:
Anish Sarkar 2026-04-24 01:45:13 +05:30
parent 6721919398
commit 3ee2683391
6 changed files with 93 additions and 30 deletions

View file

@ -228,7 +228,7 @@ export function registerIpcHandlers(): void {
ipcMain.handle( ipcMain.handle(
IPC_CHANNELS.AGENT_FILESYSTEM_SET_SETTINGS, 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) setAgentFilesystemSettings(settings)
); );

View file

@ -1,16 +1,17 @@
import { app, dialog } from "electron"; 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"; import { dirname, isAbsolute, join, relative, resolve } from "node:path";
export type AgentFilesystemMode = "cloud" | "desktop_local_folder"; export type AgentFilesystemMode = "cloud" | "desktop_local_folder";
export interface AgentFilesystemSettings { export interface AgentFilesystemSettings {
mode: AgentFilesystemMode; mode: AgentFilesystemMode;
localRootPath: string | null; localRootPaths: string[];
updatedAt: string; updatedAt: string;
} }
const SETTINGS_FILENAME = "agent-filesystem-settings.json"; const SETTINGS_FILENAME = "agent-filesystem-settings.json";
const MAX_LOCAL_ROOTS = 5;
function getSettingsPath(): string { function getSettingsPath(): string {
return join(app.getPath("userData"), SETTINGS_FILENAME); return join(app.getPath("userData"), SETTINGS_FILENAME);
@ -19,11 +20,28 @@ function getSettingsPath(): string {
function getDefaultSettings(): AgentFilesystemSettings { function getDefaultSettings(): AgentFilesystemSettings {
return { return {
mode: "cloud", mode: "cloud",
localRootPath: null, localRootPaths: [],
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
}; };
} }
function normalizeLocalRootPaths(paths: unknown): string[] {
if (!Array.isArray(paths)) {
return [];
}
const uniquePaths = new Set<string>();
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<AgentFilesystemSettings> { export async function getAgentFilesystemSettings(): Promise<AgentFilesystemSettings> {
try { try {
const raw = await readFile(getSettingsPath(), "utf8"); const raw = await readFile(getSettingsPath(), "utf8");
@ -33,7 +51,7 @@ export async function getAgentFilesystemSettings(): Promise<AgentFilesystemSetti
} }
return { return {
mode: parsed.mode, mode: parsed.mode,
localRootPath: parsed.localRootPath ?? null, localRootPaths: normalizeLocalRootPaths(parsed.localRootPaths),
updatedAt: parsed.updatedAt ?? new Date().toISOString(), updatedAt: parsed.updatedAt ?? new Date().toISOString(),
}; };
} catch { } catch {
@ -42,7 +60,10 @@ export async function getAgentFilesystemSettings(): Promise<AgentFilesystemSetti
} }
export async function setAgentFilesystemSettings( export async function setAgentFilesystemSettings(
settings: Partial<Pick<AgentFilesystemSettings, "mode" | "localRootPath">> settings: {
mode?: AgentFilesystemMode;
localRootPaths?: string[] | null;
}
): Promise<AgentFilesystemSettings> { ): Promise<AgentFilesystemSettings> {
const current = await getAgentFilesystemSettings(); const current = await getAgentFilesystemSettings();
const nextMode = const nextMode =
@ -51,8 +72,10 @@ export async function setAgentFilesystemSettings(
: current.mode; : current.mode;
const next: AgentFilesystemSettings = { const next: AgentFilesystemSettings = {
mode: nextMode, mode: nextMode,
localRootPath: localRootPaths:
settings.localRootPath === undefined ? current.localRootPath : settings.localRootPath, settings.localRootPaths === undefined
? current.localRootPaths
: normalizeLocalRootPaths(settings.localRootPaths ?? []),
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
}; };
@ -101,33 +124,72 @@ function toVirtualPath(rootPath: string, absolutePath: string): string {
async function resolveCurrentRootPath(): Promise<string> { async function resolveCurrentRootPath(): Promise<string> {
const settings = await getAgentFilesystemSettings(); const settings = await getAgentFilesystemSettings();
if (!settings.localRootPath) { if (settings.localRootPaths.length === 0) {
throw new Error("No local filesystem root selected"); throw new Error("No local filesystem roots selected");
} }
return settings.localRootPath; return settings.localRootPaths[0];
}
async function resolveCurrentRootPaths(): Promise<string[]> {
const settings = await getAgentFilesystemSettings();
if (settings.localRootPaths.length === 0) {
throw new Error("No local filesystem roots selected");
}
return settings.localRootPaths;
} }
export async function readAgentLocalFileText( export async function readAgentLocalFileText(
virtualPath: string virtualPath: string
): Promise<{ path: string; content: string }> { ): Promise<{ path: string; content: string }> {
const rootPath = await resolveCurrentRootPath(); const rootPaths = await resolveCurrentRootPaths();
for (const rootPath of rootPaths) {
const absolutePath = resolveVirtualPath(rootPath, virtualPath); const absolutePath = resolveVirtualPath(rootPath, virtualPath);
try {
const content = await readFile(absolutePath, "utf8"); const content = await readFile(absolutePath, "utf8");
return { return {
path: toVirtualPath(rootPath, absolutePath), path: toVirtualPath(rootPath, absolutePath),
content, 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(fallbackRootPath, fallbackAbsolutePath),
content,
};
} }
export async function writeAgentLocalFileText( export async function writeAgentLocalFileText(
virtualPath: string, virtualPath: string,
content: string content: string
): Promise<{ path: string }> { ): Promise<{ path: string }> {
const rootPath = await resolveCurrentRootPath(); const rootPaths = await resolveCurrentRootPaths();
let selectedRootPath = rootPaths[0];
let selectedAbsolutePath = resolveVirtualPath(selectedRootPath, virtualPath);
for (const rootPath of rootPaths) {
const absolutePath = resolveVirtualPath(rootPath, virtualPath); const absolutePath = resolveVirtualPath(rootPath, virtualPath);
await mkdir(dirname(absolutePath), { recursive: true }); try {
await writeFile(absolutePath, content, "utf8"); 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 { return {
path: toVirtualPath(rootPath, absolutePath), path: toVirtualPath(selectedRootPath, selectedAbsolutePath),
}; };
} }

View file

@ -110,7 +110,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
ipcRenderer.invoke(IPC_CHANNELS.AGENT_FILESYSTEM_GET_SETTINGS), ipcRenderer.invoke(IPC_CHANNELS.AGENT_FILESYSTEM_GET_SETTINGS),
setAgentFilesystemSettings: (settings: { setAgentFilesystemSettings: (settings: {
mode?: "cloud" | "desktop_local_folder"; mode?: "cloud" | "desktop_local_folder";
localRootPath?: string | null; localRootPaths?: string[] | null;
}) => ipcRenderer.invoke(IPC_CHANNELS.AGENT_FILESYSTEM_SET_SETTINGS, settings), }) => ipcRenderer.invoke(IPC_CHANNELS.AGENT_FILESYSTEM_SET_SETTINGS, settings),
pickAgentFilesystemRoot: () => ipcRenderer.invoke(IPC_CHANNELS.AGENT_FILESYSTEM_PICK_ROOT), pickAgentFilesystemRoot: () => ipcRenderer.invoke(IPC_CHANNELS.AGENT_FILESYSTEM_PICK_ROOT),
}); });

View file

@ -660,7 +660,7 @@ export default function NewChatPage() {
const selection = await getAgentFilesystemSelection(); const selection = await getAgentFilesystemSelection();
if ( if (
selection.filesystem_mode === "desktop_local_folder" && 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."); toast.error("Select a local folder before using Local Folder mode.");
return; return;
@ -702,7 +702,7 @@ export default function NewChatPage() {
search_space_id: searchSpaceId, search_space_id: searchSpaceId,
filesystem_mode: selection.filesystem_mode, filesystem_mode: selection.filesystem_mode,
client_platform: selection.client_platform, client_platform: selection.client_platform,
local_filesystem_root: selection.local_filesystem_root, local_filesystem_roots: selection.local_filesystem_roots,
messages: messageHistory, messages: messageHistory,
mentioned_document_ids: hasDocumentIds ? mentionedDocumentIds.document_ids : undefined, mentioned_document_ids: hasDocumentIds ? mentionedDocumentIds.document_ids : undefined,
mentioned_surfsense_doc_ids: hasSurfsenseDocIds mentioned_surfsense_doc_ids: hasSurfsenseDocIds
@ -1098,7 +1098,7 @@ export default function NewChatPage() {
decisions, decisions,
filesystem_mode: selection.filesystem_mode, filesystem_mode: selection.filesystem_mode,
client_platform: selection.client_platform, client_platform: selection.client_platform,
local_filesystem_root: selection.local_filesystem_root, local_filesystem_roots: selection.local_filesystem_roots,
}), }),
signal: controller.signal, signal: controller.signal,
}); });
@ -1435,7 +1435,7 @@ export default function NewChatPage() {
disabled_tools: disabledTools.length > 0 ? disabledTools : undefined, disabled_tools: disabledTools.length > 0 ? disabledTools : undefined,
filesystem_mode: selection.filesystem_mode, filesystem_mode: selection.filesystem_mode,
client_platform: selection.client_platform, client_platform: selection.client_platform,
local_filesystem_root: selection.local_filesystem_root, local_filesystem_roots: selection.local_filesystem_roots,
}), }),
signal: controller.signal, signal: controller.signal,
}); });

View file

@ -4,7 +4,7 @@ export type ClientPlatform = "web" | "desktop";
export interface AgentFilesystemSelection { export interface AgentFilesystemSelection {
filesystem_mode: AgentFilesystemMode; filesystem_mode: AgentFilesystemMode;
client_platform: ClientPlatform; client_platform: ClientPlatform;
local_filesystem_root?: string; local_filesystem_roots?: string[];
} }
const DEFAULT_SELECTION: AgentFilesystemSelection = { const DEFAULT_SELECTION: AgentFilesystemSelection = {
@ -24,11 +24,12 @@ export async function getAgentFilesystemSelection(): Promise<AgentFilesystemSele
} }
try { try {
const settings = await window.electronAPI.getAgentFilesystemSettings(); const settings = await window.electronAPI.getAgentFilesystemSettings();
if (settings.mode === "desktop_local_folder" && settings.localRootPath) { const firstLocalRootPath = settings.localRootPaths[0];
if (settings.mode === "desktop_local_folder" && firstLocalRootPath) {
return { return {
filesystem_mode: "desktop_local_folder", filesystem_mode: "desktop_local_folder",
client_platform: "desktop", client_platform: "desktop",
local_filesystem_root: settings.localRootPath, local_filesystem_roots: settings.localRootPaths,
}; };
} }
return { return {

View file

@ -45,7 +45,7 @@ type AgentFilesystemMode = "cloud" | "desktop_local_folder";
interface AgentFilesystemSettings { interface AgentFilesystemSettings {
mode: AgentFilesystemMode; mode: AgentFilesystemMode;
localRootPath: string | null; localRootPaths: string[];
updatedAt: string; updatedAt: string;
} }
@ -149,7 +149,7 @@ interface ElectronAPI {
getAgentFilesystemSettings: () => Promise<AgentFilesystemSettings>; getAgentFilesystemSettings: () => Promise<AgentFilesystemSettings>;
setAgentFilesystemSettings: (settings: { setAgentFilesystemSettings: (settings: {
mode?: AgentFilesystemMode; mode?: AgentFilesystemMode;
localRootPath?: string | null; localRootPaths?: string[] | null;
}) => Promise<AgentFilesystemSettings>; }) => Promise<AgentFilesystemSettings>;
pickAgentFilesystemRoot: () => Promise<string | null>; pickAgentFilesystemRoot: () => Promise<string | null>;
} }