- {filesystemSettings.localRootPath ? (
+
+ {primaryLocalRootPath ? (
<>
- {filesystemSettings.localRootPath.split("/").at(-1) ||
- filesystemSettings.localRootPath}
+ {getFolderDisplayName(primaryLocalRootPath)}
+ {extraLocalRootCount > 0 && (
+
+
+
+
+
+
+ {localRootPaths.map((rootPath) => (
+
+
+
+ {getFolderDisplayName(rootPath)}
+
+
+
+ ))}
+
+
+
+
+
+
+ )}
@@ -909,9 +1001,9 @@ const Composer: FC = () => {
Local mode can read and edit files inside the folders you select. Continue only if
you trust this workspace and its contents.
- {(pendingLocalPath || filesystemSettings?.localRootPath) && (
+ {(pendingLocalPath || primaryLocalRootPath) && (
- Folder path: {pendingLocalPath || filesystemSettings?.localRootPath}
+ Folder path: {pendingLocalPath || primaryLocalRootPath}
)}
From c1a07a093e46c760c370df05c01f5c126d198286 Mon Sep 17 00:00:00 2001
From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com>
Date: Fri, 24 Apr 2026 01:46:44 +0530
Subject: [PATCH 26/35] refactor(sidebar): use Monitor icon for system theme
option
---
.../components/layout/ui/sidebar/SidebarUserProfile.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/surfsense_web/components/layout/ui/sidebar/SidebarUserProfile.tsx b/surfsense_web/components/layout/ui/sidebar/SidebarUserProfile.tsx
index 81fbeef91..acece2d5c 100644
--- a/surfsense_web/components/layout/ui/sidebar/SidebarUserProfile.tsx
+++ b/surfsense_web/components/layout/ui/sidebar/SidebarUserProfile.tsx
@@ -7,8 +7,8 @@ import {
ExternalLink,
Info,
Languages,
- Laptop,
LogOut,
+ Monitor,
Moon,
Sun,
UserCog,
@@ -49,7 +49,7 @@ const LANGUAGES = [
const THEMES = [
{ value: "light" as const, name: "Light", icon: Sun },
{ value: "dark" as const, name: "Dark", icon: Moon },
- { value: "system" as const, name: "System", icon: Laptop },
+ { value: "system" as const, name: "System", icon: Monitor },
];
const LEARN_MORE_LINKS = [
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 27/35] 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) => {
From 17f9ee4b592d3ba696333c818dbcf51f6320a59d Mon Sep 17 00:00:00 2001
From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com>
Date: Fri, 24 Apr 2026 02:33:57 +0530
Subject: [PATCH 28/35] refactor(icons): replace 'Pen' icon with 'Pencil'
across various components for consistency
---
.../user-settings/components/MemoryContent.tsx | 4 ++--
.../user-settings/components/PromptsContent.tsx | 4 ++--
surfsense_web/components/assistant-ui/user-message.tsx | 4 ++--
.../chat-comments/comment-item/comment-actions.tsx | 4 ++--
surfsense_web/components/documents/DocumentNode.tsx | 6 +++---
surfsense_web/components/documents/FolderNode.tsx | 6 +++---
.../components/layout/ui/sidebar/AllPrivateChatsSidebar.tsx | 4 ++--
.../components/layout/ui/sidebar/AllSharedChatsSidebar.tsx | 4 ++--
surfsense_web/components/layout/ui/sidebar/ChatListItem.tsx | 4 ++--
surfsense_web/components/layout/ui/sidebar/Sidebar.tsx | 4 ++--
.../components/layout/ui/tabs/DocumentTabContent.tsx | 4 ++--
surfsense_web/components/settings/team-memory-manager.tsx | 4 ++--
.../tool-ui/confluence/create-confluence-page.tsx | 4 ++--
.../tool-ui/confluence/update-confluence-page.tsx | 4 ++--
surfsense_web/components/tool-ui/dropbox/create-file.tsx | 4 ++--
surfsense_web/components/tool-ui/generic-hitl-approval.tsx | 4 ++--
surfsense_web/components/tool-ui/gmail/create-draft.tsx | 4 ++--
surfsense_web/components/tool-ui/gmail/send-email.tsx | 4 ++--
surfsense_web/components/tool-ui/gmail/update-draft.tsx | 4 ++--
.../components/tool-ui/google-calendar/create-event.tsx | 4 ++--
.../components/tool-ui/google-calendar/update-event.tsx | 4 ++--
.../components/tool-ui/google-drive/create-file.tsx | 4 ++--
surfsense_web/components/tool-ui/jira/create-jira-issue.tsx | 4 ++--
surfsense_web/components/tool-ui/jira/update-jira-issue.tsx | 4 ++--
.../components/tool-ui/linear/create-linear-issue.tsx | 4 ++--
.../components/tool-ui/linear/update-linear-issue.tsx | 4 ++--
.../components/tool-ui/notion/create-notion-page.tsx | 4 ++--
.../components/tool-ui/notion/update-notion-page.tsx | 4 ++--
surfsense_web/components/tool-ui/onedrive/create-file.tsx | 4 ++--
surfsense_web/components/ui/mode-toolbar-button.tsx | 4 ++--
30 files changed, 62 insertions(+), 62 deletions(-)
diff --git a/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/MemoryContent.tsx b/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/MemoryContent.tsx
index ef17e5a89..3d0550b6c 100644
--- a/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/MemoryContent.tsx
+++ b/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/MemoryContent.tsx
@@ -1,7 +1,7 @@
"use client";
import { useAtomValue } from "jotai";
-import { ArrowUp, ChevronDown, ClipboardCopy, Download, Info, Pen } from "lucide-react";
+import { ArrowUp, ChevronDown, ClipboardCopy, Download, Info, Pencil } from "lucide-react";
import { useCallback, useEffect, useRef, useState } from "react";
import { toast } from "sonner";
import { z } from "zod";
@@ -241,7 +241,7 @@ export function MemoryContent() {
onClick={openInput}
className="absolute bottom-3 right-3 z-10 h-[54px] w-[54px] rounded-full border bg-muted/60 backdrop-blur-sm shadow-sm"
>
-
+
)}
diff --git a/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/PromptsContent.tsx b/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/PromptsContent.tsx
index 1e7087afc..c78d4f9f0 100644
--- a/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/PromptsContent.tsx
+++ b/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/PromptsContent.tsx
@@ -1,7 +1,7 @@
"use client";
import { useAtomValue } from "jotai";
-import { AlertTriangle, Globe, Lock, PenLine, Sparkles, Trash2 } from "lucide-react";
+import { AlertTriangle, Globe, Lock, Pencil, Sparkles, Trash2 } from "lucide-react";
import { useCallback, useState } from "react";
import { toast } from "sonner";
import {
@@ -308,7 +308,7 @@ export function PromptsContent() {
className="size-7"
onClick={() => handleEdit(prompt)}
>
-
+
)}
diff --git a/surfsense_web/components/tool-ui/confluence/update-confluence-page.tsx b/surfsense_web/components/tool-ui/confluence/update-confluence-page.tsx
index 2038f7a0e..c30357fb6 100644
--- a/surfsense_web/components/tool-ui/confluence/update-confluence-page.tsx
+++ b/surfsense_web/components/tool-ui/confluence/update-confluence-page.tsx
@@ -2,7 +2,7 @@
import type { ToolCallMessagePartProps } from "@assistant-ui/react";
import { useSetAtom } from "jotai";
-import { CornerDownLeftIcon, Pen } from "lucide-react";
+import { CornerDownLeftIcon, Pencil } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
import { PlateEditor } from "@/components/editor/plate-editor";
@@ -241,7 +241,7 @@ function ApprovalCard({
});
}}
>
-
+
Edit
)}
diff --git a/surfsense_web/components/tool-ui/dropbox/create-file.tsx b/surfsense_web/components/tool-ui/dropbox/create-file.tsx
index 02eae2c83..f76a45f62 100644
--- a/surfsense_web/components/tool-ui/dropbox/create-file.tsx
+++ b/surfsense_web/components/tool-ui/dropbox/create-file.tsx
@@ -2,7 +2,7 @@
import type { ToolCallMessagePartProps } from "@assistant-ui/react";
import { useSetAtom } from "jotai";
-import { CornerDownLeftIcon, FileIcon, Pen } from "lucide-react";
+import { CornerDownLeftIcon, FileIcon, Pencil } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
import { PlateEditor } from "@/components/editor/plate-editor";
@@ -224,7 +224,7 @@ function ApprovalCard({
});
}}
>
-
+
Edit
)}
diff --git a/surfsense_web/components/tool-ui/generic-hitl-approval.tsx b/surfsense_web/components/tool-ui/generic-hitl-approval.tsx
index 809b76c38..d4ee61eeb 100644
--- a/surfsense_web/components/tool-ui/generic-hitl-approval.tsx
+++ b/surfsense_web/components/tool-ui/generic-hitl-approval.tsx
@@ -1,7 +1,7 @@
"use client";
import type { ToolCallMessagePartComponent } from "@assistant-ui/react";
-import { CornerDownLeftIcon, Pen } from "lucide-react";
+import { CornerDownLeftIcon, Pencil } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { Button } from "@/components/ui/button";
@@ -167,7 +167,7 @@ function GenericApprovalCard({
className="rounded-lg text-muted-foreground -mt-1 -mr-2"
onClick={() => setIsEditing(true)}
>
-
+
Edit
)}
diff --git a/surfsense_web/components/tool-ui/gmail/create-draft.tsx b/surfsense_web/components/tool-ui/gmail/create-draft.tsx
index cfe61351a..a00760ca3 100644
--- a/surfsense_web/components/tool-ui/gmail/create-draft.tsx
+++ b/surfsense_web/components/tool-ui/gmail/create-draft.tsx
@@ -2,7 +2,7 @@
import type { ToolCallMessagePartProps } from "@assistant-ui/react";
import { useSetAtom } from "jotai";
-import { CornerDownLeftIcon, Pen, UserIcon, UsersIcon } from "lucide-react";
+import { CornerDownLeftIcon, Pencil, UserIcon, UsersIcon } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import type { ExtraField } from "@/atoms/chat/hitl-edit-panel.atom";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
@@ -251,7 +251,7 @@ function ApprovalCard({
});
}}
>
-
+
Edit
)}
diff --git a/surfsense_web/components/tool-ui/gmail/send-email.tsx b/surfsense_web/components/tool-ui/gmail/send-email.tsx
index a21ece7b3..c22045fa1 100644
--- a/surfsense_web/components/tool-ui/gmail/send-email.tsx
+++ b/surfsense_web/components/tool-ui/gmail/send-email.tsx
@@ -2,7 +2,7 @@
import type { ToolCallMessagePartProps } from "@assistant-ui/react";
import { useSetAtom } from "jotai";
-import { CornerDownLeftIcon, MailIcon, Pen, UserIcon, UsersIcon } from "lucide-react";
+import { CornerDownLeftIcon, MailIcon, Pencil, UserIcon, UsersIcon } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import type { ExtraField } from "@/atoms/chat/hitl-edit-panel.atom";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
@@ -250,7 +250,7 @@ function ApprovalCard({
});
}}
>
-
+
Edit
)}
diff --git a/surfsense_web/components/tool-ui/gmail/update-draft.tsx b/surfsense_web/components/tool-ui/gmail/update-draft.tsx
index 0cbf338d7..b8c8c10f6 100644
--- a/surfsense_web/components/tool-ui/gmail/update-draft.tsx
+++ b/surfsense_web/components/tool-ui/gmail/update-draft.tsx
@@ -2,7 +2,7 @@
import type { ToolCallMessagePartProps } from "@assistant-ui/react";
import { useSetAtom } from "jotai";
-import { CornerDownLeftIcon, MailIcon, Pen, UserIcon, UsersIcon } from "lucide-react";
+import { CornerDownLeftIcon, MailIcon, Pencil, UserIcon, UsersIcon } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import type { ExtraField } from "@/atoms/chat/hitl-edit-panel.atom";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
@@ -283,7 +283,7 @@ function ApprovalCard({
});
}}
>
-
+
Edit
)}
diff --git a/surfsense_web/components/tool-ui/google-calendar/create-event.tsx b/surfsense_web/components/tool-ui/google-calendar/create-event.tsx
index 40a9f0106..9427c989b 100644
--- a/surfsense_web/components/tool-ui/google-calendar/create-event.tsx
+++ b/surfsense_web/components/tool-ui/google-calendar/create-event.tsx
@@ -2,7 +2,7 @@
import type { ToolCallMessagePartProps } from "@assistant-ui/react";
import { useSetAtom } from "jotai";
-import { ClockIcon, CornerDownLeftIcon, GlobeIcon, MapPinIcon, Pen, UsersIcon } from "lucide-react";
+import { ClockIcon, CornerDownLeftIcon, GlobeIcon, MapPinIcon, Pencil, UsersIcon } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import type { ExtraField } from "@/atoms/chat/hitl-edit-panel.atom";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
@@ -332,7 +332,7 @@ function ApprovalCard({
});
}}
>
-
+
Edit
)}
diff --git a/surfsense_web/components/tool-ui/google-calendar/update-event.tsx b/surfsense_web/components/tool-ui/google-calendar/update-event.tsx
index cd6ec0618..649174245 100644
--- a/surfsense_web/components/tool-ui/google-calendar/update-event.tsx
+++ b/surfsense_web/components/tool-ui/google-calendar/update-event.tsx
@@ -7,7 +7,7 @@ import {
ClockIcon,
CornerDownLeftIcon,
MapPinIcon,
- Pen,
+ Pencil,
UsersIcon,
} from "lucide-react";
import { useCallback, useEffect, useState } from "react";
@@ -415,7 +415,7 @@ function ApprovalCard({
});
}}
>
-
+
Edit
)}
diff --git a/surfsense_web/components/tool-ui/google-drive/create-file.tsx b/surfsense_web/components/tool-ui/google-drive/create-file.tsx
index 638db3db9..b13089877 100644
--- a/surfsense_web/components/tool-ui/google-drive/create-file.tsx
+++ b/surfsense_web/components/tool-ui/google-drive/create-file.tsx
@@ -2,7 +2,7 @@
import type { ToolCallMessagePartProps } from "@assistant-ui/react";
import { useSetAtom } from "jotai";
-import { CornerDownLeftIcon, FileIcon, Pen } from "lucide-react";
+import { CornerDownLeftIcon, FileIcon, Pencil } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
import { PlateEditor } from "@/components/editor/plate-editor";
@@ -240,7 +240,7 @@ function ApprovalCard({
});
}}
>
-
+
Edit
)}
diff --git a/surfsense_web/components/tool-ui/jira/create-jira-issue.tsx b/surfsense_web/components/tool-ui/jira/create-jira-issue.tsx
index 91041d15e..6916f9fa0 100644
--- a/surfsense_web/components/tool-ui/jira/create-jira-issue.tsx
+++ b/surfsense_web/components/tool-ui/jira/create-jira-issue.tsx
@@ -2,7 +2,7 @@
import type { ToolCallMessagePartProps } from "@assistant-ui/react";
import { useSetAtom } from "jotai";
-import { CornerDownLeftIcon, Pen } from "lucide-react";
+import { CornerDownLeftIcon, Pencil } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
import { PlateEditor } from "@/components/editor/plate-editor";
@@ -257,7 +257,7 @@ function ApprovalCard({
});
}}
>
-
+
Edit
)}
diff --git a/surfsense_web/components/tool-ui/jira/update-jira-issue.tsx b/surfsense_web/components/tool-ui/jira/update-jira-issue.tsx
index f377563da..72e697532 100644
--- a/surfsense_web/components/tool-ui/jira/update-jira-issue.tsx
+++ b/surfsense_web/components/tool-ui/jira/update-jira-issue.tsx
@@ -2,7 +2,7 @@
import type { ToolCallMessagePartProps } from "@assistant-ui/react";
import { useSetAtom } from "jotai";
-import { CornerDownLeftIcon, Pen } from "lucide-react";
+import { CornerDownLeftIcon, Pencil } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
import { PlateEditor } from "@/components/editor/plate-editor";
@@ -273,7 +273,7 @@ function ApprovalCard({
});
}}
>
-
+
Edit
)}
diff --git a/surfsense_web/components/tool-ui/linear/create-linear-issue.tsx b/surfsense_web/components/tool-ui/linear/create-linear-issue.tsx
index 8abc7b50b..7d5098c3e 100644
--- a/surfsense_web/components/tool-ui/linear/create-linear-issue.tsx
+++ b/surfsense_web/components/tool-ui/linear/create-linear-issue.tsx
@@ -2,7 +2,7 @@
import type { ToolCallMessagePartProps } from "@assistant-ui/react";
import { useSetAtom } from "jotai";
-import { CornerDownLeftIcon, Pen } from "lucide-react";
+import { CornerDownLeftIcon, Pencil } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
import { PlateEditor } from "@/components/editor/plate-editor";
@@ -269,7 +269,7 @@ function ApprovalCard({
});
}}
>
-
+
Edit
)}
diff --git a/surfsense_web/components/tool-ui/linear/update-linear-issue.tsx b/surfsense_web/components/tool-ui/linear/update-linear-issue.tsx
index daadfbc63..2d6846cea 100644
--- a/surfsense_web/components/tool-ui/linear/update-linear-issue.tsx
+++ b/surfsense_web/components/tool-ui/linear/update-linear-issue.tsx
@@ -2,7 +2,7 @@
import type { ToolCallMessagePartProps } from "@assistant-ui/react";
import { useSetAtom } from "jotai";
-import { CornerDownLeftIcon, Pen } from "lucide-react";
+import { CornerDownLeftIcon, Pencil } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
import { PlateEditor } from "@/components/editor/plate-editor";
@@ -332,7 +332,7 @@ function ApprovalCard({
});
}}
>
-
+
Edit
)}
diff --git a/surfsense_web/components/tool-ui/notion/create-notion-page.tsx b/surfsense_web/components/tool-ui/notion/create-notion-page.tsx
index 8c93c7648..b16a1d8cd 100644
--- a/surfsense_web/components/tool-ui/notion/create-notion-page.tsx
+++ b/surfsense_web/components/tool-ui/notion/create-notion-page.tsx
@@ -2,7 +2,7 @@
import type { ToolCallMessagePartProps } from "@assistant-ui/react";
import { useSetAtom } from "jotai";
-import { CornerDownLeftIcon, Pen } from "lucide-react";
+import { CornerDownLeftIcon, Pencil } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
import { PlateEditor } from "@/components/editor/plate-editor";
@@ -219,7 +219,7 @@ function ApprovalCard({
});
}}
>
-
+
Edit
)}
diff --git a/surfsense_web/components/tool-ui/notion/update-notion-page.tsx b/surfsense_web/components/tool-ui/notion/update-notion-page.tsx
index cf714b1b4..ef75c5d92 100644
--- a/surfsense_web/components/tool-ui/notion/update-notion-page.tsx
+++ b/surfsense_web/components/tool-ui/notion/update-notion-page.tsx
@@ -2,7 +2,7 @@
import type { ToolCallMessagePartProps } from "@assistant-ui/react";
import { useSetAtom } from "jotai";
-import { CornerDownLeftIcon, Pen } from "lucide-react";
+import { CornerDownLeftIcon, Pencil } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
import { PlateEditor } from "@/components/editor/plate-editor";
@@ -196,7 +196,7 @@ function ApprovalCard({
});
}}
>
-
+
Edit
)}
diff --git a/surfsense_web/components/tool-ui/onedrive/create-file.tsx b/surfsense_web/components/tool-ui/onedrive/create-file.tsx
index 8a64a6cf8..7621f152f 100644
--- a/surfsense_web/components/tool-ui/onedrive/create-file.tsx
+++ b/surfsense_web/components/tool-ui/onedrive/create-file.tsx
@@ -2,7 +2,7 @@
import type { ToolCallMessagePartProps } from "@assistant-ui/react";
import { useSetAtom } from "jotai";
-import { CornerDownLeftIcon, FileIcon, Pen } from "lucide-react";
+import { CornerDownLeftIcon, FileIcon, Pencil } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
import { PlateEditor } from "@/components/editor/plate-editor";
@@ -209,7 +209,7 @@ function ApprovalCard({
});
}}
>
-
+
Edit
)}
diff --git a/surfsense_web/components/ui/mode-toolbar-button.tsx b/surfsense_web/components/ui/mode-toolbar-button.tsx
index 37231991f..394eaf97c 100644
--- a/surfsense_web/components/ui/mode-toolbar-button.tsx
+++ b/surfsense_web/components/ui/mode-toolbar-button.tsx
@@ -1,6 +1,6 @@
"use client";
-import { BookOpenIcon, PenLineIcon } from "lucide-react";
+import { BookOpenIcon, Pencil } from "lucide-react";
import { usePlateState } from "platejs/react";
import { ToolbarButton } from "./toolbar";
@@ -13,7 +13,7 @@ export function ModeToolbarButton() {
tooltip={readOnly ? "Click to edit" : "Click to view"}
onClick={() => setReadOnly(!readOnly)}
>
- {readOnly ?
:
}
+ {readOnly ?
:
}
);
}
From 2618205749ebcbe532561a97868e04164c57bdd8 Mon Sep 17 00:00:00 2001
From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com>
Date: Fri, 24 Apr 2026 03:52:39 +0530
Subject: [PATCH 29/35] refactor(thread): remove unused filesystem settings and
related logic from Composer component
---
.../components/assistant-ui/thread.tsx | 361 ------------------
1 file changed, 361 deletions(-)
diff --git a/surfsense_web/components/assistant-ui/thread.tsx b/surfsense_web/components/assistant-ui/thread.tsx
index 6fde33061..2ec422fbf 100644
--- a/surfsense_web/components/assistant-ui/thread.tsx
+++ b/surfsense_web/components/assistant-ui/thread.tsx
@@ -12,15 +12,11 @@ import {
AlertCircle,
ArrowDownIcon,
ArrowUpIcon,
- Check,
ChevronDown,
ChevronUp,
Clipboard,
Dot,
- Folder,
- FolderPlus,
Globe,
- Laptop,
Plus,
Settings2,
SquareIcon,
@@ -70,16 +66,6 @@ import {
} from "@/components/new-chat/document-mention-picker";
import { PromptPicker, type PromptPickerRef } from "@/components/new-chat/prompt-picker";
import { Avatar, AvatarFallback, AvatarGroup } from "@/components/ui/avatar";
-import {
- AlertDialog,
- AlertDialogAction,
- AlertDialogCancel,
- AlertDialogContent,
- AlertDialogDescription,
- AlertDialogFooter,
- AlertDialogHeader,
- AlertDialogTitle,
-} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { Drawer, DrawerContent, DrawerHandle, DrawerTitle } from "@/components/ui/drawer";
import {
@@ -108,18 +94,6 @@ import { cn } from "@/lib/utils";
const COMPOSER_PLACEHOLDER = "Ask anything, type / for prompts, type @ to mention docs";
-type ComposerFilesystemSettings = {
- mode: "cloud" | "desktop_local_folder";
- localRootPaths: string[];
- updatedAt: string;
-};
-
-const LOCAL_FILESYSTEM_TRUST_KEY = "surfsense.local-filesystem-trust.v1";
-const MAX_LOCAL_FILESYSTEM_ROOTS = 5;
-
-const getFolderDisplayName = (rootPath: string): string =>
- rootPath.split(/[\\/]/).at(-1) || rootPath;
-
export const Thread: FC = () => {
return
;
};
@@ -388,12 +362,6 @@ const Composer: FC = () => {
}, []);
const electronAPI = useElectronAPI();
- const [filesystemSettings, setFilesystemSettings] = useState
(
- null
- );
- const [localTrustDialogOpen, setLocalTrustDialogOpen] = useState(false);
- const [localFoldersOpen, setLocalFoldersOpen] = useState(false);
- const [pendingLocalPath, setPendingLocalPath] = useState(null);
const [clipboardInitialText, setClipboardInitialText] = useState();
const clipboardLoadedRef = useRef(false);
useEffect(() => {
@@ -406,116 +374,6 @@ const Composer: FC = () => {
});
}, [electronAPI]);
- useEffect(() => {
- if (!electronAPI?.getAgentFilesystemSettings) return;
- let mounted = true;
- electronAPI
- .getAgentFilesystemSettings()
- .then((settings: ComposerFilesystemSettings) => {
- if (!mounted) return;
- setFilesystemSettings(settings);
- })
- .catch(() => {
- if (!mounted) return;
- setFilesystemSettings({
- mode: "cloud",
- localRootPaths: [],
- updatedAt: new Date().toISOString(),
- });
- });
- return () => {
- mounted = false;
- };
- }, [electronAPI]);
-
- const hasLocalFilesystemTrust = useCallback(() => {
- try {
- return window.localStorage.getItem(LOCAL_FILESYSTEM_TRUST_KEY) === "true";
- } catch {
- return false;
- }
- }, []);
-
- const localRootPaths = filesystemSettings?.localRootPaths ?? [];
- const primaryLocalRootPath = localRootPaths[0] ?? null;
- const extraLocalRootCount = Math.max(0, localRootPaths.length - 1);
- const canAddMoreLocalRoots = localRootPaths.length < MAX_LOCAL_FILESYSTEM_ROOTS;
-
- const applyLocalRootPath = useCallback(
- async (path: string) => {
- if (!electronAPI?.setAgentFilesystemSettings) return;
- const nextLocalRootPaths = [...localRootPaths, path]
- .filter((rootPath, index, allPaths) => allPaths.indexOf(rootPath) === index)
- .slice(0, MAX_LOCAL_FILESYSTEM_ROOTS);
- if (nextLocalRootPaths.length === localRootPaths.length) {
- return;
- }
- const updated = await electronAPI.setAgentFilesystemSettings({
- mode: "desktop_local_folder",
- localRootPaths: nextLocalRootPaths,
- });
- setFilesystemSettings(updated);
- },
- [electronAPI, localRootPaths]
- );
-
- const runSwitchToLocalMode = useCallback(async () => {
- if (!electronAPI?.setAgentFilesystemSettings) return;
- const updated = await electronAPI.setAgentFilesystemSettings({ mode: "desktop_local_folder" });
- setFilesystemSettings(updated);
- }, [electronAPI]);
-
- const runPickLocalRoot = useCallback(async () => {
- if (!electronAPI?.pickAgentFilesystemRoot) return;
- const picked = await electronAPI.pickAgentFilesystemRoot();
- if (!picked) return;
- await applyLocalRootPath(picked);
- }, [applyLocalRootPath, electronAPI]);
-
- const handleFilesystemModeChange = useCallback(
- async (mode: "cloud" | "desktop_local_folder") => {
- if (!electronAPI?.setAgentFilesystemSettings) return;
- if (mode === "desktop_local_folder") return void runSwitchToLocalMode();
- const updated = await electronAPI.setAgentFilesystemSettings({ mode });
- setFilesystemSettings(updated);
- },
- [electronAPI, runSwitchToLocalMode]
- );
-
- const handlePickFilesystemRoot = useCallback(async () => {
- if (!canAddMoreLocalRoots) return;
- if (hasLocalFilesystemTrust()) {
- await runPickLocalRoot();
- return;
- }
- if (!electronAPI?.pickAgentFilesystemRoot) return;
- const picked = await electronAPI.pickAgentFilesystemRoot();
- if (!picked) return;
- setPendingLocalPath(picked);
- setLocalTrustDialogOpen(true);
- }, [canAddMoreLocalRoots, electronAPI, hasLocalFilesystemTrust, runPickLocalRoot]);
-
- const handleRemoveFilesystemRoot = useCallback(
- async (rootPathToRemove: string) => {
- if (!electronAPI?.setAgentFilesystemSettings) return;
- const updated = await electronAPI.setAgentFilesystemSettings({
- mode: "desktop_local_folder",
- localRootPaths: localRootPaths.filter((rootPath) => rootPath !== rootPathToRemove),
- });
- setFilesystemSettings(updated);
- },
- [electronAPI, localRootPaths]
- );
-
- const handleClearFilesystemRoots = useCallback(async () => {
- if (!electronAPI?.setAgentFilesystemSettings) return;
- const updated = await electronAPI.setAgentFilesystemSettings({
- mode: "desktop_local_folder",
- localRootPaths: [],
- });
- setFilesystemSettings(updated);
- }, [electronAPI]);
-
const isThreadEmpty = useAuiState(({ thread }) => thread.isEmpty);
const isThreadRunning = useAuiState(({ thread }) => thread.isRunning);
@@ -810,225 +668,6 @@ const Composer: FC = () => {
currentUserId={currentUser?.id ?? null}
members={members ?? []}
/>
- {electronAPI && filesystemSettings ? (
-
-
-
-
-
-
- handleFilesystemModeChange("cloud")}
- className="flex items-center justify-between"
- >
-
-
- Cloud
-
- {filesystemSettings.mode === "cloud" && }
-
- handleFilesystemModeChange("desktop_local_folder")}
- className="flex items-center justify-between"
- >
-
-
- Local
-
- {filesystemSettings.mode === "desktop_local_folder" && (
-
- )}
-
-
-
-
- {filesystemSettings.mode === "desktop_local_folder" && (
- <>
-
-
- {primaryLocalRootPath ? (
- <>
-
-
-
- {getFolderDisplayName(primaryLocalRootPath)}
-
-
-
- {extraLocalRootCount > 0 && (
-
-
-
-
-
-
- {localRootPaths.map((rootPath) => (
-
-
-
- {getFolderDisplayName(rootPath)}
-
-
-
- ))}
-
-
-
-
-
-
- )}
-
- >
- ) : (
-
- )}
-
- >
- )}
-
- ) : null}
- {
- setLocalTrustDialogOpen(open);
- if (!open) {
- setPendingLocalPath(null);
- }
- }}
- >
-
-
- Trust this workspace?
-
- Local mode can read and edit files inside the folders you select. Continue only if
- you trust this workspace and its contents.
-
- {(pendingLocalPath || primaryLocalRootPath) && (
-
- Folder path: {pendingLocalPath || primaryLocalRootPath}
-
- )}
-
-
- Cancel
- {
- try {
- window.localStorage.setItem(LOCAL_FILESYSTEM_TRUST_KEY, "true");
- } catch {}
- setLocalTrustDialogOpen(false);
- const path = pendingLocalPath;
- setPendingLocalPath(null);
- if (path) {
- await applyLocalRootPath(path);
- } else {
- await runPickLocalRoot();
- }
- }}
- >
- I trust this workspace
-
-
-
-
{showDocumentPopover && (
Date: Fri, 24 Apr 2026 03:55:24 +0530
Subject: [PATCH 30/35] feat(sidebar): implement local filesystem browser and
enhance document sidebar with local folder management features
---
.../layout/ui/sidebar/DocumentsSidebar.tsx | 466 +++++++++++++++---
.../ui/sidebar/LocalFilesystemBrowser.tsx | 271 ++++++++++
2 files changed, 675 insertions(+), 62 deletions(-)
create mode 100644 surfsense_web/components/layout/ui/sidebar/LocalFilesystemBrowser.tsx
diff --git a/surfsense_web/components/layout/ui/sidebar/DocumentsSidebar.tsx b/surfsense_web/components/layout/ui/sidebar/DocumentsSidebar.tsx
index daed8747d..5c955a53e 100644
--- a/surfsense_web/components/layout/ui/sidebar/DocumentsSidebar.tsx
+++ b/surfsense_web/components/layout/ui/sidebar/DocumentsSidebar.tsx
@@ -6,9 +6,12 @@ import {
ChevronLeft,
ChevronRight,
FileText,
+ Folder,
FolderClock,
+ Globe,
Lock,
Paperclip,
+ Search,
Trash2,
Unplug,
Upload,
@@ -59,7 +62,9 @@ import {
import { Avatar, AvatarFallback, AvatarGroup } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import { Drawer, DrawerContent, DrawerHandle, DrawerTitle } from "@/components/ui/drawer";
+import { Input } from "@/components/ui/input";
import { Spinner } from "@/components/ui/spinner";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { useAnonymousMode, useIsAnonymous } from "@/contexts/anonymous-mode";
import { useLoginGate } from "@/contexts/login-gate";
@@ -76,9 +81,31 @@ import { BACKEND_URL } from "@/lib/env-config";
import { uploadFolderScan } from "@/lib/folder-sync-upload";
import { getSupportedExtensionsSet } from "@/lib/supported-extensions";
import { queries } from "@/zero/queries/index";
+import { LocalFilesystemBrowser } from "./LocalFilesystemBrowser";
import { SidebarSlideOutPanel } from "./SidebarSlideOutPanel";
const NON_DELETABLE_DOCUMENT_TYPES: readonly string[] = ["SURFSENSE_DOCS"];
+const LOCAL_FILESYSTEM_TRUST_KEY = "surfsense.local-filesystem-trust.v1";
+const MAX_LOCAL_FILESYSTEM_ROOTS = 5;
+
+type FilesystemSettings = {
+ mode: "cloud" | "desktop_local_folder";
+ localRootPaths: string[];
+ updatedAt: string;
+};
+
+interface WatchedFolderEntry {
+ path: string;
+ name: string;
+ excludePatterns: string[];
+ fileExtensions: string[] | null;
+ rootFolderId: number | null;
+ searchSpaceId: number;
+ active: boolean;
+}
+
+const getFolderDisplayName = (rootPath: string): string =>
+ rootPath.split(/[\\/]/).at(-1) || rootPath;
const SHOWCASE_CONNECTORS = [
{ type: "GOOGLE_DRIVE_CONNECTOR", label: "Google Drive" },
@@ -133,12 +160,119 @@ function AuthenticatedDocumentsSidebar({
const [search, setSearch] = useState("");
const debouncedSearch = useDebouncedValue(search, 250);
+ const [localSearch, setLocalSearch] = useState("");
+ const debouncedLocalSearch = useDebouncedValue(localSearch, 250);
+ const localSearchInputRef = useRef(null);
const [activeTypes, setActiveTypes] = useState([]);
+ const [filesystemSettings, setFilesystemSettings] = useState(null);
+ const [localTrustDialogOpen, setLocalTrustDialogOpen] = useState(false);
+ const [pendingLocalPath, setPendingLocalPath] = useState(null);
const [watchedFolderIds, setWatchedFolderIds] = useState>(new Set());
const [folderWatchOpen, setFolderWatchOpen] = useAtom(folderWatchDialogOpenAtom);
const [watchInitialFolder, setWatchInitialFolder] = useAtom(folderWatchInitialFolderAtom);
const isElectron = typeof window !== "undefined" && !!window.electronAPI;
+ useEffect(() => {
+ if (!electronAPI?.getAgentFilesystemSettings) return;
+ let mounted = true;
+ electronAPI
+ .getAgentFilesystemSettings()
+ .then((settings: FilesystemSettings) => {
+ if (!mounted) return;
+ setFilesystemSettings(settings);
+ })
+ .catch(() => {
+ if (!mounted) return;
+ setFilesystemSettings({
+ mode: "cloud",
+ localRootPaths: [],
+ updatedAt: new Date().toISOString(),
+ });
+ });
+ return () => {
+ mounted = false;
+ };
+ }, [electronAPI]);
+
+ const hasLocalFilesystemTrust = useCallback(() => {
+ try {
+ return window.localStorage.getItem(LOCAL_FILESYSTEM_TRUST_KEY) === "true";
+ } catch {
+ return false;
+ }
+ }, []);
+
+ const localRootPaths = filesystemSettings?.localRootPaths ?? [];
+ const canAddMoreLocalRoots = localRootPaths.length < MAX_LOCAL_FILESYSTEM_ROOTS;
+
+ const applyLocalRootPath = useCallback(
+ async (path: string) => {
+ if (!electronAPI?.setAgentFilesystemSettings) return;
+ const nextLocalRootPaths = [...localRootPaths, path]
+ .filter((rootPath, index, allPaths) => allPaths.indexOf(rootPath) === index)
+ .slice(0, MAX_LOCAL_FILESYSTEM_ROOTS);
+ if (nextLocalRootPaths.length === localRootPaths.length) return;
+ const updated = await electronAPI.setAgentFilesystemSettings({
+ mode: "desktop_local_folder",
+ localRootPaths: nextLocalRootPaths,
+ });
+ setFilesystemSettings(updated);
+ },
+ [electronAPI, localRootPaths]
+ );
+
+ const runPickLocalRoot = useCallback(async () => {
+ if (!electronAPI?.pickAgentFilesystemRoot) return;
+ const picked = await electronAPI.pickAgentFilesystemRoot();
+ if (!picked) return;
+ await applyLocalRootPath(picked);
+ }, [applyLocalRootPath, electronAPI]);
+
+ const handlePickFilesystemRoot = useCallback(async () => {
+ if (!canAddMoreLocalRoots) return;
+ if (hasLocalFilesystemTrust()) {
+ await runPickLocalRoot();
+ return;
+ }
+ if (!electronAPI?.pickAgentFilesystemRoot) return;
+ const picked = await electronAPI.pickAgentFilesystemRoot();
+ if (!picked) return;
+ setPendingLocalPath(picked);
+ setLocalTrustDialogOpen(true);
+ }, [canAddMoreLocalRoots, electronAPI, hasLocalFilesystemTrust, runPickLocalRoot]);
+
+ const handleRemoveFilesystemRoot = useCallback(
+ async (rootPathToRemove: string) => {
+ if (!electronAPI?.setAgentFilesystemSettings) return;
+ const updated = await electronAPI.setAgentFilesystemSettings({
+ mode: "desktop_local_folder",
+ localRootPaths: localRootPaths.filter((rootPath) => rootPath !== rootPathToRemove),
+ });
+ setFilesystemSettings(updated);
+ },
+ [electronAPI, localRootPaths]
+ );
+
+ const handleClearFilesystemRoots = useCallback(async () => {
+ if (!electronAPI?.setAgentFilesystemSettings) return;
+ const updated = await electronAPI.setAgentFilesystemSettings({
+ mode: "desktop_local_folder",
+ localRootPaths: [],
+ });
+ setFilesystemSettings(updated);
+ }, [electronAPI]);
+
+ const handleFilesystemTabChange = useCallback(
+ async (tab: "cloud" | "local") => {
+ if (!electronAPI?.setAgentFilesystemSettings) return;
+ const updated = await electronAPI.setAgentFilesystemSettings({
+ mode: tab === "cloud" ? "cloud" : "desktop_local_folder",
+ });
+ setFilesystemSettings(updated);
+ },
+ [electronAPI]
+ );
+
// AI File Sort state
const { data: searchSpaces, refetch: refetchSearchSpaces } = useAtomValue(searchSpacesAtom);
const activeSearchSpace = useMemo(
@@ -196,7 +330,7 @@ function AuthenticatedDocumentsSidebar({
if (!electronAPI?.getWatchedFolders) return;
const api = electronAPI;
- const folders = await api.getWatchedFolders();
+ const folders = (await api.getWatchedFolders()) as WatchedFolderEntry[];
if (folders.length === 0) {
try {
@@ -214,9 +348,11 @@ function AuthenticatedDocumentsSidebar({
active: true,
});
}
- const recovered = await api.getWatchedFolders();
+ const recovered = (await api.getWatchedFolders()) as WatchedFolderEntry[];
const ids = new Set(
- recovered.filter((f) => f.rootFolderId != null).map((f) => f.rootFolderId as number)
+ recovered
+ .filter((f: WatchedFolderEntry) => f.rootFolderId != null)
+ .map((f: WatchedFolderEntry) => f.rootFolderId as number)
);
setWatchedFolderIds(ids);
return;
@@ -226,7 +362,9 @@ function AuthenticatedDocumentsSidebar({
}
const ids = new Set(
- folders.filter((f) => f.rootFolderId != null).map((f) => f.rootFolderId as number)
+ folders
+ .filter((f: WatchedFolderEntry) => f.rootFolderId != null)
+ .map((f: WatchedFolderEntry) => f.rootFolderId as number)
);
setWatchedFolderIds(ids);
}, [searchSpaceId, electronAPI]);
@@ -375,8 +513,8 @@ function AuthenticatedDocumentsSidebar({
async (folder: FolderDisplay) => {
if (!electronAPI) return;
- const watchedFolders = await electronAPI.getWatchedFolders();
- const matched = watchedFolders.find((wf) => wf.rootFolderId === folder.id);
+ const watchedFolders = (await electronAPI.getWatchedFolders()) as WatchedFolderEntry[];
+ const matched = watchedFolders.find((wf: WatchedFolderEntry) => wf.rootFolderId === folder.id);
if (!matched) {
toast.error("This folder is not being watched");
return;
@@ -405,8 +543,8 @@ function AuthenticatedDocumentsSidebar({
async (folder: FolderDisplay) => {
if (!electronAPI) return;
- const watchedFolders = await electronAPI.getWatchedFolders();
- const matched = watchedFolders.find((wf) => wf.rootFolderId === folder.id);
+ const watchedFolders = (await electronAPI.getWatchedFolders()) as WatchedFolderEntry[];
+ const matched = watchedFolders.find((wf: WatchedFolderEntry) => wf.rootFolderId === folder.id);
if (!matched) {
toast.error("This folder is not being watched");
return;
@@ -438,8 +576,10 @@ function AuthenticatedDocumentsSidebar({
if (!confirm(`Delete folder "${folder.name}" and all its contents?`)) return;
try {
if (electronAPI) {
- const watchedFolders = await electronAPI.getWatchedFolders();
- const matched = watchedFolders.find((wf) => wf.rootFolderId === folder.id);
+ const watchedFolders = (await electronAPI.getWatchedFolders()) as WatchedFolderEntry[];
+ const matched = watchedFolders.find(
+ (wf: WatchedFolderEntry) => wf.rootFolderId === folder.id
+ );
if (matched) {
await electronAPI.removeWatchedFolder(matched.path);
}
@@ -836,59 +976,11 @@ function AuthenticatedDocumentsSidebar({
return () => document.removeEventListener("keydown", handleEscape);
}, [open, onOpenChange, isMobile, setRightPanelCollapsed]);
- const documentsContent = (
- <>
-
-
-
- {isMobile && (
-
- )}
-
{t("title") || "Documents"}
-
-
- {!isMobile && onDockedChange && (
-
-
-
-
-
- {isDocked ? "Collapse panel" : "Expand panel"}
-
-
- )}
- {headerAction}
-
-
-
+ const showFilesystemTabs = !isMobile && !!electronAPI && !!filesystemSettings;
+ const currentFilesystemTab = filesystemSettings?.mode === "desktop_local_folder" ? "local" : "cloud";
+ const cloudContent = (
+ <>
{/* Connected tools strip */}
+ >
+ );
+
+ const localContent = (
+
+
+ {localRootPaths.length > 0 ? (
+ <>
+ {localRootPaths.map((rootPath) => (
+
+
+ {getFolderDisplayName(rootPath)}
+
+
+ ))}
+
+
+ >
+ ) : (
+
+ )}
+
+
+
+
+
+
+
setLocalSearch(e.target.value)}
+ placeholder="Search local files"
+ type="text"
+ aria-label="Search local files"
+ />
+ {Boolean(localSearch) && (
+
+ )}
+
+
+
{
+ openEditorPanel({
+ kind: "local_file",
+ localFilePath,
+ title: localFilePath.split("/").pop() || localFilePath,
+ searchSpaceId,
+ });
+ }}
+ />
+
+ );
+
+ const documentsContent = (
+ <>
+
+
+
+ {isMobile && (
+
+ )}
+
{t("title") || "Documents"}
+ {showFilesystemTabs && (
+ {
+ void handleFilesystemTabChange(value === "local" ? "local" : "cloud");
+ }}
+ >
+
+
+
+ Cloud
+
+
+
+ Local
+
+
+
+ )}
+
+
+ {!isMobile && onDockedChange && (
+
+
+
+
+
+ {isDocked ? "Collapse panel" : "Expand panel"}
+
+
+ )}
+ {headerAction}
+
+
+
+ {showFilesystemTabs ? (
+ {
+ void handleFilesystemTabChange(value === "local" ? "local" : "cloud");
+ }}
+ className="flex min-h-0 flex-1 flex-col"
+ >
+
+ {cloudContent}
+
+
+ {localContent}
+
+
+ ) : (
+ cloudContent
+ )}
{versionDocId !== null && (
)}
+ {
+ setLocalTrustDialogOpen(nextOpen);
+ if (!nextOpen) setPendingLocalPath(null);
+ }}
+ >
+
+
+ Trust this workspace?
+
+ Local mode can read and edit files inside the folders you select. Continue only if
+ you trust this workspace and its contents.
+
+ {pendingLocalPath && (
+
+ Folder path: {pendingLocalPath}
+
+ )}
+
+
+ Cancel
+ {
+ try {
+ window.localStorage.setItem(LOCAL_FILESYSTEM_TRUST_KEY, "true");
+ } catch {}
+ setLocalTrustDialogOpen(false);
+ const path = pendingLocalPath;
+ setPendingLocalPath(null);
+ if (path) {
+ await applyLocalRootPath(path);
+ } else {
+ await runPickLocalRoot();
+ }
+ }}
+ >
+ I trust this workspace
+
+
+
+
void;
+}
+
+interface LocalFolderFileEntry {
+ relativePath: string;
+ fullPath: string;
+ size: number;
+ mtimeMs: number;
+}
+
+type RootLoadState = {
+ loading: boolean;
+ error: string | null;
+ files: LocalFolderFileEntry[];
+};
+
+interface LocalFolderNode {
+ key: string;
+ name: string;
+ folders: Map;
+ files: LocalFolderFileEntry[];
+}
+
+const getFolderDisplayName = (rootPath: string): string =>
+ rootPath.split(/[\\/]/).at(-1) || rootPath;
+
+function createFolderNode(key: string, name: string): LocalFolderNode {
+ return {
+ key,
+ name,
+ folders: new Map(),
+ files: [],
+ };
+}
+
+function getFileName(pathValue: string): string {
+ return pathValue.split(/[\\/]/).at(-1) || pathValue;
+}
+
+export function LocalFilesystemBrowser({
+ rootPaths,
+ searchSpaceId,
+ searchQuery,
+ onOpenFile,
+}: LocalFilesystemBrowserProps) {
+ const electronAPI = useElectronAPI();
+ const [rootStateMap, setRootStateMap] = useState>({});
+ const [expandedFolderKeys, setExpandedFolderKeys] = useState>(new Set());
+ const supportedExtensions = useMemo(() => Array.from(getSupportedExtensionsSet()), []);
+
+ useEffect(() => {
+ setExpandedFolderKeys((prev) => {
+ const next = new Set(prev);
+ for (const rootPath of rootPaths) {
+ next.add(rootPath);
+ }
+ return next;
+ });
+ }, [rootPaths]);
+
+ useEffect(() => {
+ if (!electronAPI?.listFolderFiles) return;
+ let cancelled = false;
+
+ for (const rootPath of rootPaths) {
+ setRootStateMap((prev) => ({
+ ...prev,
+ [rootPath]: {
+ loading: true,
+ error: null,
+ files: prev[rootPath]?.files ?? [],
+ },
+ }));
+ }
+
+ void Promise.all(
+ rootPaths.map(async (rootPath) => {
+ try {
+ const files = (await electronAPI.listFolderFiles({
+ path: rootPath,
+ name: getFolderDisplayName(rootPath),
+ excludePatterns: DEFAULT_EXCLUDE_PATTERNS,
+ fileExtensions: supportedExtensions,
+ rootFolderId: null,
+ searchSpaceId,
+ active: true,
+ })) as LocalFolderFileEntry[];
+ if (cancelled) return;
+ setRootStateMap((prev) => ({
+ ...prev,
+ [rootPath]: {
+ loading: false,
+ error: null,
+ files,
+ },
+ }));
+ } catch (error) {
+ if (cancelled) return;
+ setRootStateMap((prev) => ({
+ ...prev,
+ [rootPath]: {
+ loading: false,
+ error: error instanceof Error ? error.message : "Failed to read folder",
+ files: [],
+ },
+ }));
+ }
+ })
+ );
+
+ return () => {
+ cancelled = true;
+ };
+ }, [electronAPI, rootPaths, searchSpaceId, supportedExtensions]);
+
+ const treeByRoot = useMemo(() => {
+ const query = searchQuery?.trim().toLowerCase() ?? "";
+ const hasQuery = query.length > 0;
+
+ return rootPaths.map((rootPath) => {
+ const rootNode = createFolderNode(rootPath, getFolderDisplayName(rootPath));
+ const allFiles = rootStateMap[rootPath]?.files ?? [];
+ const files = hasQuery
+ ? allFiles.filter((file) => {
+ const relativePath = file.relativePath.toLowerCase();
+ const fileName = getFileName(file.relativePath).toLowerCase();
+ return relativePath.includes(query) || fileName.includes(query);
+ })
+ : allFiles;
+ for (const file of files) {
+ const parts = file.relativePath.split(/[\\/]/).filter(Boolean);
+ let cursor = rootNode;
+ for (let i = 0; i < parts.length - 1; i++) {
+ const part = parts[i];
+ const folderKey = `${cursor.key}/${part}`;
+ if (!cursor.folders.has(part)) {
+ cursor.folders.set(part, createFolderNode(folderKey, part));
+ }
+ cursor = cursor.folders.get(part) as LocalFolderNode;
+ }
+ cursor.files.push(file);
+ }
+ return { rootPath, rootNode, matchCount: files.length, totalCount: allFiles.length };
+ });
+ }, [rootPaths, rootStateMap, searchQuery]);
+
+ const toggleFolder = useCallback((folderKey: string) => {
+ setExpandedFolderKeys((prev) => {
+ const next = new Set(prev);
+ if (next.has(folderKey)) {
+ next.delete(folderKey);
+ } else {
+ next.add(folderKey);
+ }
+ return next;
+ });
+ }, []);
+
+ const renderFolder = useCallback(
+ (folder: LocalFolderNode, depth: number) => {
+ const isExpanded = expandedFolderKeys.has(folder.key);
+ const childFolders = Array.from(folder.folders.values()).sort((a, b) =>
+ a.name.localeCompare(b.name)
+ );
+ const files = [...folder.files].sort((a, b) => a.relativePath.localeCompare(b.relativePath));
+ return (
+
+
+ {isExpanded && (
+ <>
+ {childFolders.map((childFolder) => renderFolder(childFolder, depth + 1))}
+ {files.map((file) => (
+
+ ))}
+ >
+ )}
+
+ );
+ },
+ [expandedFolderKeys, onOpenFile, toggleFolder]
+ );
+
+ if (rootPaths.length === 0) {
+ return (
+
+
No local folder selected
+
+ Add a local folder above to browse files in desktop mode.
+
+
+ );
+ }
+
+ return (
+
+ {treeByRoot.map(({ rootPath, rootNode, matchCount, totalCount }) => {
+ const state = rootStateMap[rootPath];
+ if (!state || state.loading) {
+ return (
+
+
+ Loading {getFolderDisplayName(rootPath)}...
+
+ );
+ }
+ if (state.error) {
+ return (
+
+
Failed to load local folder
+
{state.error}
+
+ );
+ }
+ const isEmpty = totalCount === 0;
+ return (
+
+ {renderFolder(rootNode, 0)}
+ {isEmpty && (
+
+ No supported files found in this folder.
+
+ )}
+ {!isEmpty && matchCount === 0 && searchQuery && (
+
+ No matching files in this folder.
+
+ )}
+
+ );
+ })}
+
+ );
+}
From d1c14160e3ac2b4025357fb14571b344e27025fd Mon Sep 17 00:00:00 2001
From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com>
Date: Fri, 24 Apr 2026 04:42:24 +0530
Subject: [PATCH 31/35] feat(sidebar): enhance DocumentsSidebar with dropdown
menu for local folder management and improve UI interactions
---
.../layout/ui/sidebar/DocumentsSidebar.tsx | 150 +++++++++++-------
.../ui/sidebar/LocalFilesystemBrowser.tsx | 10 --
2 files changed, 89 insertions(+), 71 deletions(-)
diff --git a/surfsense_web/components/layout/ui/sidebar/DocumentsSidebar.tsx b/surfsense_web/components/layout/ui/sidebar/DocumentsSidebar.tsx
index 5c955a53e..dbe2f16e4 100644
--- a/surfsense_web/components/layout/ui/sidebar/DocumentsSidebar.tsx
+++ b/surfsense_web/components/layout/ui/sidebar/DocumentsSidebar.tsx
@@ -7,11 +7,13 @@ import {
ChevronRight,
FileText,
Folder,
+ FolderPlus,
FolderClock,
- Globe,
+ Laptop,
Lock,
Paperclip,
Search,
+ Server,
Trash2,
Unplug,
Upload,
@@ -61,8 +63,17 @@ import {
} from "@/components/ui/alert-dialog";
import { Avatar, AvatarFallback, AvatarGroup } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
import { Drawer, DrawerContent, DrawerHandle, DrawerTitle } from "@/components/ui/drawer";
import { Input } from "@/components/ui/input";
+import { Separator } from "@/components/ui/separator";
import { Spinner } from "@/components/ui/spinner";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
@@ -1135,76 +1146,93 @@ function AuthenticatedDocumentsSidebar({
);
const localContent = (
-
-
- {localRootPaths.length > 0 ? (
- <>
- {localRootPaths.map((rootPath) => (
-
-
-
{getFolderDisplayName(rootPath)}
+
+
+
+ {localRootPaths.length > 0 ? (
+
+
-
- ))}
-
+
-
+
setLocalSearch(e.target.value)}
placeholder="Search local files"
@@ -1214,14 +1242,14 @@ function AuthenticatedDocumentsSidebar({
{Boolean(localSearch) && (
{
setLocalSearch("");
localSearchInputRef.current?.focus();
}}
>
-
+
)}
@@ -1266,21 +1294,21 @@ function AuthenticatedDocumentsSidebar({
void handleFilesystemTabChange(value === "local" ? "local" : "cloud");
}}
>
-
+
-
+
Cloud
-
+
Local
diff --git a/surfsense_web/components/layout/ui/sidebar/LocalFilesystemBrowser.tsx b/surfsense_web/components/layout/ui/sidebar/LocalFilesystemBrowser.tsx
index 544280116..7aebf4695 100644
--- a/surfsense_web/components/layout/ui/sidebar/LocalFilesystemBrowser.tsx
+++ b/surfsense_web/components/layout/ui/sidebar/LocalFilesystemBrowser.tsx
@@ -61,16 +61,6 @@ export function LocalFilesystemBrowser({
const [expandedFolderKeys, setExpandedFolderKeys] = useState>(new Set());
const supportedExtensions = useMemo(() => Array.from(getSupportedExtensionsSet()), []);
- useEffect(() => {
- setExpandedFolderKeys((prev) => {
- const next = new Set(prev);
- for (const rootPath of rootPaths) {
- next.add(rootPath);
- }
- return next;
- });
- }, [rootPaths]);
-
useEffect(() => {
if (!electronAPI?.listFolderFiles) return;
let cancelled = false;
From ce71897286c4f4772928f9155f033715c2690732 Mon Sep 17 00:00:00 2001
From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com>
Date: Fri, 24 Apr 2026 04:54:48 +0530
Subject: [PATCH 32/35] refactor(hotkeys): simplify hotkey display logic and
replace icon representation with text in DesktopShortcutsContent and login
page
---
.../components/DesktopShortcutsContent.tsx | 45 ++++++-------------
surfsense_web/app/desktop/login/page.tsx | 45 ++++++-------------
2 files changed, 26 insertions(+), 64 deletions(-)
diff --git a/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/DesktopShortcutsContent.tsx b/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/DesktopShortcutsContent.tsx
index f4981b8f0..6207457c4 100644
--- a/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/DesktopShortcutsContent.tsx
+++ b/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/DesktopShortcutsContent.tsx
@@ -1,10 +1,11 @@
"use client";
-import { ArrowBigUp, BrainCog, Command, Option, Rocket, RotateCcw, Zap } from "lucide-react";
+import { BrainCog, Rocket, RotateCcw, Zap } from "lucide-react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { toast } from "sonner";
import { DEFAULT_SHORTCUTS, keyEventToAccelerator } from "@/components/desktop/shortcut-recorder";
import { Button } from "@/components/ui/button";
+import { ShortcutKbd } from "@/components/ui/shortcut-kbd";
import { Spinner } from "@/components/ui/spinner";
import { useElectronAPI } from "@/hooks/use-platform";
@@ -17,24 +18,20 @@ const HOTKEY_ROWS: Array<{ key: ShortcutKey; label: string; icon: React.ElementT
{ key: "autocomplete", label: "Extreme Assist", icon: BrainCog },
];
-type ShortcutToken =
- | { kind: "text"; value: string }
- | { kind: "icon"; value: "command" | "option" | "shift" };
-
-function acceleratorToTokens(accel: string, isMac: boolean): ShortcutToken[] {
+function acceleratorToKeys(accel: string, isMac: boolean): string[] {
if (!accel) return [];
return accel.split("+").map((part) => {
if (part === "CommandOrControl") {
- return isMac ? { kind: "icon", value: "command" as const } : { kind: "text", value: "Ctrl" };
+ return isMac ? "⌘" : "Ctrl";
}
if (part === "Alt") {
- return isMac ? { kind: "icon", value: "option" as const } : { kind: "text", value: "Alt" };
+ return isMac ? "⌥" : "Alt";
}
if (part === "Shift") {
- return isMac ? { kind: "icon", value: "shift" as const } : { kind: "text", value: "Shift" };
+ return isMac ? "⇧" : "Shift";
}
- if (part === "Space") return { kind: "text", value: "Space" };
- return { kind: "text", value: part.length === 1 ? part.toUpperCase() : part };
+ if (part === "Space") return "Space";
+ return part.length === 1 ? part.toUpperCase() : part;
});
}
@@ -58,7 +55,7 @@ function HotkeyRow({
const [recording, setRecording] = useState(false);
const inputRef = useRef(null);
const isDefault = value === defaultValue;
- const displayTokens = useMemo(() => acceleratorToTokens(value, isMac), [value, isMac]);
+ const displayKeys = useMemo(() => acceleratorToKeys(value, isMac), [value, isMac]);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
@@ -103,13 +100,14 @@ function HotkeyRow({
setRecording(true)}
onKeyDown={handleKeyDown}
onBlur={() => setRecording(false)}
className={
recording
- ? "flex h-7 items-center rounded-md border border-primary bg-primary/5 ring-2 ring-primary/20"
- : "flex h-7 items-center rounded-md border border-input bg-muted/50 hover:bg-muted"
+ ? "flex h-7 items-center rounded-md border border-transparent bg-primary/5 outline-none ring-0 focus:outline-none focus-visible:outline-none focus-visible:ring-0"
+ : "flex h-7 cursor-pointer items-center rounded-md border border-transparent bg-transparent outline-none ring-0 transition-colors hover:bg-accent hover:text-accent-foreground focus:outline-none focus-visible:outline-none focus-visible:ring-0"
}
>
{recording ? (
@@ -117,24 +115,7 @@ function HotkeyRow({
Press hotkeys...
) : (
- <>
- {displayTokens.map((token, idx) => (
-
- {token.kind === "text" ? (
- token.value
- ) : token.value === "command" ? (
-
- ) : token.value === "option" ? (
-
- ) : (
-
- )}
-
- ))}
- >
+
)}
diff --git a/surfsense_web/app/desktop/login/page.tsx b/surfsense_web/app/desktop/login/page.tsx
index 6d5e2abd4..451143949 100644
--- a/surfsense_web/app/desktop/login/page.tsx
+++ b/surfsense_web/app/desktop/login/page.tsx
@@ -2,7 +2,7 @@
import { IconBrandGoogleFilled } from "@tabler/icons-react";
import { useAtom } from "jotai";
-import { ArrowBigUp, BrainCog, Command, Eye, EyeOff, Option, Rocket, RotateCcw, Zap } from "lucide-react";
+import { BrainCog, Eye, EyeOff, Rocket, RotateCcw, Zap } from "lucide-react";
import Image from "next/image";
import { useRouter } from "next/navigation";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
@@ -13,6 +13,7 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator";
+import { ShortcutKbd } from "@/components/ui/shortcut-kbd";
import { Spinner } from "@/components/ui/spinner";
import { useElectronAPI } from "@/hooks/use-platform";
import { searchSpacesApiService } from "@/lib/apis/search-spaces-api.service";
@@ -23,10 +24,6 @@ const isGoogleAuth = AUTH_TYPE === "GOOGLE";
type ShortcutKey = "generalAssist" | "quickAsk" | "autocomplete";
type ShortcutMap = typeof DEFAULT_SHORTCUTS;
-type ShortcutToken =
- | { kind: "text"; value: string }
- | { kind: "icon"; value: "command" | "option" | "shift" };
-
const HOTKEY_ROWS: Array<{ key: ShortcutKey; label: string; description: string; icon: React.ElementType }> = [
{
key: "generalAssist",
@@ -48,20 +45,20 @@ const HOTKEY_ROWS: Array<{ key: ShortcutKey; label: string; description: string;
},
];
-function acceleratorToTokens(accel: string, isMac: boolean): ShortcutToken[] {
+function acceleratorToKeys(accel: string, isMac: boolean): string[] {
if (!accel) return [];
return accel.split("+").map((part) => {
if (part === "CommandOrControl") {
- return isMac ? { kind: "icon", value: "command" as const } : { kind: "text", value: "Ctrl" };
+ return isMac ? "⌘" : "Ctrl";
}
if (part === "Alt") {
- return isMac ? { kind: "icon", value: "option" as const } : { kind: "text", value: "Alt" };
+ return isMac ? "⌥" : "Alt";
}
if (part === "Shift") {
- return isMac ? { kind: "icon", value: "shift" as const } : { kind: "text", value: "Shift" };
+ return isMac ? "⇧" : "Shift";
}
- if (part === "Space") return { kind: "text", value: "Space" };
- return { kind: "text", value: part.length === 1 ? part.toUpperCase() : part };
+ if (part === "Space") return "Space";
+ return part.length === 1 ? part.toUpperCase() : part;
});
}
@@ -87,7 +84,7 @@ function HotkeyRow({
const [recording, setRecording] = useState(false);
const inputRef = useRef
(null);
const isDefault = value === defaultValue;
- const displayTokens = useMemo(() => acceleratorToTokens(value, isMac), [value, isMac]);
+ const displayKeys = useMemo(() => acceleratorToKeys(value, isMac), [value, isMac]);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
@@ -135,36 +132,20 @@ function HotkeyRow({
setRecording(true)}
onKeyDown={handleKeyDown}
onBlur={() => setRecording(false)}
className={
recording
- ? "flex h-7 items-center rounded-md border border-primary bg-primary/5 ring-2 ring-primary/20"
- : "flex h-7 items-center rounded-md border border-input bg-muted/50 hover:bg-muted"
+ ? "flex h-7 items-center rounded-md border border-transparent bg-primary/5 outline-none ring-0 focus:outline-none focus-visible:outline-none focus-visible:ring-0"
+ : "flex h-7 cursor-pointer items-center rounded-md border border-transparent bg-transparent outline-none ring-0 transition-colors hover:bg-accent hover:text-accent-foreground focus:outline-none focus-visible:outline-none focus-visible:ring-0"
}
>
{recording ? (
Press hotkeys...
) : (
- <>
- {displayTokens.map((token, idx) => (
-
- {token.kind === "text" ? (
- token.value
- ) : token.value === "command" ? (
-
- ) : token.value === "option" ? (
-
- ) : (
-
- )}
-
- ))}
- >
+
)}
From a7a758f26edc04be3e3a6ec3a3cde207f8046bef Mon Sep 17 00:00:00 2001
From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com>
Date: Fri, 24 Apr 2026 05:03:23 +0530
Subject: [PATCH 33/35] feat(filesystem): add getAgentFilesystemMounts API and
integrate with LocalFilesystemBrowser for improved mount management
---
surfsense_desktop/src/ipc/channels.ts | 1 +
surfsense_desktop/src/ipc/handlers.ts | 5 ++
.../src/modules/agent-filesystem.ts | 7 ++-
surfsense_desktop/src/preload.ts | 2 +
.../ui/sidebar/LocalFilesystemBrowser.tsx | 61 +++++++++++++++++--
surfsense_web/types/window.d.ts | 6 ++
6 files changed, 77 insertions(+), 5 deletions(-)
diff --git a/surfsense_desktop/src/ipc/channels.ts b/surfsense_desktop/src/ipc/channels.ts
index 5cf6e9001..ccd166899 100644
--- a/surfsense_desktop/src/ipc/channels.ts
+++ b/surfsense_desktop/src/ipc/channels.ts
@@ -55,6 +55,7 @@ export const IPC_CHANNELS = {
ANALYTICS_GET_CONTEXT: 'analytics:get-context',
// Agent filesystem mode
AGENT_FILESYSTEM_GET_SETTINGS: 'agent-filesystem:get-settings',
+ AGENT_FILESYSTEM_GET_MOUNTS: 'agent-filesystem:get-mounts',
AGENT_FILESYSTEM_SET_SETTINGS: 'agent-filesystem:set-settings',
AGENT_FILESYSTEM_PICK_ROOT: 'agent-filesystem:pick-root',
} as const;
diff --git a/surfsense_desktop/src/ipc/handlers.ts b/surfsense_desktop/src/ipc/handlers.ts
index 247d171f5..54882f4ee 100644
--- a/surfsense_desktop/src/ipc/handlers.ts
+++ b/surfsense_desktop/src/ipc/handlers.ts
@@ -39,6 +39,7 @@ import {
import {
readAgentLocalFileText,
writeAgentLocalFileText,
+ getAgentFilesystemMounts,
getAgentFilesystemSettings,
pickAgentFilesystemRoot,
setAgentFilesystemSettings,
@@ -226,6 +227,10 @@ export function registerIpcHandlers(): void {
getAgentFilesystemSettings()
);
+ ipcMain.handle(IPC_CHANNELS.AGENT_FILESYSTEM_GET_MOUNTS, () =>
+ getAgentFilesystemMounts()
+ );
+
ipcMain.handle(
IPC_CHANNELS.AGENT_FILESYSTEM_SET_SETTINGS,
(_event, settings: { mode?: 'cloud' | 'desktop_local_folder'; localRootPaths?: string[] | null }) =>
diff --git a/surfsense_desktop/src/modules/agent-filesystem.ts b/surfsense_desktop/src/modules/agent-filesystem.ts
index 2bf0101d6..f00c185f8 100644
--- a/surfsense_desktop/src/modules/agent-filesystem.ts
+++ b/surfsense_desktop/src/modules/agent-filesystem.ts
@@ -122,7 +122,7 @@ function toVirtualPath(rootPath: string, absolutePath: string): string {
return `/${rel.replace(/\\/g, "/")}`;
}
-type LocalRootMount = {
+export type LocalRootMount = {
mount: string;
rootPath: string;
};
@@ -145,6 +145,11 @@ function buildRootMounts(rootPaths: string[]): LocalRootMount[] {
return mounts;
}
+export async function getAgentFilesystemMounts(): Promise
{
+ const rootPaths = await resolveCurrentRootPaths();
+ return buildRootMounts(rootPaths);
+}
+
function parseMountedVirtualPath(virtualPath: string): {
mount: string;
subPath: string;
diff --git a/surfsense_desktop/src/preload.ts b/surfsense_desktop/src/preload.ts
index f7aaf9633..9c538f691 100644
--- a/surfsense_desktop/src/preload.ts
+++ b/surfsense_desktop/src/preload.ts
@@ -108,6 +108,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
// Agent filesystem mode
getAgentFilesystemSettings: () =>
ipcRenderer.invoke(IPC_CHANNELS.AGENT_FILESYSTEM_GET_SETTINGS),
+ getAgentFilesystemMounts: () =>
+ ipcRenderer.invoke(IPC_CHANNELS.AGENT_FILESYSTEM_GET_MOUNTS),
setAgentFilesystemSettings: (settings: {
mode?: "cloud" | "desktop_local_folder";
localRootPaths?: string[] | null;
diff --git a/surfsense_web/components/layout/ui/sidebar/LocalFilesystemBrowser.tsx b/surfsense_web/components/layout/ui/sidebar/LocalFilesystemBrowser.tsx
index 7aebf4695..5b08f2e37 100644
--- a/surfsense_web/components/layout/ui/sidebar/LocalFilesystemBrowser.tsx
+++ b/surfsense_web/components/layout/ui/sidebar/LocalFilesystemBrowser.tsx
@@ -34,6 +34,11 @@ interface LocalFolderNode {
files: LocalFolderFileEntry[];
}
+type LocalRootMount = {
+ mount: string;
+ rootPath: string;
+};
+
const getFolderDisplayName = (rootPath: string): string =>
rootPath.split(/[\\/]/).at(-1) || rootPath;
@@ -50,6 +55,20 @@ function getFileName(pathValue: string): string {
return pathValue.split(/[\\/]/).at(-1) || pathValue;
}
+function toVirtualPath(relativePath: string): string {
+ const normalized = relativePath.replace(/\\/g, "/").replace(/^\/+/, "");
+ return `/${normalized}`;
+}
+
+function normalizeRootPathForLookup(rootPath: string, isWindows: boolean): string {
+ const normalized = rootPath.replace(/\\/g, "/").replace(/\/+$/, "");
+ return isWindows ? normalized.toLowerCase() : normalized;
+}
+
+function toMountedVirtualPath(mount: string, relativePath: string): string {
+ return `/${mount}${toVirtualPath(relativePath)}`;
+}
+
export function LocalFilesystemBrowser({
rootPaths,
searchSpaceId,
@@ -59,7 +78,9 @@ export function LocalFilesystemBrowser({
const electronAPI = useElectronAPI();
const [rootStateMap, setRootStateMap] = useState>({});
const [expandedFolderKeys, setExpandedFolderKeys] = useState>(new Set());
+ const [mountByRootKey, setMountByRootKey] = useState