From 1e9db6f26f12f399f9b94eed51184782ab7f7ae4 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Fri, 24 Apr 2026 02:12:30 +0530 Subject: [PATCH] feat(filesystem): enhance local mount path normalization and improve virtual path handling in agent filesystem --- .../agents/new_chat/middleware/filesystem.py | 41 ++++--- .../src/modules/agent-filesystem.ts | 110 ++++++++++++------ .../components/editor/source-code-editor.tsx | 2 +- 3 files changed, 96 insertions(+), 57 deletions(-) diff --git a/surfsense_backend/app/agents/new_chat/middleware/filesystem.py b/surfsense_backend/app/agents/new_chat/middleware/filesystem.py index 6c30b20ef..a086357af 100644 --- a/surfsense_backend/app/agents/new_chat/middleware/filesystem.py +++ b/surfsense_backend/app/agents/new_chat/middleware/filesystem.py @@ -782,6 +782,27 @@ class SurfSenseFilesystemMiddleware(FilesystemMiddleware): return f"/{backend.default_mount()}" return "" + def _normalize_local_mount_path( + self, candidate: str, runtime: ToolRuntime[None, FilesystemState] + ) -> str: + backend = self._get_backend(runtime) + mount_prefix = self._default_mount_prefix(runtime) + if not mount_prefix or not isinstance(backend, MultiRootLocalFolderBackend): + return candidate if candidate.startswith("/") else f"/{candidate.lstrip('/')}" + + mount_names = set(backend.list_mounts()) + if candidate.startswith("/"): + first_segment = candidate.lstrip("/").split("/", 1)[0] + if first_segment in mount_names: + return candidate + return f"{mount_prefix}{candidate}" + + relative = candidate.lstrip("/") + first_segment = relative.split("/", 1)[0] + if first_segment in mount_names: + return f"/{relative}" + return f"{mount_prefix}/{relative}" + def _get_contract_suggested_path( self, runtime: ToolRuntime[None, FilesystemState] ) -> str: @@ -790,11 +811,7 @@ class SurfSenseFilesystemMiddleware(FilesystemMiddleware): if isinstance(suggested, str) and suggested.strip(): cleaned = suggested.strip() if self._filesystem_mode == FilesystemMode.DESKTOP_LOCAL_FOLDER: - mount_prefix = self._default_mount_prefix(runtime) - if mount_prefix and cleaned.startswith("/") and not cleaned.startswith( - f"{mount_prefix}/" - ): - return f"{mount_prefix}{cleaned}" + return self._normalize_local_mount_path(cleaned, runtime) return cleaned if self._filesystem_mode == FilesystemMode.DESKTOP_LOCAL_FOLDER: mount_prefix = self._default_mount_prefix(runtime) @@ -811,19 +828,7 @@ class SurfSenseFilesystemMiddleware(FilesystemMiddleware): if not candidate: return self._get_contract_suggested_path(runtime) if self._filesystem_mode == FilesystemMode.DESKTOP_LOCAL_FOLDER: - backend = self._get_backend(runtime) - mount_prefix = self._default_mount_prefix(runtime) - if mount_prefix and not candidate.startswith("/"): - return f"{mount_prefix}/{candidate.lstrip('/')}" - if ( - mount_prefix - and isinstance(backend, MultiRootLocalFolderBackend) - and candidate.startswith("/") - ): - mount_names = backend.list_mounts() - first_segment = candidate.lstrip("/").split("/", 1)[0] - if first_segment not in mount_names: - return f"{mount_prefix}{candidate}" + return self._normalize_local_mount_path(candidate, runtime) if not candidate.startswith("/"): return f"/{candidate.lstrip('/')}" return candidate diff --git a/surfsense_desktop/src/modules/agent-filesystem.ts b/surfsense_desktop/src/modules/agent-filesystem.ts index afad98f24..2bf0101d6 100644 --- a/surfsense_desktop/src/modules/agent-filesystem.ts +++ b/surfsense_desktop/src/modules/agent-filesystem.ts @@ -122,12 +122,55 @@ function toVirtualPath(rootPath: string, absolutePath: string): string { return `/${rel.replace(/\\/g, "/")}`; } -async function resolveCurrentRootPath(): Promise { - const settings = await getAgentFilesystemSettings(); - if (settings.localRootPaths.length === 0) { - throw new Error("No local filesystem roots selected"); +type LocalRootMount = { + mount: string; + rootPath: string; +}; + +function buildRootMounts(rootPaths: string[]): LocalRootMount[] { + const mounts: LocalRootMount[] = []; + const usedMounts = new Set(); + for (const rawRootPath of rootPaths) { + const normalizedRoot = resolve(rawRootPath); + const baseMount = normalizedRoot.split(/[\\/]/).at(-1) || "root"; + let mount = baseMount; + let suffix = 2; + while (usedMounts.has(mount)) { + mount = `${baseMount}-${suffix}`; + suffix += 1; + } + usedMounts.add(mount); + mounts.push({ mount, rootPath: normalizedRoot }); } - return settings.localRootPaths[0]; + return mounts; +} + +function parseMountedVirtualPath(virtualPath: string): { + mount: string; + subPath: string; +} { + if (!virtualPath.startsWith("/")) { + throw new Error("Path must start with '/'"); + } + const trimmed = virtualPath.replace(/^\/+/, ""); + if (!trimmed) { + throw new Error("Path must include a mounted root segment"); + } + const [mount, ...rest] = trimmed.split("/"); + const remainder = rest.join("/"); + if (!remainder) { + throw new Error("Path must include a file path under the mounted root"); + } + return { mount, subPath: `/${remainder}` }; +} + +function findMountByName(mounts: LocalRootMount[], mountName: string): LocalRootMount | undefined { + return mounts.find((entry) => entry.mount === mountName); +} + +function toMountedVirtualPath(mount: string, rootPath: string, absolutePath: string): string { + const relativePath = toVirtualPath(rootPath, absolutePath); + return `/${mount}${relativePath}`; } async function resolveCurrentRootPaths(): Promise { @@ -142,27 +185,18 @@ export async function readAgentLocalFileText( virtualPath: string ): Promise<{ path: string; content: string }> { 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; - } + const mounts = buildRootMounts(rootPaths); + const { mount, subPath } = parseMountedVirtualPath(virtualPath); + const rootMount = findMountByName(mounts, mount); + if (!rootMount) { + throw new Error( + `Unknown mounted root '${mount}'. Available roots: ${mounts.map((entry) => `/${entry.mount}`).join(", ")}` + ); } - // 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"); + const absolutePath = resolveVirtualPath(rootMount.rootPath, subPath); + const content = await readFile(absolutePath, "utf8"); return { - path: toVirtualPath(fallbackRootPath, fallbackAbsolutePath), + path: toMountedVirtualPath(rootMount.mount, rootMount.rootPath, absolutePath), content, }; } @@ -172,24 +206,24 @@ export async function writeAgentLocalFileText( content: string ): Promise<{ path: string }> { 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. - } + const mounts = buildRootMounts(rootPaths); + const { mount, subPath } = parseMountedVirtualPath(virtualPath); + const rootMount = findMountByName(mounts, mount); + if (!rootMount) { + throw new Error( + `Unknown mounted root '${mount}'. Available roots: ${mounts.map((entry) => `/${entry.mount}`).join(", ")}` + ); } + let selectedAbsolutePath = resolveVirtualPath(rootMount.rootPath, subPath); + try { + await access(selectedAbsolutePath); + } catch { + // New files are created under the selected mounted root. + } await mkdir(dirname(selectedAbsolutePath), { recursive: true }); await writeFile(selectedAbsolutePath, content, "utf8"); return { - path: toVirtualPath(selectedRootPath, selectedAbsolutePath), + path: toMountedVirtualPath(rootMount.mount, rootMount.rootPath, selectedAbsolutePath), }; } diff --git a/surfsense_web/components/editor/source-code-editor.tsx b/surfsense_web/components/editor/source-code-editor.tsx index 11f9266b6..c2d77be60 100644 --- a/surfsense_web/components/editor/source-code-editor.tsx +++ b/surfsense_web/components/editor/source-code-editor.tsx @@ -89,7 +89,7 @@ export function SourceCodeEditor({ onChange={(next) => onChange(next ?? "")} loading={
- +
} beforeMount={(monaco) => {