mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-25 00:36:31 +02:00
feat(filesystem): enhance local mount path normalization and improve virtual path handling in agent filesystem
This commit is contained in:
parent
c1a07a093e
commit
1e9db6f26f
3 changed files with 96 additions and 57 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -122,12 +122,55 @@ function toVirtualPath(rootPath: string, absolutePath: string): string {
|
|||
return `/${rel.replace(/\\/g, "/")}`;
|
||||
}
|
||||
|
||||
async function resolveCurrentRootPath(): Promise<string> {
|
||||
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<string>();
|
||||
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<string[]> {
|
||||
|
|
@ -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),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ export function SourceCodeEditor({
|
|||
onChange={(next) => onChange(next ?? "")}
|
||||
loading={
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Spinner size="sm" className="text-muted-foreground" />
|
||||
<Spinner size="md" className="text-muted-foreground" />
|
||||
</div>
|
||||
}
|
||||
beforeMount={(monaco) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue