feat(filesystem): enhance local file handling in editor and IPC integration

This commit is contained in:
Anish Sarkar 2026-04-23 17:23:38 +05:30
parent 4899588cd7
commit 864f6f798a
12 changed files with 350 additions and 47 deletions

View file

@ -34,6 +34,8 @@ export const IPC_CHANNELS = {
FOLDER_SYNC_SEED_MTIMES: 'folder-sync:seed-mtimes',
BROWSE_FILES: 'browse:files',
READ_LOCAL_FILES: 'browse:read-local-files',
READ_AGENT_LOCAL_FILE_TEXT: 'agent-filesystem:read-local-file-text',
WRITE_AGENT_LOCAL_FILE_TEXT: 'agent-filesystem:write-local-file-text',
// Auth token sync across windows
GET_AUTH_TOKENS: 'auth:get-tokens',
SET_AUTH_TOKENS: 'auth:set-tokens',

View file

@ -37,6 +37,8 @@ import {
trackEvent,
} from '../modules/analytics';
import {
readAgentLocalFileText,
writeAgentLocalFileText,
getAgentFilesystemSettings,
pickAgentFilesystemRoot,
setAgentFilesystemSettings,
@ -123,6 +125,29 @@ export function registerIpcHandlers(): void {
readLocalFiles(paths)
);
ipcMain.handle(IPC_CHANNELS.READ_AGENT_LOCAL_FILE_TEXT, async (_event, virtualPath: string) => {
try {
const result = await readAgentLocalFileText(virtualPath);
return { ok: true, path: result.path, content: result.content };
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to read local file';
return { ok: false, path: virtualPath, error: message };
}
});
ipcMain.handle(
IPC_CHANNELS.WRITE_AGENT_LOCAL_FILE_TEXT,
async (_event, virtualPath: string, content: string) => {
try {
const result = await writeAgentLocalFileText(virtualPath, content);
return { ok: true, path: result.path };
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to write local file';
return { ok: false, path: virtualPath, error: message };
}
}
);
ipcMain.handle(IPC_CHANNELS.SET_AUTH_TOKENS, (_event, tokens: { bearer: string; refresh: string }) => {
authTokens = tokens;
});

View file

@ -1,6 +1,6 @@
import { app, dialog } from "electron";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
export type AgentFilesystemMode = "cloud" | "desktop_local_folder";
@ -72,3 +72,62 @@ export async function pickAgentFilesystemRoot(): Promise<string | null> {
}
return result.filePaths[0] ?? null;
}
function resolveVirtualPath(rootPath: string, virtualPath: string): string {
if (!virtualPath.startsWith("/")) {
throw new Error("Path must start with '/'");
}
const normalizedRoot = resolve(rootPath);
const relativePath = virtualPath.replace(/^\/+/, "");
if (!relativePath) {
throw new Error("Path must refer to a file under the selected root");
}
const absolutePath = resolve(normalizedRoot, relativePath);
const rel = relative(normalizedRoot, absolutePath);
if (!rel || rel.startsWith("..") || isAbsolute(rel)) {
throw new Error("Path escapes selected local root");
}
return absolutePath;
}
function toVirtualPath(rootPath: string, absolutePath: string): string {
const normalizedRoot = resolve(rootPath);
const rel = relative(normalizedRoot, absolutePath);
if (!rel || rel.startsWith("..") || isAbsolute(rel)) {
return "/";
}
return `/${rel.replace(/\\/g, "/")}`;
}
async function resolveCurrentRootPath(): Promise<string> {
const settings = await getAgentFilesystemSettings();
if (!settings.localRootPath) {
throw new Error("No local filesystem root selected");
}
return settings.localRootPath;
}
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");
return {
path: toVirtualPath(rootPath, absolutePath),
content,
};
}
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");
return {
path: toVirtualPath(rootPath, absolutePath),
};
}

View file

@ -71,6 +71,10 @@ contextBridge.exposeInMainWorld('electronAPI', {
// Browse files via native dialog
browseFiles: () => ipcRenderer.invoke(IPC_CHANNELS.BROWSE_FILES),
readLocalFiles: (paths: string[]) => ipcRenderer.invoke(IPC_CHANNELS.READ_LOCAL_FILES, paths),
readAgentLocalFileText: (virtualPath: string) =>
ipcRenderer.invoke(IPC_CHANNELS.READ_AGENT_LOCAL_FILE_TEXT, virtualPath),
writeAgentLocalFileText: (virtualPath: string, content: string) =>
ipcRenderer.invoke(IPC_CHANNELS.WRITE_AGENT_LOCAL_FILE_TEXT, virtualPath, content),
// Auth token sync across windows
getAuthTokens: () => ipcRenderer.invoke(IPC_CHANNELS.GET_AUTH_TOKENS),