feat(web): connect new chat UI to agent filesystem APIs

This commit is contained in:
Anish Sarkar 2026-04-23 15:46:39 +05:30
parent 5c3a327a0c
commit 4899588cd7
5 changed files with 209 additions and 1 deletions

View file

@ -0,0 +1,74 @@
import { app, dialog } from "electron";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
export type AgentFilesystemMode = "cloud" | "desktop_local_folder";
export interface AgentFilesystemSettings {
mode: AgentFilesystemMode;
localRootPath: string | null;
updatedAt: string;
}
const SETTINGS_FILENAME = "agent-filesystem-settings.json";
function getSettingsPath(): string {
return join(app.getPath("userData"), SETTINGS_FILENAME);
}
function getDefaultSettings(): AgentFilesystemSettings {
return {
mode: "cloud",
localRootPath: null,
updatedAt: new Date().toISOString(),
};
}
export async function getAgentFilesystemSettings(): Promise<AgentFilesystemSettings> {
try {
const raw = await readFile(getSettingsPath(), "utf8");
const parsed = JSON.parse(raw) as Partial<AgentFilesystemSettings>;
if (parsed.mode !== "cloud" && parsed.mode !== "desktop_local_folder") {
return getDefaultSettings();
}
return {
mode: parsed.mode,
localRootPath: parsed.localRootPath ?? null,
updatedAt: parsed.updatedAt ?? new Date().toISOString(),
};
} catch {
return getDefaultSettings();
}
}
export async function setAgentFilesystemSettings(
settings: Partial<Pick<AgentFilesystemSettings, "mode" | "localRootPath">>
): Promise<AgentFilesystemSettings> {
const current = await getAgentFilesystemSettings();
const nextMode =
settings.mode === "cloud" || settings.mode === "desktop_local_folder"
? settings.mode
: current.mode;
const next: AgentFilesystemSettings = {
mode: nextMode,
localRootPath:
settings.localRootPath === undefined ? current.localRootPath : settings.localRootPath,
updatedAt: new Date().toISOString(),
};
const settingsPath = getSettingsPath();
await mkdir(dirname(settingsPath), { recursive: true });
await writeFile(settingsPath, JSON.stringify(next, null, 2), "utf8");
return next;
}
export async function pickAgentFilesystemRoot(): Promise<string | null> {
const result = await dialog.showOpenDialog({
title: "Select local folder for Agent Filesystem",
properties: ["openDirectory"],
});
if (result.canceled || result.filePaths.length === 0) {
return null;
}
return result.filePaths[0] ?? null;
}

View file

@ -46,6 +46,7 @@ import {
import { useChatSessionStateSync } from "@/hooks/use-chat-session-state";
import { useMessagesSync } from "@/hooks/use-messages-sync";
import { documentsApiService } from "@/lib/apis/documents-api.service";
import { getAgentFilesystemSelection } from "@/lib/agent-filesystem";
import { getBearerToken } from "@/lib/auth-utils";
import { convertToThreadMessage } from "@/lib/chat/message-utils";
import {
@ -656,6 +657,14 @@ export default function NewChatPage() {
try {
const backendUrl = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL || "http://localhost:8000";
const selection = await getAgentFilesystemSelection();
if (
selection.filesystem_mode === "desktop_local_folder" &&
!selection.local_filesystem_root
) {
toast.error("Select a local folder before using Local Folder mode.");
return;
}
// Build message history for context
const messageHistory = messages
@ -691,6 +700,9 @@ export default function NewChatPage() {
chat_id: currentThreadId,
user_query: userQuery.trim(),
search_space_id: searchSpaceId,
filesystem_mode: selection.filesystem_mode,
client_platform: selection.client_platform,
local_filesystem_root: selection.local_filesystem_root,
messages: messageHistory,
mentioned_document_ids: hasDocumentIds ? mentionedDocumentIds.document_ids : undefined,
mentioned_surfsense_doc_ids: hasSurfsenseDocIds
@ -1074,6 +1086,7 @@ export default function NewChatPage() {
try {
const backendUrl = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL || "http://localhost:8000";
const selection = await getAgentFilesystemSelection();
const response = await fetch(`${backendUrl}/api/v1/threads/${resumeThreadId}/resume`, {
method: "POST",
headers: {
@ -1083,6 +1096,9 @@ export default function NewChatPage() {
body: JSON.stringify({
search_space_id: searchSpaceId,
decisions,
filesystem_mode: selection.filesystem_mode,
client_platform: selection.client_platform,
local_filesystem_root: selection.local_filesystem_root,
}),
signal: controller.signal,
});
@ -1406,6 +1422,7 @@ export default function NewChatPage() {
]);
try {
const selection = await getAgentFilesystemSelection();
const response = await fetch(getRegenerateUrl(threadId), {
method: "POST",
headers: {
@ -1416,6 +1433,9 @@ export default function NewChatPage() {
search_space_id: searchSpaceId,
user_query: newUserQuery || null,
disabled_tools: disabledTools.length > 0 ? disabledTools : undefined,
filesystem_mode: selection.filesystem_mode,
client_platform: selection.client_platform,
local_filesystem_root: selection.local_filesystem_root,
}),
signal: controller.signal,
});

View file

@ -94,6 +94,12 @@ import { cn } from "@/lib/utils";
const COMPOSER_PLACEHOLDER = "Ask anything, type / for prompts, type @ to mention docs";
type ComposerFilesystemSettings = {
mode: "cloud" | "desktop_local_folder";
localRootPath: string | null;
updatedAt: string;
};
export const Thread: FC = () => {
return <ThreadContent />;
};
@ -362,6 +368,9 @@ const Composer: FC = () => {
}, []);
const electronAPI = useElectronAPI();
const [filesystemSettings, setFilesystemSettings] = useState<ComposerFilesystemSettings | null>(
null
);
const [clipboardInitialText, setClipboardInitialText] = useState<string | undefined>();
const clipboardLoadedRef = useRef(false);
useEffect(() => {
@ -374,6 +383,48 @@ const Composer: FC = () => {
});
}, [electronAPI]);
useEffect(() => {
if (!electronAPI?.getAgentFilesystemSettings) return;
let mounted = true;
electronAPI
.getAgentFilesystemSettings()
.then((settings) => {
if (!mounted) return;
setFilesystemSettings(settings);
})
.catch(() => {
if (!mounted) return;
setFilesystemSettings({
mode: "cloud",
localRootPath: null,
updatedAt: new Date().toISOString(),
});
});
return () => {
mounted = false;
};
}, [electronAPI]);
const handleFilesystemModeChange = useCallback(
async (mode: "cloud" | "desktop_local_folder") => {
if (!electronAPI?.setAgentFilesystemSettings) return;
const updated = await electronAPI.setAgentFilesystemSettings({ mode });
setFilesystemSettings(updated);
},
[electronAPI]
);
const handlePickFilesystemRoot = useCallback(async () => {
if (!electronAPI?.pickAgentFilesystemRoot || !electronAPI?.setAgentFilesystemSettings) return;
const picked = await electronAPI.pickAgentFilesystemRoot();
if (!picked) return;
const updated = await electronAPI.setAgentFilesystemSettings({
mode: "desktop_local_folder",
localRootPath: picked,
});
setFilesystemSettings(updated);
}, [electronAPI]);
const isThreadEmpty = useAuiState(({ thread }) => thread.isEmpty);
const isThreadRunning = useAuiState(({ thread }) => thread.isRunning);
@ -668,6 +719,45 @@ const Composer: FC = () => {
currentUserId={currentUser?.id ?? null}
members={members ?? []}
/>
{electronAPI && filesystemSettings ? (
<div className="mx-3 flex items-center gap-2 rounded-xl border border-border/60 bg-background/60 p-1.5">
<button
type="button"
onClick={() => handleFilesystemModeChange("cloud")}
className={cn(
"rounded-lg px-2.5 py-1 text-xs font-medium transition-colors",
filesystemSettings.mode === "cloud"
? "bg-accent text-foreground"
: "text-muted-foreground hover:bg-accent/60 hover:text-foreground"
)}
>
Cloud
</button>
<button
type="button"
onClick={() => handleFilesystemModeChange("desktop_local_folder")}
className={cn(
"rounded-lg px-2.5 py-1 text-xs font-medium transition-colors",
filesystemSettings.mode === "desktop_local_folder"
? "bg-accent text-foreground"
: "text-muted-foreground hover:bg-accent/60 hover:text-foreground"
)}
>
Local
</button>
<div className="h-4 w-px bg-border/70" />
<button
type="button"
onClick={handlePickFilesystemRoot}
className="min-w-0 flex-1 rounded-lg px-2.5 py-1 text-left text-xs text-muted-foreground hover:bg-accent/60 hover:text-foreground transition-colors"
title={filesystemSettings.localRootPath ?? "Select local folder"}
>
{filesystemSettings.localRootPath
? filesystemSettings.localRootPath.split("/").at(-1) || filesystemSettings.localRootPath
: "Select folder..."}
</button>
</div>
) : null}
{showDocumentPopover && (
<div className="absolute bottom-full left-0 z-[9999] mb-2">
<DocumentMentionPicker
@ -1104,7 +1194,13 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
group.tools.flatMap((t, i) =>
i === 0
? [t.description]
: [<Dot key={i} className="inline h-4 w-4" />, t.description]
: [
<Dot
key={`dot-${group.label}-${t.description}`}
className="inline h-4 w-4"
/>,
t.description,
]
)}
</TooltipContent>
</Tooltip>

View file

@ -1,4 +1,5 @@
import type { ZodType } from "zod";
import { getClientPlatform } from "../agent-filesystem";
import { getBearerToken, handleUnauthorized, refreshAccessToken } from "../auth-utils";
import {
AbortedError,
@ -75,6 +76,8 @@ class BaseApiService {
const defaultOptions: RequestOptions = {
headers: {
Authorization: `Bearer ${this.bearerToken || ""}`,
"X-SurfSense-Client-Platform":
typeof window === "undefined" ? "web" : getClientPlatform(),
},
method: "GET",
responseType: ResponseType.JSON,

View file

@ -41,6 +41,14 @@ interface FolderFileEntry {
mtimeMs: number;
}
type AgentFilesystemMode = "cloud" | "desktop_local_folder";
interface AgentFilesystemSettings {
mode: AgentFilesystemMode;
localRootPath: string | null;
updatedAt: string;
}
interface ElectronAPI {
versions: {
electron: string;
@ -125,6 +133,13 @@ interface ElectronAPI {
appVersion: string;
platform: string;
}>;
// Agent filesystem mode
getAgentFilesystemSettings: () => Promise<AgentFilesystemSettings>;
setAgentFilesystemSettings: (settings: {
mode?: AgentFilesystemMode;
localRootPath?: string | null;
}) => Promise<AgentFilesystemSettings>;
pickAgentFilesystemRoot: () => Promise<string | null>;
}
declare global {