feat(editor): implement local filesystem trust dialog and enhance filesystem mode selection

This commit is contained in:
Anish Sarkar 2026-04-23 22:27:58 +05:30
parent 18b4a6ea24
commit 84145566e3

View file

@ -12,11 +12,15 @@ import {
AlertCircle, AlertCircle,
ArrowDownIcon, ArrowDownIcon,
ArrowUpIcon, ArrowUpIcon,
Check,
ChevronDown, ChevronDown,
ChevronUp, ChevronUp,
Clipboard, Clipboard,
Dot, Dot,
Folder,
FolderPlus,
Globe, Globe,
Laptop,
Plus, Plus,
Settings2, Settings2,
SquareIcon, SquareIcon,
@ -66,6 +70,16 @@ import {
} from "@/components/new-chat/document-mention-picker"; } from "@/components/new-chat/document-mention-picker";
import { PromptPicker, type PromptPickerRef } from "@/components/new-chat/prompt-picker"; import { PromptPicker, type PromptPickerRef } from "@/components/new-chat/prompt-picker";
import { Avatar, AvatarFallback, AvatarGroup } from "@/components/ui/avatar"; 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 { Button } from "@/components/ui/button";
import { Drawer, DrawerContent, DrawerHandle, DrawerTitle } from "@/components/ui/drawer"; import { Drawer, DrawerContent, DrawerHandle, DrawerTitle } from "@/components/ui/drawer";
import { import {
@ -100,6 +114,8 @@ type ComposerFilesystemSettings = {
updatedAt: string; updatedAt: string;
}; };
const LOCAL_FILESYSTEM_TRUST_KEY = "surfsense.local-filesystem-trust.v1";
export const Thread: FC = () => { export const Thread: FC = () => {
return <ThreadContent />; return <ThreadContent />;
}; };
@ -371,6 +387,8 @@ const Composer: FC = () => {
const [filesystemSettings, setFilesystemSettings] = useState<ComposerFilesystemSettings | null>( const [filesystemSettings, setFilesystemSettings] = useState<ComposerFilesystemSettings | null>(
null null
); );
const [localTrustDialogOpen, setLocalTrustDialogOpen] = useState(false);
const [pendingLocalPath, setPendingLocalPath] = useState<string | null>(null);
const [clipboardInitialText, setClipboardInitialText] = useState<string | undefined>(); const [clipboardInitialText, setClipboardInitialText] = useState<string | undefined>();
const clipboardLoadedRef = useRef(false); const clipboardLoadedRef = useRef(false);
useEffect(() => { useEffect(() => {
@ -388,7 +406,7 @@ const Composer: FC = () => {
let mounted = true; let mounted = true;
electronAPI electronAPI
.getAgentFilesystemSettings() .getAgentFilesystemSettings()
.then((settings) => { .then((settings: ComposerFilesystemSettings) => {
if (!mounted) return; if (!mounted) return;
setFilesystemSettings(settings); setFilesystemSettings(settings);
}) })
@ -405,22 +423,66 @@ const Composer: FC = () => {
}; };
}, [electronAPI]); }, [electronAPI]);
const handleFilesystemModeChange = useCallback( const hasLocalFilesystemTrust = useCallback(() => {
async (mode: "cloud" | "desktop_local_folder") => { try {
return window.localStorage.getItem(LOCAL_FILESYSTEM_TRUST_KEY) === "true";
} catch {
return false;
}
}, []);
const applyLocalRootPath = useCallback(
async (path: string) => {
if (!electronAPI?.setAgentFilesystemSettings) return; if (!electronAPI?.setAgentFilesystemSettings) return;
const updated = await electronAPI.setAgentFilesystemSettings({ mode }); const updated = await electronAPI.setAgentFilesystemSettings({
mode: "desktop_local_folder",
localRootPath: path,
});
setFilesystemSettings(updated); setFilesystemSettings(updated);
}, },
[electronAPI] [electronAPI]
); );
const handlePickFilesystemRoot = useCallback(async () => { const runSwitchToLocalMode = useCallback(async () => {
if (!electronAPI?.pickAgentFilesystemRoot || !electronAPI?.setAgentFilesystemSettings) return; 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(); const picked = await electronAPI.pickAgentFilesystemRoot();
if (!picked) return; 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({ const updated = await electronAPI.setAgentFilesystemSettings({
mode: "desktop_local_folder", mode: "desktop_local_folder",
localRootPath: picked, localRootPath: null,
}); });
setFilesystemSettings(updated); setFilesystemSettings(updated);
}, [electronAPI]); }, [electronAPI]);
@ -720,44 +782,161 @@ const Composer: FC = () => {
members={members ?? []} members={members ?? []}
/> />
{electronAPI && filesystemSettings ? ( {electronAPI && filesystemSettings ? (
<div className="mx-3 flex items-center gap-2 rounded-xl border border-border/60 bg-background/60 p-1.5"> <div className="mx-3 flex items-center gap-1.5 pt-2 pb-0.5">
<button <DropdownMenu>
type="button" <DropdownMenuTrigger asChild>
onClick={() => handleFilesystemModeChange("cloud")} <Button
className={cn( type="button"
"rounded-lg px-2.5 py-1 text-xs font-medium transition-colors", variant="ghost"
filesystemSettings.mode === "cloud" size="sm"
? "bg-accent text-foreground" className="h-7 gap-1.5 rounded-md bg-muted px-2.5 font-medium text-xs select-none hover:bg-muted/90"
: "text-muted-foreground hover:bg-accent/60 hover:text-foreground" >
)} {filesystemSettings.mode === "cloud" ? (
> <>
Cloud <Globe className="size-3.5" />
</button> Cloud
<button </>
type="button" ) : (
onClick={() => handleFilesystemModeChange("desktop_local_folder")} <>
className={cn( <Laptop className="size-3.5" />
"rounded-lg px-2.5 py-1 text-xs font-medium transition-colors", Local
filesystemSettings.mode === "desktop_local_folder" </>
? "bg-accent text-foreground" )}
: "text-muted-foreground hover:bg-accent/60 hover:text-foreground" </Button>
)} </DropdownMenuTrigger>
> <DropdownMenuContent side="top" align="start" className="min-w-[180px]">
Local <DropdownMenuItem
</button> onClick={() => handleFilesystemModeChange("cloud")}
<div className="h-4 w-px bg-border/70" /> className="flex items-center justify-between"
<button >
type="button" <span className="flex items-center gap-2">
onClick={handlePickFilesystemRoot} <Globe className="size-4" />
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" Cloud
title={filesystemSettings.localRootPath ?? "Select local folder"} </span>
> {filesystemSettings.mode === "cloud" && <Check className="size-4 text-primary" />}
{filesystemSettings.localRootPath </DropdownMenuItem>
? filesystemSettings.localRootPath.split("/").at(-1) || filesystemSettings.localRootPath <DropdownMenuItem
: "Select folder..."} onClick={() => handleFilesystemModeChange("desktop_local_folder")}
</button> className="flex items-center justify-between"
>
<span className="flex items-center gap-2">
<Laptop className="size-4" />
Local
</span>
{filesystemSettings.mode === "desktop_local_folder" && (
<Check className="size-4 text-primary" />
)}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{filesystemSettings.mode === "desktop_local_folder" && (
<>
<div className="h-4 w-px bg-muted" />
<div className="flex min-w-0 flex-1 items-center gap-1.5 overflow-x-auto scrollbar-hide">
{filesystemSettings.localRootPath ? (
<>
<div
className="inline-flex h-7 max-w-[190px] shrink-0 items-center gap-1.5 rounded-md bg-muted px-2.5 text-xs"
title={filesystemSettings.localRootPath}
>
<Folder className="size-3.5 shrink-0 text-muted-foreground" />
<span className="truncate">
{filesystemSettings.localRootPath.split("/").at(-1) ||
filesystemSettings.localRootPath}
</span>
<button
type="button"
onClick={(event) => {
event.stopPropagation();
void handleClearFilesystemRoot();
}}
className="ml-0.5 shrink-0 text-muted-foreground transition-colors hover:text-foreground"
aria-label="Clear local folder"
title="Clear local folder"
>
<X className="size-3.5" />
</button>
</div>
<Button
type="button"
variant="ghost"
size="icon"
className="size-7 shrink-0 rounded-md bg-muted hover:bg-muted/90"
onClick={() => {
void handlePickFilesystemRoot();
}}
title="Select local folder"
aria-label="Select local folder"
>
<FolderPlus className="size-3.5" />
</Button>
</>
) : (
<Button
type="button"
variant="ghost"
size="sm"
className="h-7 max-w-[190px] shrink-0 justify-start gap-1.5 rounded-md bg-muted px-2.5 font-medium text-xs select-none hover:bg-muted/90"
onClick={() => {
void handlePickFilesystemRoot();
}}
title="Select local folder"
aria-label="Select local folder"
>
<Folder className="size-3.5 shrink-0 text-muted-foreground" />
<span className="truncate">Select a folder</span>
</Button>
)}
</div>
</>
)}
</div> </div>
) : null} ) : null}
<AlertDialog
open={localTrustDialogOpen}
onOpenChange={(open) => {
setLocalTrustDialogOpen(open);
if (!open) {
setPendingLocalPath(null);
}
}}
>
<AlertDialogContent className="sm:max-w-md select-none">
<AlertDialogHeader>
<AlertDialogTitle>Trust this workspace?</AlertDialogTitle>
<AlertDialogDescription>
Local mode can read and edit files inside the folders you select. Continue only if
you trust this workspace and its contents.
</AlertDialogDescription>
{(pendingLocalPath || filesystemSettings?.localRootPath) && (
<AlertDialogDescription className="mt-1 whitespace-pre-wrap break-words font-mono text-xs">
Folder path: {pendingLocalPath || filesystemSettings?.localRootPath}
</AlertDialogDescription>
)}
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
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
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{showDocumentPopover && ( {showDocumentPopover && (
<div className="absolute bottom-full left-0 z-[9999] mb-2"> <div className="absolute bottom-full left-0 z-[9999] mb-2">
<DocumentMentionPicker <DocumentMentionPicker