mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-04 22:02:16 +02:00
feat(thread): support selecting and managing multiple local folders
This commit is contained in:
parent
3ee2683391
commit
a250f97162
1 changed files with 110 additions and 18 deletions
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue