feat(thread): support selecting and managing multiple local folders

This commit is contained in:
Anish Sarkar 2026-04-24 01:46:32 +05:30
parent 3ee2683391
commit a250f97162

View file

@ -110,11 +110,15 @@ const COMPOSER_PLACEHOLDER = "Ask anything, type / for prompts, type @ to mentio
type ComposerFilesystemSettings = { type ComposerFilesystemSettings = {
mode: "cloud" | "desktop_local_folder"; mode: "cloud" | "desktop_local_folder";
localRootPath: string | null; localRootPaths: string[];
updatedAt: string; updatedAt: string;
}; };
const LOCAL_FILESYSTEM_TRUST_KEY = "surfsense.local-filesystem-trust.v1"; 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 = () => { export const Thread: FC = () => {
return <ThreadContent />; return <ThreadContent />;
@ -388,6 +392,7 @@ const Composer: FC = () => {
null null
); );
const [localTrustDialogOpen, setLocalTrustDialogOpen] = useState(false); const [localTrustDialogOpen, setLocalTrustDialogOpen] = useState(false);
const [localFoldersOpen, setLocalFoldersOpen] = useState(false);
const [pendingLocalPath, setPendingLocalPath] = useState<string | null>(null); 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);
@ -414,7 +419,7 @@ const Composer: FC = () => {
if (!mounted) return; if (!mounted) return;
setFilesystemSettings({ setFilesystemSettings({
mode: "cloud", mode: "cloud",
localRootPath: null, localRootPaths: [],
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
}); });
}); });
@ -431,16 +436,27 @@ const Composer: FC = () => {
} }
}, []); }, []);
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( const applyLocalRootPath = useCallback(
async (path: string) => { async (path: string) => {
if (!electronAPI?.setAgentFilesystemSettings) return; 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({ const updated = await electronAPI.setAgentFilesystemSettings({
mode: "desktop_local_folder", mode: "desktop_local_folder",
localRootPath: path, localRootPaths: nextLocalRootPaths,
}); });
setFilesystemSettings(updated); setFilesystemSettings(updated);
}, },
[electronAPI] [electronAPI, localRootPaths]
); );
const runSwitchToLocalMode = useCallback(async () => { const runSwitchToLocalMode = useCallback(async () => {
@ -467,6 +483,7 @@ const Composer: FC = () => {
); );
const handlePickFilesystemRoot = useCallback(async () => { const handlePickFilesystemRoot = useCallback(async () => {
if (!canAddMoreLocalRoots) return;
if (hasLocalFilesystemTrust()) { if (hasLocalFilesystemTrust()) {
await runPickLocalRoot(); await runPickLocalRoot();
return; return;
@ -476,13 +493,25 @@ const Composer: FC = () => {
if (!picked) return; if (!picked) return;
setPendingLocalPath(picked); setPendingLocalPath(picked);
setLocalTrustDialogOpen(true); setLocalTrustDialogOpen(true);
}, [electronAPI, hasLocalFilesystemTrust, runPickLocalRoot]); }, [canAddMoreLocalRoots, electronAPI, hasLocalFilesystemTrust, runPickLocalRoot]);
const handleClearFilesystemRoot = useCallback(async () => { 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; if (!electronAPI?.setAgentFilesystemSettings) return;
const updated = await electronAPI.setAgentFilesystemSettings({ const updated = await electronAPI.setAgentFilesystemSettings({
mode: "desktop_local_folder", mode: "desktop_local_folder",
localRootPath: null, localRootPaths: [],
}); });
setFilesystemSettings(updated); setFilesystemSettings(updated);
}, [electronAPI]); }, [electronAPI]);
@ -833,31 +862,89 @@ const Composer: FC = () => {
{filesystemSettings.mode === "desktop_local_folder" && ( {filesystemSettings.mode === "desktop_local_folder" && (
<> <>
<div className="h-4 w-px bg-muted" /> <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"> <div className="flex min-w-0 flex-1 items-center gap-1.5">
{filesystemSettings.localRootPath ? ( {primaryLocalRootPath ? (
<> <>
<div <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" 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} title={primaryLocalRootPath}
> >
<Folder className="size-3.5 shrink-0 text-muted-foreground" /> <Folder className="size-3.5 shrink-0 text-muted-foreground" />
<span className="truncate"> <span className="truncate">
{filesystemSettings.localRootPath.split("/").at(-1) || {getFolderDisplayName(primaryLocalRootPath)}
filesystemSettings.localRootPath}
</span> </span>
<button <button
type="button" type="button"
onClick={(event) => { onClick={(event) => {
event.stopPropagation(); event.stopPropagation();
void handleClearFilesystemRoot(); void handleRemoveFilesystemRoot(primaryLocalRootPath);
}} }}
className="ml-0.5 shrink-0 text-muted-foreground transition-colors hover:text-foreground" className="ml-0.5 shrink-0 text-muted-foreground transition-colors hover:text-foreground"
aria-label="Clear local folder" aria-label="Remove local folder"
title="Clear local folder" title="Remove local folder"
> >
<X className="size-3.5" /> <X className="size-3.5" />
</button> </button>
</div> </div>
{extraLocalRootCount > 0 && (
<Popover open={localFoldersOpen} onOpenChange={setLocalFoldersOpen}>
<PopoverTrigger asChild>
<Button
type="button"
variant="ghost"
size="sm"
className="h-7 shrink-0 rounded-md bg-muted px-2 text-xs hover:bg-muted/90"
title="View selected folders"
aria-label="View selected folders"
>
+{extraLocalRootCount}
</Button>
</PopoverTrigger>
<PopoverContent
side="top"
align="start"
className="w-[320px] max-w-[calc(100vw-2rem)] p-2"
>
<div className="space-y-1.5">
{localRootPaths.map((rootPath) => (
<div
key={rootPath}
className="flex h-8 items-center gap-2 rounded-md bg-muted px-2"
title={rootPath}
>
<Folder className="size-3.5 shrink-0 text-muted-foreground" />
<span className="min-w-0 flex-1 truncate text-xs">
{getFolderDisplayName(rootPath)}
</span>
<button
type="button"
onClick={() => {
void handleRemoveFilesystemRoot(rootPath);
}}
className="text-muted-foreground transition-colors hover:text-foreground"
aria-label={`Remove ${rootPath}`}
>
<X className="size-3.5" />
</button>
</div>
))}
<div className="mt-2 flex items-center justify-end gap-2">
<Button
type="button"
variant="ghost"
size="sm"
className="h-7 px-2.5 text-xs"
onClick={() => {
void handleClearFilesystemRoots();
}}
>
Clear all
</Button>
</div>
</div>
</PopoverContent>
</Popover>
)}
<Button <Button
type="button" type="button"
variant="ghost" variant="ghost"
@ -866,8 +953,13 @@ const Composer: FC = () => {
onClick={() => { onClick={() => {
void handlePickFilesystemRoot(); void handlePickFilesystemRoot();
}} }}
title="Select local folder" title={
canAddMoreLocalRoots
? "Select local folder"
: `Maximum ${MAX_LOCAL_FILESYSTEM_ROOTS} folders`
}
aria-label="Select local folder" aria-label="Select local folder"
disabled={!canAddMoreLocalRoots}
> >
<FolderPlus className="size-3.5" /> <FolderPlus className="size-3.5" />
</Button> </Button>
@ -909,9 +1001,9 @@ const Composer: FC = () => {
Local mode can read and edit files inside the folders you select. Continue only if Local mode can read and edit files inside the folders you select. Continue only if
you trust this workspace and its contents. you trust this workspace and its contents.
</AlertDialogDescription> </AlertDialogDescription>
{(pendingLocalPath || filesystemSettings?.localRootPath) && ( {(pendingLocalPath || primaryLocalRootPath) && (
<AlertDialogDescription className="mt-1 whitespace-pre-wrap break-words font-mono text-xs"> <AlertDialogDescription className="mt-1 whitespace-pre-wrap break-words font-mono text-xs">
Folder path: {pendingLocalPath || filesystemSettings?.localRootPath} Folder path: {pendingLocalPath || primaryLocalRootPath}
</AlertDialogDescription> </AlertDialogDescription>
)} )}
</AlertDialogHeader> </AlertDialogHeader>