File size: 4,721 Bytes
f59fbe2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 | import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { IconFolder, IconFolderOpen, IconCheck, IconHome } from "@tabler/icons-react";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { useSettingsStore, useChatStore } from "@/stores";
import { bridge } from "@/services";
import { cn } from "@/lib/utils";
export function WorkDirModal() {
const { workDirModalOpen, setWorkDirModalOpen, currentWorkDir, workspaceRoot, setCurrentWorkDir } = useSettingsStore();
const { startNewConversation } = useChatStore();
const [loading, setLoading] = useState(false);
const { data: workDirs = [], isPending, isError } = useQuery({
queryKey: ["registeredWorkDirs"],
queryFn: () => bridge.getRegisteredWorkDirs(),
enabled: workDirModalOpen,
});
const handleSelect = async (dir: string | null) => {
setLoading(true);
try {
const result = await bridge.setWorkDir(dir);
if (result.ok) {
setCurrentWorkDir(result.workDir === workspaceRoot ? null : result.workDir);
await startNewConversation();
setWorkDirModalOpen(false);
}
} finally {
setLoading(false);
}
};
const handleBrowse = async () => {
setLoading(true);
try {
const result = await bridge.browseWorkDir();
if (result.ok && result.workDir) {
setCurrentWorkDir(result.workDir === workspaceRoot ? null : result.workDir);
await startNewConversation();
setWorkDirModalOpen(false);
}
} finally {
setLoading(false);
}
};
const displayPath = (fullPath: string) => {
if (!workspaceRoot) return fullPath;
if (fullPath === workspaceRoot) return fullPath.split("/").pop() || fullPath;
return fullPath.replace(workspaceRoot, ".");
};
const isSelected = (dir: string) => {
if (!currentWorkDir) return dir === workspaceRoot;
return dir === currentWorkDir;
};
return (
<Dialog open={workDirModalOpen} onOpenChange={setWorkDirModalOpen}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Select Working Directory</DialogTitle>
</DialogHeader>
<div className="space-y-1 max-h-64 overflow-y-auto -mx-1 px-1">
{isPending ? (
<div className="px-3 py-8 text-center text-xs text-muted-foreground">Loading…</div>
) : isError ? (
<div className="px-3 py-8 text-center text-xs text-destructive">Failed to load working directories</div>
) : (
workDirs.map((dir) => (
<button
key={dir}
onClick={() => {
void handleSelect(dir === workspaceRoot ? null : dir);
}}
disabled={loading}
className={cn(
"w-full flex items-center gap-2 px-3 py-2 rounded-md text-left text-sm transition-colors",
isSelected(dir) ? "bg-accent" : "hover:bg-accent/50",
loading && "opacity-50 cursor-not-allowed",
)}
>
{dir === workspaceRoot ? (
<IconHome className="size-4 text-muted-foreground shrink-0" />
) : (
<IconFolder className="size-4 text-muted-foreground shrink-0" />
)}
<span className="flex-1 truncate">{displayPath(dir)}</span>
{isSelected(dir) && <IconCheck className="size-4 text-blue-500 shrink-0" />}
{dir === workspaceRoot && <span className="text-xs text-muted-foreground">(root)</span>}
</button>
))
)}
</div>
<DialogFooter className="sm:justify-between gap-2">
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => {
void handleBrowse();
}}
disabled={loading}
>
<IconFolderOpen className="size-4 mr-1.5" />
Browse...
</Button>
{currentWorkDir && (
<Button
variant="outline"
size="sm"
onClick={() => {
void handleSelect(null);
}}
disabled={loading}
>
<IconHome className="size-4 mr-1.5" />
Reset
</Button>
)}
</div>
<Button variant="ghost" size="sm" onClick={() => setWorkDirModalOpen(false)}>
Cancel
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
|