diff --git a/surfsense_web/components/assistant-ui/thread.tsx b/surfsense_web/components/assistant-ui/thread.tsx index 094d99a29..9df41ee55 100644 --- a/surfsense_web/components/assistant-ui/thread.tsx +++ b/surfsense_web/components/assistant-ui/thread.tsx @@ -12,11 +12,15 @@ import { AlertCircle, ArrowDownIcon, ArrowUpIcon, + Check, ChevronDown, ChevronUp, Clipboard, Dot, + Folder, + FolderPlus, Globe, + Laptop, Plus, Settings2, SquareIcon, @@ -66,6 +70,16 @@ 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 { @@ -100,6 +114,8 @@ type ComposerFilesystemSettings = { updatedAt: string; }; +const LOCAL_FILESYSTEM_TRUST_KEY = "surfsense.local-filesystem-trust.v1"; + export const Thread: FC = () => { return ; }; @@ -371,6 +387,8 @@ const Composer: FC = () => { const [filesystemSettings, setFilesystemSettings] = useState( null ); + const [localTrustDialogOpen, setLocalTrustDialogOpen] = useState(false); + const [pendingLocalPath, setPendingLocalPath] = useState(null); const [clipboardInitialText, setClipboardInitialText] = useState(); const clipboardLoadedRef = useRef(false); useEffect(() => { @@ -388,7 +406,7 @@ const Composer: FC = () => { let mounted = true; electronAPI .getAgentFilesystemSettings() - .then((settings) => { + .then((settings: ComposerFilesystemSettings) => { if (!mounted) return; setFilesystemSettings(settings); }) @@ -405,22 +423,66 @@ const Composer: FC = () => { }; }, [electronAPI]); - const handleFilesystemModeChange = useCallback( - async (mode: "cloud" | "desktop_local_folder") => { + const hasLocalFilesystemTrust = useCallback(() => { + try { + return window.localStorage.getItem(LOCAL_FILESYSTEM_TRUST_KEY) === "true"; + } catch { + return false; + } + }, []); + + const applyLocalRootPath = useCallback( + async (path: string) => { if (!electronAPI?.setAgentFilesystemSettings) return; - const updated = await electronAPI.setAgentFilesystemSettings({ mode }); + const updated = await electronAPI.setAgentFilesystemSettings({ + mode: "desktop_local_folder", + localRootPath: path, + }); setFilesystemSettings(updated); }, [electronAPI] ); - const handlePickFilesystemRoot = useCallback(async () => { - if (!electronAPI?.pickAgentFilesystemRoot || !electronAPI?.setAgentFilesystemSettings) return; + 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 (hasLocalFilesystemTrust()) { + await runPickLocalRoot(); + return; + } + if (!electronAPI?.pickAgentFilesystemRoot) return; + const picked = await electronAPI.pickAgentFilesystemRoot(); + if (!picked) return; + setPendingLocalPath(picked); + setLocalTrustDialogOpen(true); + }, [electronAPI, hasLocalFilesystemTrust, runPickLocalRoot]); + + const handleClearFilesystemRoot = useCallback(async () => { + if (!electronAPI?.setAgentFilesystemSettings) return; const updated = await electronAPI.setAgentFilesystemSettings({ mode: "desktop_local_folder", - localRootPath: picked, + localRootPath: null, }); setFilesystemSettings(updated); }, [electronAPI]); @@ -720,44 +782,161 @@ const Composer: FC = () => { 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" && ( + <> +
+
+ {filesystemSettings.localRootPath ? ( + <> +
+ + + {filesystemSettings.localRootPath.split("/").at(-1) || + filesystemSettings.localRootPath} + + +
+ + + ) : ( + + )} +
+ + )}
) : 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 || filesystemSettings?.localRootPath) && ( + + Folder path: {pendingLocalPath || filesystemSettings?.localRootPath} + + )} + + + 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 && (