- ) : isLargeDocument ? (
+ ) : isLargeDocument && !isLocalFileMode ? (
) : (
@@ -324,13 +598,19 @@ function DesktopEditorPanel() {
return () => document.removeEventListener("keydown", handleKeyDown);
}, [closePanel]);
- if (!panelState.isOpen || !panelState.documentId || !panelState.searchSpaceId) return null;
+ const hasTarget =
+ panelState.kind === "document"
+ ? !!panelState.documentId && !!panelState.searchSpaceId
+ : !!panelState.localFilePath;
+ if (!panelState.isOpen || !hasTarget) return null;
return (
);
@@ -110,7 +114,11 @@ export function RightPanel({ documentsPanel }: RightPanelProps) {
const documentsOpen = documentsPanel?.open ?? false;
const reportOpen = reportState.isOpen && !!reportState.reportId;
- const editorOpen = editorState.isOpen && !!editorState.documentId;
+ const editorOpen =
+ editorState.isOpen &&
+ (editorState.kind === "document"
+ ? !!editorState.documentId
+ : !!editorState.localFilePath);
const hitlEditOpen = hitlEditState.isOpen && !!hitlEditState.onSave;
useEffect(() => {
@@ -179,8 +187,10 @@ export function RightPanel({ documentsPanel }: RightPanelProps) {
{effectiveTab === "editor" && editorOpen && (
diff --git a/surfsense_web/components/layout/ui/sidebar/AllPrivateChatsSidebar.tsx b/surfsense_web/components/layout/ui/sidebar/AllPrivateChatsSidebar.tsx
index 3459fccf6..ab5213db2 100644
--- a/surfsense_web/components/layout/ui/sidebar/AllPrivateChatsSidebar.tsx
+++ b/surfsense_web/components/layout/ui/sidebar/AllPrivateChatsSidebar.tsx
@@ -8,7 +8,7 @@ import {
ChevronLeft,
MessageCircleMore,
MoreHorizontal,
- PenLine,
+ Pencil,
RotateCcwIcon,
Search,
Trash2,
@@ -429,7 +429,7 @@ export function AllPrivateChatsSidebarContent({
handleStartRename(thread.id, thread.title || "New Chat")}
>
-
+
{t("rename") || "Rename"}
)}
diff --git a/surfsense_web/components/layout/ui/sidebar/AllSharedChatsSidebar.tsx b/surfsense_web/components/layout/ui/sidebar/AllSharedChatsSidebar.tsx
index 097d10121..ab1072459 100644
--- a/surfsense_web/components/layout/ui/sidebar/AllSharedChatsSidebar.tsx
+++ b/surfsense_web/components/layout/ui/sidebar/AllSharedChatsSidebar.tsx
@@ -8,7 +8,7 @@ import {
ChevronLeft,
MessageCircleMore,
MoreHorizontal,
- PenLine,
+ Pencil,
RotateCcwIcon,
Search,
Trash2,
@@ -428,7 +428,7 @@ export function AllSharedChatsSidebarContent({
handleStartRename(thread.id, thread.title || "New Chat")}
>
-
+
{t("rename") || "Rename"}
)}
diff --git a/surfsense_web/components/layout/ui/sidebar/ChatListItem.tsx b/surfsense_web/components/layout/ui/sidebar/ChatListItem.tsx
index 7f3089a89..bfc930b25 100644
--- a/surfsense_web/components/layout/ui/sidebar/ChatListItem.tsx
+++ b/surfsense_web/components/layout/ui/sidebar/ChatListItem.tsx
@@ -1,6 +1,6 @@
"use client";
-import { ArchiveIcon, MoreHorizontal, PenLine, RotateCcwIcon, Trash2 } from "lucide-react";
+import { ArchiveIcon, MoreHorizontal, Pencil, RotateCcwIcon, Trash2 } from "lucide-react";
import { useTranslations } from "next-intl";
import { useCallback, useState } from "react";
import { Button } from "@/components/ui/button";
@@ -106,7 +106,7 @@ export function ChatListItem({
onRename();
}}
>
-
+
{t("rename") || "Rename"}
)}
diff --git a/surfsense_web/components/layout/ui/sidebar/DocumentsSidebar.tsx b/surfsense_web/components/layout/ui/sidebar/DocumentsSidebar.tsx
index daed8747d..5819dcef4 100644
--- a/surfsense_web/components/layout/ui/sidebar/DocumentsSidebar.tsx
+++ b/surfsense_web/components/layout/ui/sidebar/DocumentsSidebar.tsx
@@ -6,9 +6,14 @@ import {
ChevronLeft,
ChevronRight,
FileText,
+ Folder,
+ FolderPlus,
FolderClock,
+ Laptop,
Lock,
Paperclip,
+ Search,
+ Server,
Trash2,
Unplug,
Upload,
@@ -58,8 +63,19 @@ 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";
import { useAnonymousMode, useIsAnonymous } from "@/contexts/anonymous-mode";
import { useLoginGate } from "@/contexts/login-gate";
@@ -68,17 +84,39 @@ import type { DocumentTypeEnum } from "@/contracts/types/document.types";
import { useDebouncedValue } from "@/hooks/use-debounced-value";
import { useMediaQuery } from "@/hooks/use-media-query";
import { useElectronAPI } from "@/hooks/use-platform";
+import { anonymousChatApiService } from "@/lib/apis/anonymous-chat-api.service";
import { documentsApiService } from "@/lib/apis/documents-api.service";
import { foldersApiService } from "@/lib/apis/folders-api.service";
import { searchSpacesApiService } from "@/lib/apis/search-spaces-api.service";
import { authenticatedFetch } from "@/lib/auth-utils";
-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 +171,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 +341,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 +359,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 +373,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 +524,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 +554,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 +587,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 +987,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 */}