feat(filesystem): enhance agent filesystem API with searchSpaceId support for improved context handling

This commit is contained in:
Anish Sarkar 2026-04-27 21:00:40 +05:30
parent 86e2dc8a5d
commit 27e16231c1
10 changed files with 349 additions and 85 deletions

View file

@ -56,6 +56,7 @@ export const IPC_CHANNELS = {
// Agent filesystem mode
AGENT_FILESYSTEM_GET_SETTINGS: 'agent-filesystem:get-settings',
AGENT_FILESYSTEM_GET_MOUNTS: 'agent-filesystem:get-mounts',
AGENT_FILESYSTEM_LIST_FILES: 'agent-filesystem:list-files',
AGENT_FILESYSTEM_SET_SETTINGS: 'agent-filesystem:set-settings',
AGENT_FILESYSTEM_PICK_ROOT: 'agent-filesystem:pick-root',
} as const;

View file

@ -37,6 +37,7 @@ import {
trackEvent,
} from '../modules/analytics';
import {
listAgentFilesystemFiles,
readAgentLocalFileText,
writeAgentLocalFileText,
getAgentFilesystemMounts,
@ -126,21 +127,24 @@ export function registerIpcHandlers(): void {
readLocalFiles(paths)
);
ipcMain.handle(IPC_CHANNELS.READ_AGENT_LOCAL_FILE_TEXT, async (_event, virtualPath: string) => {
ipcMain.handle(
IPC_CHANNELS.READ_AGENT_LOCAL_FILE_TEXT,
async (_event, virtualPath: string, searchSpaceId?: number | null) => {
try {
const result = await readAgentLocalFileText(virtualPath);
const result = await readAgentLocalFileText(virtualPath, searchSpaceId);
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) => {
async (_event, virtualPath: string, content: string, searchSpaceId?: number | null) => {
try {
const result = await writeAgentLocalFileText(virtualPath, content);
const result = await writeAgentLocalFileText(virtualPath, content, searchSpaceId);
return { ok: true, path: result.path };
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to write local file';
@ -223,18 +227,37 @@ export function registerIpcHandlers(): void {
};
});
ipcMain.handle(IPC_CHANNELS.AGENT_FILESYSTEM_GET_SETTINGS, () =>
getAgentFilesystemSettings()
ipcMain.handle(IPC_CHANNELS.AGENT_FILESYSTEM_GET_SETTINGS, (_event, searchSpaceId?: number | null) =>
getAgentFilesystemSettings(searchSpaceId)
);
ipcMain.handle(IPC_CHANNELS.AGENT_FILESYSTEM_GET_MOUNTS, () =>
getAgentFilesystemMounts()
ipcMain.handle(IPC_CHANNELS.AGENT_FILESYSTEM_GET_MOUNTS, (_event, searchSpaceId?: number | null) =>
getAgentFilesystemMounts(searchSpaceId)
);
ipcMain.handle(
IPC_CHANNELS.AGENT_FILESYSTEM_LIST_FILES,
(
_event,
options: {
rootPath: string;
searchSpaceId?: number | null;
excludePatterns?: string[] | null;
fileExtensions?: string[] | null;
}
) =>
listAgentFilesystemFiles(options)
);
ipcMain.handle(
IPC_CHANNELS.AGENT_FILESYSTEM_SET_SETTINGS,
(_event, settings: { mode?: 'cloud' | 'desktop_local_folder'; localRootPaths?: string[] | null }) =>
setAgentFilesystemSettings(settings)
(
_event,
payload: {
searchSpaceId?: number | null;
settings: { mode?: 'cloud' | 'desktop_local_folder'; localRootPaths?: string[] | null };
}
) => setAgentFilesystemSettings(payload?.searchSpaceId, payload?.settings ?? {})
);
ipcMain.handle(IPC_CHANNELS.AGENT_FILESYSTEM_PICK_ROOT, () =>

View file

@ -1,6 +1,7 @@
import { app, dialog } from "electron";
import { access, mkdir, readFile, realpath, writeFile } from "node:fs/promises";
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
import type { Dirent } from "node:fs";
import { access, mkdir, readdir, readFile, realpath, stat, writeFile } from "node:fs/promises";
import { dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
export type AgentFilesystemMode = "cloud" | "desktop_local_folder";
@ -10,9 +11,15 @@ export interface AgentFilesystemSettings {
updatedAt: string;
}
type AgentFilesystemSettingsStore = {
version: 2;
spaces: Record<string, AgentFilesystemSettings>;
};
const SETTINGS_FILENAME = "agent-filesystem-settings.json";
const MAX_LOCAL_ROOTS = 10;
let cachedSettings: AgentFilesystemSettings | null = null;
const DEFAULT_SPACE_KEY = "default";
let cachedSettingsStore: AgentFilesystemSettingsStore | null = null;
function getSettingsPath(): string {
return join(app.getPath("userData"), SETTINGS_FILENAME);
@ -67,37 +74,97 @@ async function normalizeLocalRootPathsCanonical(paths: unknown): Promise<string[
return [...uniquePaths];
}
export async function getAgentFilesystemSettings(): Promise<AgentFilesystemSettings> {
if (cachedSettings) {
return cachedSettings;
function normalizeSearchSpaceKey(searchSpaceId?: number | null): string {
if (typeof searchSpaceId === "number" && Number.isFinite(searchSpaceId) && searchSpaceId > 0) {
return String(searchSpaceId);
}
return DEFAULT_SPACE_KEY;
}
function toSettingsFromUnknown(value: unknown): AgentFilesystemSettings | null {
if (!value || typeof value !== "object") {
return null;
}
const parsed = value as Partial<AgentFilesystemSettings>;
if (parsed.mode !== "cloud" && parsed.mode !== "desktop_local_folder") {
return null;
}
return {
mode: parsed.mode,
localRootPaths: normalizeLocalRootPaths(parsed.localRootPaths),
updatedAt: parsed.updatedAt ?? new Date().toISOString(),
};
}
function getDefaultStore(): AgentFilesystemSettingsStore {
return { version: 2, spaces: {} };
}
function getSettingsFromStore(
store: AgentFilesystemSettingsStore,
searchSpaceId?: number | null
): AgentFilesystemSettings {
const key = normalizeSearchSpaceKey(searchSpaceId);
return store.spaces[key] ?? getDefaultSettings();
}
async function loadAgentFilesystemSettingsStore(): Promise<AgentFilesystemSettingsStore> {
if (cachedSettingsStore) {
return cachedSettingsStore;
}
const settingsPath = getSettingsPath();
try {
const raw = await readFile(getSettingsPath(), "utf8");
const parsed = JSON.parse(raw) as Partial<AgentFilesystemSettings>;
if (parsed.mode !== "cloud" && parsed.mode !== "desktop_local_folder") {
cachedSettings = getDefaultSettings();
return cachedSettings;
const raw = await readFile(settingsPath, "utf8");
const parsed = JSON.parse(raw) as unknown;
const nextStore = getDefaultStore();
if (
parsed &&
typeof parsed === "object" &&
"version" in parsed &&
"spaces" in parsed &&
(parsed as { version?: unknown }).version === 2
) {
const parsedStore = parsed as { spaces?: Record<string, unknown>; version: 2 };
if (parsedStore.spaces && typeof parsedStore.spaces === "object") {
for (const [spaceKey, rawSettings] of Object.entries(parsedStore.spaces)) {
const normalizedSettings = toSettingsFromUnknown(rawSettings);
if (normalizedSettings) {
nextStore.spaces[String(spaceKey)] = normalizedSettings;
}
}
}
} else {
// Strict migration: reject legacy/non-scoped settings and reset.
await mkdir(dirname(settingsPath), { recursive: true });
await writeFile(settingsPath, JSON.stringify(nextStore, null, 2), "utf8");
}
cachedSettings = {
mode: parsed.mode,
// Avoid filesystem I/O during reads; canonicalize paths on write.
localRootPaths: normalizeLocalRootPaths(parsed.localRootPaths),
updatedAt: parsed.updatedAt ?? new Date().toISOString(),
};
return cachedSettings;
cachedSettingsStore = nextStore;
return nextStore;
} catch {
cachedSettings = getDefaultSettings();
return cachedSettings;
cachedSettingsStore = getDefaultStore();
await mkdir(dirname(settingsPath), { recursive: true });
await writeFile(settingsPath, JSON.stringify(cachedSettingsStore, null, 2), "utf8");
return cachedSettingsStore;
}
}
export async function getAgentFilesystemSettings(
searchSpaceId?: number | null
): Promise<AgentFilesystemSettings> {
const store = await loadAgentFilesystemSettingsStore();
return getSettingsFromStore(store, searchSpaceId);
}
export async function setAgentFilesystemSettings(
searchSpaceId: number | null | undefined,
settings: {
mode?: AgentFilesystemMode;
localRootPaths?: string[] | null;
}
): Promise<AgentFilesystemSettings> {
const current = await getAgentFilesystemSettings();
const store = await loadAgentFilesystemSettingsStore();
const key = normalizeSearchSpaceKey(searchSpaceId);
const current = getSettingsFromStore(store, searchSpaceId);
const nextMode =
settings.mode === "cloud" || settings.mode === "desktop_local_folder"
? settings.mode
@ -113,8 +180,15 @@ export async function setAgentFilesystemSettings(
const settingsPath = getSettingsPath();
await mkdir(dirname(settingsPath), { recursive: true });
await writeFile(settingsPath, JSON.stringify(next, null, 2), "utf8");
cachedSettings = next;
const nextStore: AgentFilesystemSettingsStore = {
version: 2,
spaces: {
...store.spaces,
[key]: next,
},
};
await writeFile(settingsPath, JSON.stringify(nextStore, null, 2), "utf8");
cachedSettingsStore = nextStore;
return next;
}
@ -160,6 +234,20 @@ export type LocalRootMount = {
rootPath: string;
};
export type AgentFilesystemListOptions = {
rootPath: string;
searchSpaceId?: number | null;
excludePatterns?: string[] | null;
fileExtensions?: string[] | null;
};
export type AgentFilesystemFileEntry = {
relativePath: string;
fullPath: string;
size: number;
mtimeMs: number;
};
function sanitizeMountName(rawMount: string): string {
const normalized = rawMount
.trim()
@ -188,11 +276,111 @@ function buildRootMounts(rootPaths: string[]): LocalRootMount[] {
return mounts;
}
export async function getAgentFilesystemMounts(): Promise<LocalRootMount[]> {
const rootPaths = await resolveCurrentRootPaths();
export async function getAgentFilesystemMounts(
searchSpaceId?: number | null
): Promise<LocalRootMount[]> {
const rootPaths = await resolveCurrentRootPaths(searchSpaceId);
return buildRootMounts(rootPaths);
}
function normalizeComparablePath(pathValue: string): string {
const normalized = resolve(pathValue);
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
}
function normalizeExtensionSet(fileExtensions: string[] | null | undefined): Set<string> | null {
if (!fileExtensions || fileExtensions.length === 0) {
return null;
}
const set = new Set<string>();
for (const extension of fileExtensions) {
if (typeof extension !== "string") continue;
const trimmed = extension.trim().toLowerCase();
if (!trimmed) continue;
set.add(trimmed.startsWith(".") ? trimmed : `.${trimmed}`);
}
return set.size > 0 ? set : null;
}
function normalizeExcludeSet(excludePatterns: string[] | null | undefined): Set<string> {
const set = new Set<string>();
for (const pattern of excludePatterns ?? []) {
if (typeof pattern !== "string") continue;
const trimmed = pattern.trim();
if (!trimmed) continue;
set.add(trimmed);
}
return set;
}
export async function listAgentFilesystemFiles(
options: AgentFilesystemListOptions
): Promise<AgentFilesystemFileEntry[]> {
const allowedRootPaths = await resolveCurrentRootPaths(options.searchSpaceId);
const requestedRootPath = await canonicalizeRootPath(options.rootPath);
const normalizedRequestedRoot = normalizeComparablePath(requestedRootPath);
const allowedRoots = new Set(
(
await Promise.all(allowedRootPaths.map((rootPath) => canonicalizeRootPath(rootPath)))
).map((rootPath) => normalizeComparablePath(rootPath))
);
if (!allowedRoots.has(normalizedRequestedRoot)) {
throw new Error("Selected path is not an allowed local root");
}
const excludePatterns = normalizeExcludeSet(options.excludePatterns);
const extensionSet = normalizeExtensionSet(options.fileExtensions);
const files: AgentFilesystemFileEntry[] = [];
const stack: string[] = [requestedRootPath];
while (stack.length > 0) {
const currentDir = stack.pop();
if (!currentDir) continue;
let entries: Dirent[];
try {
entries = await readdir(currentDir, { withFileTypes: true });
} catch {
continue;
}
for (const entry of entries) {
if (entry.name.startsWith(".") || excludePatterns.has(entry.name)) {
continue;
}
const absolutePath = join(currentDir, entry.name);
if (entry.isDirectory()) {
stack.push(absolutePath);
continue;
}
if (!entry.isFile()) {
continue;
}
if (extensionSet) {
const extension = extname(entry.name).toLowerCase();
if (!extensionSet.has(extension)) {
continue;
}
}
try {
const fileStat = await stat(absolutePath);
if (!fileStat.isFile()) {
continue;
}
files.push({
relativePath: relative(requestedRootPath, absolutePath).replace(/\\/g, "/"),
fullPath: absolutePath,
size: fileStat.size,
mtimeMs: fileStat.mtimeMs,
});
} catch {
// Files can disappear while scanning.
}
}
}
return files;
}
function parseMountedVirtualPath(
virtualPath: string,
mounts: LocalRootMount[]
@ -231,8 +419,8 @@ function toMountedVirtualPath(mount: string, rootPath: string, absolutePath: str
return `/${mount}${relativePath}`;
}
async function resolveCurrentRootPaths(): Promise<string[]> {
const settings = await getAgentFilesystemSettings();
async function resolveCurrentRootPaths(searchSpaceId?: number | null): Promise<string[]> {
const settings = await getAgentFilesystemSettings(searchSpaceId);
if (settings.localRootPaths.length === 0) {
throw new Error("No local filesystem roots selected");
}
@ -240,9 +428,10 @@ async function resolveCurrentRootPaths(): Promise<string[]> {
}
export async function readAgentLocalFileText(
virtualPath: string
virtualPath: string,
searchSpaceId?: number | null
): Promise<{ path: string; content: string }> {
const rootPaths = await resolveCurrentRootPaths();
const rootPaths = await resolveCurrentRootPaths(searchSpaceId);
const mounts = buildRootMounts(rootPaths);
const { mount, subPath } = parseMountedVirtualPath(virtualPath, mounts);
const rootMount = findMountByName(mounts, mount);
@ -261,9 +450,10 @@ export async function readAgentLocalFileText(
export async function writeAgentLocalFileText(
virtualPath: string,
content: string
content: string,
searchSpaceId?: number | null
): Promise<{ path: string }> {
const rootPaths = await resolveCurrentRootPaths();
const rootPaths = await resolveCurrentRootPaths(searchSpaceId);
const mounts = buildRootMounts(rootPaths);
const { mount, subPath } = parseMountedVirtualPath(virtualPath, mounts);
const rootMount = findMountByName(mounts, mount);

View file

@ -71,10 +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),
readAgentLocalFileText: (virtualPath: string, searchSpaceId?: number | null) =>
ipcRenderer.invoke(IPC_CHANNELS.READ_AGENT_LOCAL_FILE_TEXT, virtualPath, searchSpaceId),
writeAgentLocalFileText: (virtualPath: string, content: string, searchSpaceId?: number | null) =>
ipcRenderer.invoke(IPC_CHANNELS.WRITE_AGENT_LOCAL_FILE_TEXT, virtualPath, content, searchSpaceId),
// Auth token sync across windows
getAuthTokens: () => ipcRenderer.invoke(IPC_CHANNELS.GET_AUTH_TOKENS),
@ -106,13 +106,20 @@ contextBridge.exposeInMainWorld('electronAPI', {
ipcRenderer.invoke(IPC_CHANNELS.ANALYTICS_CAPTURE, { event, properties }),
getAnalyticsContext: () => ipcRenderer.invoke(IPC_CHANNELS.ANALYTICS_GET_CONTEXT),
// Agent filesystem mode
getAgentFilesystemSettings: () =>
ipcRenderer.invoke(IPC_CHANNELS.AGENT_FILESYSTEM_GET_SETTINGS),
getAgentFilesystemMounts: () =>
ipcRenderer.invoke(IPC_CHANNELS.AGENT_FILESYSTEM_GET_MOUNTS),
getAgentFilesystemSettings: (searchSpaceId?: number | null) =>
ipcRenderer.invoke(IPC_CHANNELS.AGENT_FILESYSTEM_GET_SETTINGS, searchSpaceId),
getAgentFilesystemMounts: (searchSpaceId?: number | null) =>
ipcRenderer.invoke(IPC_CHANNELS.AGENT_FILESYSTEM_GET_MOUNTS, searchSpaceId),
listAgentFilesystemFiles: (options: {
rootPath: string;
searchSpaceId?: number | null;
excludePatterns?: string[] | null;
fileExtensions?: string[] | null;
}) => ipcRenderer.invoke(IPC_CHANNELS.AGENT_FILESYSTEM_LIST_FILES, options),
setAgentFilesystemSettings: (settings: {
mode?: "cloud" | "desktop_local_folder";
localRootPaths?: string[] | null;
}) => ipcRenderer.invoke(IPC_CHANNELS.AGENT_FILESYSTEM_SET_SETTINGS, settings),
}, searchSpaceId?: number | null) =>
ipcRenderer.invoke(IPC_CHANNELS.AGENT_FILESYSTEM_SET_SETTINGS, { searchSpaceId, settings }),
pickAgentFilesystemRoot: () => ipcRenderer.invoke(IPC_CHANNELS.AGENT_FILESYSTEM_PICK_ROOT),
});

View file

@ -658,7 +658,7 @@ export default function NewChatPage() {
try {
const backendUrl = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL || "http://localhost:8000";
const selection = await getAgentFilesystemSelection();
const selection = await getAgentFilesystemSelection(searchSpaceId);
if (
selection.filesystem_mode === "desktop_local_folder" &&
(!selection.local_filesystem_mounts ||
@ -1088,7 +1088,7 @@ export default function NewChatPage() {
try {
const backendUrl = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL || "http://localhost:8000";
const selection = await getAgentFilesystemSelection();
const selection = await getAgentFilesystemSelection(searchSpaceId);
const response = await fetch(`${backendUrl}/api/v1/threads/${resumeThreadId}/resume`, {
method: "POST",
headers: {
@ -1424,7 +1424,7 @@ export default function NewChatPage() {
]);
try {
const selection = await getAgentFilesystemSelection();
const selection = await getAgentFilesystemSelection(searchSpaceId);
const response = await fetch(getRegenerateUrl(threadId), {
method: "POST",
headers: {

View file

@ -124,7 +124,10 @@ export function EditorPanelContent({
if (!electronAPI?.readAgentLocalFileText) {
throw new Error("Local file editor is available only in desktop mode.");
}
const readResult = await electronAPI.readAgentLocalFileText(localFilePath);
const readResult = await electronAPI.readAgentLocalFileText(
localFilePath,
searchSpaceId
);
if (!readResult.ok) {
throw new Error(readResult.error || "Failed to read local file");
}
@ -226,7 +229,7 @@ export function EditorPanelContent({
}
}, [editorDoc?.source_markdown]);
const handleSave = useCallback(async (options?: { silent?: boolean }) => {
const handleSave = useCallback(async (_options?: { silent?: boolean }) => {
setSaving(true);
try {
if (isLocalFileMode) {
@ -239,7 +242,8 @@ export function EditorPanelContent({
const contentToSave = markdownRef.current;
const writeResult = await electronAPI.writeAgentLocalFileText(
localFilePath,
contentToSave
contentToSave,
searchSpaceId
);
if (!writeResult.ok) {
throw new Error(writeResult.error || "Failed to save local file");

View file

@ -214,7 +214,7 @@ function AuthenticatedDocumentsSidebar({
if (!electronAPI?.getAgentFilesystemSettings) return;
let mounted = true;
electronAPI
.getAgentFilesystemSettings()
.getAgentFilesystemSettings(searchSpaceId)
.then((settings: FilesystemSettings) => {
if (!mounted) return;
setFilesystemSettings(settings);
@ -230,7 +230,7 @@ function AuthenticatedDocumentsSidebar({
return () => {
mounted = false;
};
}, [electronAPI]);
}, [electronAPI, searchSpaceId]);
const hasLocalFilesystemTrust = useCallback(() => {
try {
@ -253,10 +253,10 @@ function AuthenticatedDocumentsSidebar({
const updated = await electronAPI.setAgentFilesystemSettings({
mode: "desktop_local_folder",
localRootPaths: nextLocalRootPaths,
});
}, searchSpaceId);
setFilesystemSettings(updated);
},
[electronAPI, localRootPaths]
[electronAPI, localRootPaths, searchSpaceId]
);
const runPickLocalRoot = useCallback(async () => {
@ -285,10 +285,10 @@ function AuthenticatedDocumentsSidebar({
const updated = await electronAPI.setAgentFilesystemSettings({
mode: "desktop_local_folder",
localRootPaths: localRootPaths.filter((rootPath) => rootPath !== rootPathToRemove),
});
}, searchSpaceId);
setFilesystemSettings(updated);
},
[electronAPI, localRootPaths]
[electronAPI, localRootPaths, searchSpaceId]
);
const handleClearFilesystemRoots = useCallback(async () => {
@ -296,19 +296,19 @@ function AuthenticatedDocumentsSidebar({
const updated = await electronAPI.setAgentFilesystemSettings({
mode: "desktop_local_folder",
localRootPaths: [],
});
}, searchSpaceId);
setFilesystemSettings(updated);
}, [electronAPI]);
}, [electronAPI, searchSpaceId]);
const handleFilesystemTabChange = useCallback(
async (tab: "cloud" | "local") => {
if (!electronAPI?.setAgentFilesystemSettings) return;
const updated = await electronAPI.setAgentFilesystemSettings({
mode: tab === "cloud" ? "cloud" : "desktop_local_folder",
});
}, searchSpaceId);
setFilesystemSettings(updated);
},
[electronAPI]
[electronAPI, searchSpaceId]
);
// AI File Sort state
@ -1323,6 +1323,7 @@ function AuthenticatedDocumentsSidebar({
<LocalFilesystemBrowser
rootPaths={localRootPaths}
searchSpaceId={searchSpaceId}
active={currentFilesystemTab === "local"}
searchQuery={debouncedLocalSearch.trim() || undefined}
onOpenFile={(localFilePath) => {
openEditorPanel({

View file

@ -11,6 +11,7 @@ import { getSupportedExtensionsSet } from "@/lib/supported-extensions";
interface LocalFilesystemBrowserProps {
rootPaths: string[];
searchSpaceId: number;
active?: boolean;
searchQuery?: string;
onOpenFile: (fullPath: string) => void;
}
@ -75,6 +76,7 @@ function toMountedVirtualPath(mount: string, relativePath: string): string {
export function LocalFilesystemBrowser({
rootPaths,
searchSpaceId,
active = true,
searchQuery,
onOpenFile,
}: LocalFilesystemBrowserProps) {
@ -84,13 +86,36 @@ export function LocalFilesystemBrowser({
const [mountByRootKey, setMountByRootKey] = useState<Map<string, string>>(new Map());
const [mountStatus, setMountStatus] = useState<MountLoadStatus>("idle");
const [mountRefreshInFlight, setMountRefreshInFlight] = useState(false);
const lastLoadedRootsSignatureRef = useRef<string>("");
const hasLoadedMountsOnceRef = useRef(false);
const hasResolvedAtLeastOneRootRef = useRef(false);
const supportedExtensions = useMemo(() => Array.from(getSupportedExtensionsSet()), []);
const isWindowsPlatform = electronAPI?.versions.platform === "win32";
useEffect(() => {
if (!electronAPI?.listFolderFiles) return;
if (!active) return;
if (!electronAPI?.listAgentFilesystemFiles) {
for (const rootPath of rootPaths) {
setRootStateMap((prev) => ({
...prev,
[rootPath]: {
loading: false,
error: "Desktop app update required for local mode browsing.",
files: [],
},
}));
}
return;
}
const rootsSignature = rootPaths
.map((rootPath) => normalizeRootPathForLookup(rootPath, isWindowsPlatform))
.sort()
.join("|");
const settingsSignature = `${searchSpaceId}:${rootsSignature}`;
if (settingsSignature === lastLoadedRootsSignatureRef.current) {
return;
}
lastLoadedRootsSignatureRef.current = settingsSignature;
let cancelled = false;
for (const rootPath of rootPaths) {
@ -107,14 +132,11 @@ export function LocalFilesystemBrowser({
void Promise.all(
rootPaths.map(async (rootPath) => {
try {
const files = (await electronAPI.listFolderFiles({
path: rootPath,
name: getFolderDisplayName(rootPath),
const files = (await electronAPI.listAgentFilesystemFiles({
rootPath,
searchSpaceId,
excludePatterns: DEFAULT_EXCLUDE_PATTERNS,
fileExtensions: supportedExtensions,
rootFolderId: null,
searchSpaceId,
active: true,
})) as LocalFolderFileEntry[];
if (cancelled) return;
setRootStateMap((prev) => ({
@ -142,7 +164,7 @@ export function LocalFilesystemBrowser({
return () => {
cancelled = true;
};
}, [electronAPI, rootPaths, searchSpaceId, supportedExtensions]);
}, [active, electronAPI, isWindowsPlatform, rootPaths, searchSpaceId, supportedExtensions]);
useEffect(() => {
if (!electronAPI?.getAgentFilesystemMounts) {
@ -165,7 +187,7 @@ export function LocalFilesystemBrowser({
setMountRefreshInFlight(true);
}
void electronAPI
.getAgentFilesystemMounts()
.getAgentFilesystemMounts(searchSpaceId)
.then((mounts: LocalRootMount[]) => {
if (cancelled) return;
const next = new Map<string, string>();
@ -191,7 +213,7 @@ export function LocalFilesystemBrowser({
return () => {
cancelled = true;
};
}, [electronAPI, isWindowsPlatform, rootPaths]);
}, [electronAPI, isWindowsPlatform, rootPaths, searchSpaceId]);
const treeByRoot = useMemo(() => {
const query = searchQuery?.trim().toLowerCase() ?? "";

View file

@ -22,15 +22,17 @@ export function getClientPlatform(): ClientPlatform {
return window.electronAPI ? "desktop" : "web";
}
export async function getAgentFilesystemSelection(): Promise<AgentFilesystemSelection> {
export async function getAgentFilesystemSelection(
searchSpaceId?: number | null
): Promise<AgentFilesystemSelection> {
const platform = getClientPlatform();
if (platform !== "desktop" || !window.electronAPI?.getAgentFilesystemSettings) {
return { ...DEFAULT_SELECTION, client_platform: platform };
}
try {
const settings = await window.electronAPI.getAgentFilesystemSettings();
const settings = await window.electronAPI.getAgentFilesystemSettings(searchSpaceId);
if (settings.mode === "desktop_local_folder") {
const mounts = await window.electronAPI.getAgentFilesystemMounts?.();
const mounts = await window.electronAPI.getAgentFilesystemMounts?.(searchSpaceId);
const localFilesystemMounts =
mounts?.map((entry) => ({
mount_id: entry.mount,

View file

@ -54,6 +54,13 @@ interface AgentFilesystemMount {
rootPath: string;
}
interface AgentFilesystemListOptions {
rootPath: string;
searchSpaceId?: number | null;
excludePatterns?: string[] | null;
fileExtensions?: string[] | null;
}
interface LocalTextFileResult {
ok: boolean;
path: string;
@ -114,10 +121,14 @@ interface ElectronAPI {
// Browse files/folders via native dialogs
browseFiles: () => Promise<string[] | null>;
readLocalFiles: (paths: string[]) => Promise<LocalFileData[]>;
readAgentLocalFileText: (virtualPath: string) => Promise<LocalTextFileResult>;
readAgentLocalFileText: (
virtualPath: string,
searchSpaceId?: number | null
) => Promise<LocalTextFileResult>;
writeAgentLocalFileText: (
virtualPath: string,
content: string
content: string,
searchSpaceId?: number | null
) => Promise<LocalTextFileResult>;
// Auth token sync across windows
getAuthTokens: () => Promise<{ bearer: string; refresh: string } | null>;
@ -151,12 +162,15 @@ interface ElectronAPI {
platform: string;
}>;
// Agent filesystem mode
getAgentFilesystemSettings: () => Promise<AgentFilesystemSettings>;
getAgentFilesystemMounts: () => Promise<AgentFilesystemMount[]>;
getAgentFilesystemSettings: (searchSpaceId?: number | null) => Promise<AgentFilesystemSettings>;
getAgentFilesystemMounts: (searchSpaceId?: number | null) => Promise<AgentFilesystemMount[]>;
listAgentFilesystemFiles: (
options: AgentFilesystemListOptions
) => Promise<FolderFileEntry[]>;
setAgentFilesystemSettings: (settings: {
mode?: AgentFilesystemMode;
localRootPaths?: string[] | null;
}) => Promise<AgentFilesystemSettings>;
}, searchSpaceId?: number | null) => Promise<AgentFilesystemSettings>;
pickAgentFilesystemRoot: () => Promise<string | null>;
}