import { useEffect, useMemo, useState } from "react";import { Effect, Exit } from "effect"; import { Button } from "@executor-js/react/components/button";import { CardStack, CardStackContent, CardStackEntryField,} from "@executor-js/react/components/card-stack";import { FloatActions } from "@executor-js/react/components/float-actions";import { Input } from "@executor-js/react/components/input";import { Checkbox } from "@executor-js/react/components/checkbox";import { Label } from "@executor-js/react/components/label";import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle,} from "@executor-js/react/components/dialog";import { FormErrorAlert, useSlugAlreadyExists } from "@executor-js/react/lib/integration-add"; import { createCustomToolSourceEffect, formatSyncErrors, listCustomToolDirectoriesEffect, parseGitSourceUrl, slugifyCustomToolsAppName, suggestCustomToolsAppName, syncCustomToolSourceEffect, validateCustomToolsAppSlug, validateGitSourceUrl, type CustomToolsDirectoryListing, type AppSourceKind,} from "./custom-tools-client";import { directoryBrowserRows, directorySourceVerdict } from "./source-panel-model"; export default function AddCustomToolsSource(props: { readonly onComplete: (slug?: string) => void; readonly onCancel: () => void; readonly initialUrl?: string; readonly initialNamespace?: string; readonly sourceKinds?: readonly AppSourceKind[];}) { const sourceKinds = props.sourceKinds ?? ["git"]; const allowLocalDirectory = sourceKinds.includes("local-directory"); const initialKind = props.initialUrl && props.initialUrl.startsWith("/") && allowLocalDirectory ? "local-directory" : "git"; const [kind, setKind] = useState<AppSourceKind>(initialKind); const [url, setUrl] = useState(props.initialUrl ?? ""); const [path, setPath] = useState( initialKind === "local-directory" ? (props.initialUrl ?? "") : "", ); const [ref, setRef] = useState(""); const [name, setName] = useState( props.initialNamespace ? slugifyCustomToolsAppName(props.initialNamespace) : "", ); const [nameTouched, setNameTouched] = useState(props.initialNamespace !== undefined); const [token, setToken] = useState(""); const [fieldError, setFieldError] = useState<string | null>(null); const [nameError, setNameError] = useState<string | null>(null); const [syncing, setSyncing] = useState(false); const [syncError, setSyncError] = useState<string | null>(null); const [browseOpen, setBrowseOpen] = useState(false); const [pathListing, setPathListing] = useState<CustomToolsDirectoryListing | null>(null); const sourceValue = kind === "git" ? url : path; const effectiveName = useMemo( () => (nameTouched ? name : slugifyCustomToolsAppName(suggestCustomToolsAppName(sourceValue))), [name, nameTouched, sourceValue], ); const slug = effectiveName; const slugAlreadyExists = useSlugAlreadyExists(slug); useEffect(() => { if (kind !== "local-directory" || !path.trim().startsWith("/")) { setPathListing(null); return; } let active = true; const timeout = window.setTimeout(() => { void Effect.runPromiseExit(listCustomToolDirectoriesEffect({ path: path.trim() })).then( (exit) => { if (!active) return; setPathListing(Exit.isSuccess(exit) ? exit.value : null); }, ); }, 300); return () => { active = false; window.clearTimeout(timeout); }; }, [kind, path]); const submit = async () => { const nextFieldError = kind === "git" ? validateGitSourceUrl(url) : path.trim().startsWith("/") ? null : "Enter an absolute directory path."; const nextNameError = validateCustomToolsAppSlug(slug); setFieldError(nextFieldError); setNameError(nextNameError); setSyncError(null); if (nextFieldError || nextNameError) return; if (slugAlreadyExists) { setSyncError(`An integration named "${slug}" already exists. Choose another source name.`); return; } setSyncing(true); const parsedGitUrl = kind === "git" ? parseGitSourceUrl(url) : null; const createExit = await Effect.runPromiseExit( createCustomToolSourceEffect( kind === "git" ? { kind: "git", slug, app: slug, url: parsedGitUrl?.ok ? parsedGitUrl.url : url.trim(), ...(ref.trim() ? { ref: ref.trim() } : {}), ...(token.trim() ? { token: token.trim() } : {}), } : { kind: "local-directory", slug, app: slug, path: path.trim() }, ), ); if (Exit.isFailure(createExit)) { setSyncError("Failed to create custom tools source."); setSyncing(false); return; } const syncExit = await Effect.runPromiseExit( syncCustomToolSourceEffect(createExit.value.source.slug), ); if (Exit.isFailure(syncExit)) { setSyncError("Failed to sync custom tools source."); setSyncing(false); return; } const result = syncExit.value; if (result.status === "failed") { setSyncError(formatSyncErrors(result).join("\n") || "Sync failed."); setSyncing(false); return; } setSyncing(false); props.onComplete(createExit.value.source.app); }; return ( <div className="flex flex-1 flex-col gap-6"> <div> <h1 className="text-xl font-semibold text-foreground">Add custom tools</h1> <p className="mt-1 text-[13px] text-muted-foreground"> Sync tools from a Git repository, then publish them to the Apps catalog. </p> </div> <CardStack> <CardStackContent className="border-t-0"> {allowLocalDirectory && ( <CardStackEntryField label="Source type" description="- Git is portable. Local directories are local-only." > <div className="flex gap-2"> <Button type="button" variant={kind === "git" ? "default" : "outline"} onClick={() => setKind("git")} disabled={syncing} > Git repository </Button> <Button type="button" variant={kind === "local-directory" ? "default" : "outline"} onClick={() => setKind("local-directory")} disabled={syncing} > Directory path </Button> </div> </CardStackEntryField> )} {kind === "git" ? ( <> <CardStackEntryField label="Git repository" description="- Any https Git remote."> <div className="space-y-1.5"> <Input value={url} onChange={(event) => { setUrl((event.target as HTMLInputElement).value); setFieldError(null); setSyncError(null); }} onBlur={() => setFieldError(validateGitSourceUrl(url))} placeholder="https://github.com/acme/tools.git" className="font-mono text-sm" aria-invalid={fieldError ? true : undefined} /> {fieldError && <p className="text-xs text-destructive">{fieldError}</p>} </div> </CardStackEntryField> <CardStackEntryField label="Ref (optional)" description="- Branch, tag, or commit SHA." > <Input value={ref} onChange={(event) => setRef((event.target as HTMLInputElement).value)} placeholder="main" className="font-mono text-sm" disabled={syncing} /> </CardStackEntryField> <CardStackEntryField label="Token (optional)" description="- For private repositories. Never shown after saving." > <Input value={token} type="password" onChange={(event) => setToken((event.target as HTMLInputElement).value)} autoComplete="off" className="font-mono text-sm" disabled={syncing} /> </CardStackEntryField> </> ) : ( <CardStackEntryField label="Directory path" description="- Absolute path on this machine." > <div className="space-y-1.5"> <div className="flex gap-2"> <Input value={path} onChange={(event) => { setPath((event.target as HTMLInputElement).value); setFieldError(null); setSyncError(null); }} onBlur={() => { if (!path.trim().startsWith("/")) return; void Effect.runPromiseExit( listCustomToolDirectoriesEffect({ path: path.trim() }), ).then((exit) => { setPathListing(Exit.isSuccess(exit) ? exit.value : null); }); }} placeholder="/Users/me/tools" className="font-mono text-sm" aria-invalid={fieldError ? true : undefined} /> <Button type="button" variant="outline" onClick={() => setBrowseOpen(true)} disabled={syncing} > Browse </Button> </div> {pathListing && <DirectorySourceStatus listing={pathListing} />} {fieldError && <p className="text-xs text-destructive">{fieldError}</p>} </div> </CardStackEntryField> )} <CardStackEntryField label="Source name" description="- Lowercase letters, numbers, and hyphens." > <div className="space-y-1.5"> <Input value={effectiveName} onChange={(event) => { setNameTouched(true); setName(slugifyCustomToolsAppName((event.target as HTMLInputElement).value)); setNameError(null); setSyncError(null); }} placeholder="custom-tools" className="text-sm" aria-invalid={nameError ? true : undefined} /> {nameError && <p className="text-xs text-destructive">{nameError}</p>} {slugAlreadyExists && !syncing && !nameError && ( <p className="text-xs text-destructive"> An integration named "{slug}" already exists. </p> )} </div> </CardStackEntryField> </CardStackContent> </CardStack> {syncError && <FormErrorAlert message={syncError} />} <FloatActions> <Button type="button" variant="ghost" onClick={() => props.onCancel()} disabled={syncing}> Cancel </Button> <Button type="button" onClick={() => void submit()} disabled={syncing} loading={syncing}> Sync source </Button> </FloatActions> {browseOpen && ( <DirectoryBrowserModal initialPath={path.trim() || undefined} onClose={() => setBrowseOpen(false)} onSelect={(selectedPath) => { setPath(selectedPath); setFieldError(null); setSyncError(null); setBrowseOpen(false); }} /> )} </div> );} function DirectoryBrowserModal(props: { readonly initialPath?: string; readonly onSelect: (path: string) => void; readonly onClose: () => void;}) { const [currentPath, setCurrentPath] = useState(props.initialPath); const [includeHidden, setIncludeHidden] = useState(false); const [listing, setListing] = useState<CustomToolsDirectoryListing | null>(null); const [loading, setLoading] = useState(true); const [error, setError] = useState<string | null>(null); useEffect(() => { let active = true; setLoading(true); setError(null); void Effect.runPromiseExit( listCustomToolDirectoriesEffect({ path: currentPath, includeHidden }), ).then((exit) => { if (!active) return; setLoading(false); if (Exit.isFailure(exit)) { setError("Unable to read this directory."); return; } setListing(exit.value); setCurrentPath(exit.value.path); }); return () => { active = false; }; }, [currentPath, includeHidden]); const rows = listing ? directoryBrowserRows(listing) : []; const selectedPath = listing?.path ?? currentPath ?? ""; const canSelect = Boolean(selectedPath) && !loading && !error; const hasToolFiles = (listing?.source.toolFiles.length ?? 0) > 0; return ( <Dialog open onOpenChange={(open) => (open ? undefined : props.onClose())}> <DialogContent className="sm:max-w-[620px]"> <DialogHeader className="border-b border-border/60 pb-4"> <DialogTitle className="text-base">Choose directory</DialogTitle> <DialogDescription className="font-mono text-xs break-all"> {selectedPath || "Home"} </DialogDescription> {listing && <DirectorySourceStatus listing={listing} />} </DialogHeader> <div className="space-y-3"> <Label className="flex items-center gap-2 text-sm text-muted-foreground"> <Checkbox checked={includeHidden} onCheckedChange={(checked) => setIncludeHidden(checked === true)} /> Show hidden directories </Label> <div className="max-h-[340px] overflow-auto rounded-md border border-border"> {loading ? ( <div className="px-3 py-8 text-center text-sm text-muted-foreground">Loading...</div> ) : error ? ( <div className="px-3 py-8 text-center text-sm text-destructive">{error}</div> ) : rows.length === 0 ? ( <div className="px-3 py-8 text-center text-sm text-muted-foreground"> No subdirectories </div> ) : ( <div role="listbox" aria-label="Directories" className="divide-y divide-border"> {rows.map((row) => ( <Button key={`${row.kind}:${row.path}`} type="button" variant="ghost" className="h-auto w-full justify-between gap-3 rounded-none px-3 py-2 text-left text-sm font-normal hover:bg-accent focus-visible:bg-accent focus-visible:outline-none" onClick={() => setCurrentPath(row.path)} > <span className="min-w-0 truncate font-mono"> {row.kind === "parent" ? ".." : row.name} </span> <span className="flex shrink-0 items-center gap-2"> {row.kind === "dir" && row.hasTools && ( <span className="rounded-sm border border-border bg-secondary px-1.5 py-0.5 font-mono text-[11px] text-muted-foreground"> tools </span> )} {row.kind === "dir" && row.isSymlink && ( <span className="font-mono text-[11px] text-muted-foreground">symlink</span> )} </span> </Button> ))} </div> )} </div> </div> <DialogFooter className="border-t border-border/60 pt-4"> <Button type="button" variant="ghost" onClick={props.onClose}> Cancel </Button> <Button type="button" onClick={() => props.onSelect(selectedPath)} disabled={!canSelect}> {hasToolFiles ? "Select" : "Select anyway"} </Button> </DialogFooter> </DialogContent> </Dialog> );} function DirectorySourceStatus(props: { readonly listing: CustomToolsDirectoryListing }) { const verdict = directorySourceVerdict(props.listing); if (verdict.type === "valid") { return ( <p className="mt-2 text-xs text-foreground"> {verdict.message}:{" "} {verdict.visibleTools.map((tool, index) => ( <span key={tool}> {index > 0 ? ", " : ""} <span className="font-mono">{tool}</span> </span> ))} {verdict.moreCount > 0 && ( <> {", "} <span className="font-mono">+{verdict.moreCount} more</span> </> )} </p> ); } return <p className="mt-2 text-xs text-muted-foreground">{verdict.message}</p>;}
import { useEffect, useMemo, useState } from "react";
import { Effect, Exit } from "effect";
import { Button } from "@executor-js/react/components/button";
import {
CardStack,
CardStackContent,
CardStackEntryField,
} from "@executor-js/react/components/card-stack";
import { FloatActions } from "@executor-js/react/components/float-actions";
import { Input } from "@executor-js/react/components/input";
import { Checkbox } from "@executor-js/react/components/checkbox";
import { Label } from "@executor-js/react/components/label";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@executor-js/react/components/dialog";
import { FormErrorAlert, useSlugAlreadyExists } from "@executor-js/react/lib/integration-add";
import {
createCustomToolSourceEffect,
formatSyncErrors,
listCustomToolDirectoriesEffect,
parseGitSourceUrl,
slugifyCustomToolsAppName,
suggestCustomToolsAppName,
syncCustomToolSourceEffect,
validateCustomToolsAppSlug,
validateGitSourceUrl,
type CustomToolsDirectoryListing,
type AppSourceKind,
} from "./custom-tools-client";
import { directoryBrowserRows, directorySourceVerdict } from "./source-panel-model";
export default function AddCustomToolsSource(props: {
readonly onComplete: (slug?: string) => void;
readonly onCancel: () => void;
readonly initialUrl?: string;
readonly initialNamespace?: string;
readonly sourceKinds?: readonly AppSourceKind[];
}) {
const sourceKinds = props.sourceKinds ?? ["git"];
const allowLocalDirectory = sourceKinds.includes("local-directory");
const initialKind =
props.initialUrl && props.initialUrl.startsWith("/") && allowLocalDirectory
? "local-directory"
: "git";
const [kind, setKind] = useState<AppSourceKind>(initialKind);
const [url, setUrl] = useState(props.initialUrl ?? "");
const [path, setPath] = useState(
initialKind === "local-directory" ? (props.initialUrl ?? "") : "",
);
const [ref, setRef] = useState("");
const [name, setName] = useState(
props.initialNamespace ? slugifyCustomToolsAppName(props.initialNamespace) : "",
);
const [nameTouched, setNameTouched] = useState(props.initialNamespace !== undefined);
const [token, setToken] = useState("");
const [fieldError, setFieldError] = useState<string | null>(null);
const [nameError, setNameError] = useState<string | null>(null);
const [syncing, setSyncing] = useState(false);
const [syncError, setSyncError] = useState<string | null>(null);
const [browseOpen, setBrowseOpen] = useState(false);
const [pathListing, setPathListing] = useState<CustomToolsDirectoryListing | null>(null);
const sourceValue = kind === "git" ? url : path;
const effectiveName = useMemo(
() => (nameTouched ? name : slugifyCustomToolsAppName(suggestCustomToolsAppName(sourceValue))),
[name, nameTouched, sourceValue],
);
const slug = effectiveName;
const slugAlreadyExists = useSlugAlreadyExists(slug);
useEffect(() => {
if (kind !== "local-directory" || !path.trim().startsWith("/")) {
setPathListing(null);
return;
}
let active = true;
const timeout = window.setTimeout(() => {
void Effect.runPromiseExit(listCustomToolDirectoriesEffect({ path: path.trim() })).then(
(exit) => {
if (!active) return;
setPathListing(Exit.isSuccess(exit) ? exit.value : null);
},
);
}, 300);
return () => {
active = false;
window.clearTimeout(timeout);
};
}, [kind, path]);
const submit = async () => {
const nextFieldError =
kind === "git"
? validateGitSourceUrl(url)
: path.trim().startsWith("/")
? null
: "Enter an absolute directory path.";
const nextNameError = validateCustomToolsAppSlug(slug);
setFieldError(nextFieldError);
setNameError(nextNameError);
setSyncError(null);
if (nextFieldError || nextNameError) return;
if (slugAlreadyExists) {
setSyncError(`An integration named "${slug}" already exists. Choose another source name.`);
return;
}
setSyncing(true);
const parsedGitUrl = kind === "git" ? parseGitSourceUrl(url) : null;
const createExit = await Effect.runPromiseExit(
createCustomToolSourceEffect(
kind === "git"
? {
kind: "git",
slug,
app: slug,
url: parsedGitUrl?.ok ? parsedGitUrl.url : url.trim(),
...(ref.trim() ? { ref: ref.trim() } : {}),
...(token.trim() ? { token: token.trim() } : {}),
}
: { kind: "local-directory", slug, app: slug, path: path.trim() },
),
);
if (Exit.isFailure(createExit)) {
setSyncError("Failed to create custom tools source.");
setSyncing(false);
return;
}
const syncExit = await Effect.runPromiseExit(
syncCustomToolSourceEffect(createExit.value.source.slug),
);
if (Exit.isFailure(syncExit)) {
setSyncError("Failed to sync custom tools source.");
setSyncing(false);
return;
}
const result = syncExit.value;
if (result.status === "failed") {
setSyncError(formatSyncErrors(result).join("\n") || "Sync failed.");
setSyncing(false);
return;
}
setSyncing(false);
props.onComplete(createExit.value.source.app);
};
return (
<div className="flex flex-1 flex-col gap-6">
<div>
<h1 className="text-xl font-semibold text-foreground">Add custom tools</h1>
<p className="mt-1 text-[13px] text-muted-foreground">
Sync tools from a Git repository, then publish them to the Apps catalog.
</p>
</div>
<CardStack>
<CardStackContent className="border-t-0">
{allowLocalDirectory && (
<CardStackEntryField
label="Source type"
description="- Git is portable. Local directories are local-only."
>
<div className="flex gap-2">
<Button
type="button"
variant={kind === "git" ? "default" : "outline"}
onClick={() => setKind("git")}
disabled={syncing}
>
Git repository
</Button>
<Button
type="button"
variant={kind === "local-directory" ? "default" : "outline"}
onClick={() => setKind("local-directory")}
disabled={syncing}
>
Directory path
</Button>
</div>
</CardStackEntryField>
)}
{kind === "git" ? (
<>
<CardStackEntryField label="Git repository" description="- Any https Git remote.">
<div className="space-y-1.5">
<Input
value={url}
onChange={(event) => {
setUrl((event.target as HTMLInputElement).value);
setFieldError(null);
setSyncError(null);
}}
onBlur={() => setFieldError(validateGitSourceUrl(url))}
placeholder="https://github.com/acme/tools.git"
className="font-mono text-sm"
aria-invalid={fieldError ? true : undefined}
/>
{fieldError && <p className="text-xs text-destructive">{fieldError}</p>}
</div>
</CardStackEntryField>
<CardStackEntryField
label="Ref (optional)"
description="- Branch, tag, or commit SHA."
>
<Input
value={ref}
onChange={(event) => setRef((event.target as HTMLInputElement).value)}
placeholder="main"
className="font-mono text-sm"
disabled={syncing}
/>
</CardStackEntryField>
<CardStackEntryField
label="Token (optional)"
description="- For private repositories. Never shown after saving."
>
<Input
value={token}
type="password"
onChange={(event) => setToken((event.target as HTMLInputElement).value)}
autoComplete="off"
className="font-mono text-sm"
disabled={syncing}
/>
</CardStackEntryField>
</>
) : (
<CardStackEntryField
label="Directory path"
description="- Absolute path on this machine."
>
<div className="space-y-1.5">
<div className="flex gap-2">
<Input
value={path}
onChange={(event) => {
setPath((event.target as HTMLInputElement).value);
setFieldError(null);
setSyncError(null);
}}
onBlur={() => {
if (!path.trim().startsWith("/")) return;
void Effect.runPromiseExit(
listCustomToolDirectoriesEffect({ path: path.trim() }),
).then((exit) => {
setPathListing(Exit.isSuccess(exit) ? exit.value : null);
});
}}
placeholder="/Users/me/tools"
className="font-mono text-sm"
aria-invalid={fieldError ? true : undefined}
/>
<Button
type="button"
variant="outline"
onClick={() => setBrowseOpen(true)}
disabled={syncing}
>
Browse
</Button>
</div>
{pathListing && <DirectorySourceStatus listing={pathListing} />}
{fieldError && <p className="text-xs text-destructive">{fieldError}</p>}
</div>
</CardStackEntryField>
)}
<CardStackEntryField
label="Source name"
description="- Lowercase letters, numbers, and hyphens."
>
<div className="space-y-1.5">
<Input
value={effectiveName}
onChange={(event) => {
setNameTouched(true);
setName(slugifyCustomToolsAppName((event.target as HTMLInputElement).value));
setNameError(null);
setSyncError(null);
}}
placeholder="custom-tools"
className="text-sm"
aria-invalid={nameError ? true : undefined}
/>
{nameError && <p className="text-xs text-destructive">{nameError}</p>}
{slugAlreadyExists && !syncing && !nameError && (
<p className="text-xs text-destructive">
An integration named "{slug}" already exists.
</p>
)}
</div>
</CardStackEntryField>
</CardStackContent>
</CardStack>
{syncError && <FormErrorAlert message={syncError} />}
<FloatActions>
<Button type="button" variant="ghost" onClick={() => props.onCancel()} disabled={syncing}>
Cancel
</Button>
<Button type="button" onClick={() => void submit()} disabled={syncing} loading={syncing}>
Sync source
</Button>
</FloatActions>
{browseOpen && (
<DirectoryBrowserModal
initialPath={path.trim() || undefined}
onClose={() => setBrowseOpen(false)}
onSelect={(selectedPath) => {
setPath(selectedPath);
setFieldError(null);
setSyncError(null);
setBrowseOpen(false);
}}
/>
)}
</div>
);
}
function DirectoryBrowserModal(props: {
readonly initialPath?: string;
readonly onSelect: (path: string) => void;
readonly onClose: () => void;
}) {
const [currentPath, setCurrentPath] = useState(props.initialPath);
const [includeHidden, setIncludeHidden] = useState(false);
const [listing, setListing] = useState<CustomToolsDirectoryListing | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let active = true;
setLoading(true);
setError(null);
void Effect.runPromiseExit(
listCustomToolDirectoriesEffect({ path: currentPath, includeHidden }),
).then((exit) => {
if (!active) return;
setLoading(false);
if (Exit.isFailure(exit)) {
setError("Unable to read this directory.");
return;
}
setListing(exit.value);
setCurrentPath(exit.value.path);
});
return () => {
active = false;
};
}, [currentPath, includeHidden]);
const rows = listing ? directoryBrowserRows(listing) : [];
const selectedPath = listing?.path ?? currentPath ?? "";
const canSelect = Boolean(selectedPath) && !loading && !error;
const hasToolFiles = (listing?.source.toolFiles.length ?? 0) > 0;
return (
<Dialog open onOpenChange={(open) => (open ? undefined : props.onClose())}>
<DialogContent className="sm:max-w-[620px]">
<DialogHeader className="border-b border-border/60 pb-4">
<DialogTitle className="text-base">Choose directory</DialogTitle>
<DialogDescription className="font-mono text-xs break-all">
{selectedPath || "Home"}
</DialogDescription>
{listing && <DirectorySourceStatus listing={listing} />}
</DialogHeader>
<div className="space-y-3">
<Label className="flex items-center gap-2 text-sm text-muted-foreground">
<Checkbox
checked={includeHidden}
onCheckedChange={(checked) => setIncludeHidden(checked === true)}
/>
Show hidden directories
</Label>
<div className="max-h-[340px] overflow-auto rounded-md border border-border">
{loading ? (
<div className="px-3 py-8 text-center text-sm text-muted-foreground">Loading...</div>
) : error ? (
<div className="px-3 py-8 text-center text-sm text-destructive">{error}</div>
) : rows.length === 0 ? (
<div className="px-3 py-8 text-center text-sm text-muted-foreground">
No subdirectories
</div>
) : (
<div role="listbox" aria-label="Directories" className="divide-y divide-border">
{rows.map((row) => (
<Button
key={`${row.kind}:${row.path}`}
type="button"
variant="ghost"
className="h-auto w-full justify-between gap-3 rounded-none px-3 py-2 text-left text-sm font-normal hover:bg-accent focus-visible:bg-accent focus-visible:outline-none"
onClick={() => setCurrentPath(row.path)}
>
<span className="min-w-0 truncate font-mono">
{row.kind === "parent" ? ".." : row.name}
</span>
<span className="flex shrink-0 items-center gap-2">
{row.kind === "dir" && row.hasTools && (
<span className="rounded-sm border border-border bg-secondary px-1.5 py-0.5 font-mono text-[11px] text-muted-foreground">
tools
</span>
)}
{row.kind === "dir" && row.isSymlink && (
<span className="font-mono text-[11px] text-muted-foreground">symlink</span>
)}
</span>
</Button>
))}
</div>
)}
</div>
</div>
<DialogFooter className="border-t border-border/60 pt-4">
<Button type="button" variant="ghost" onClick={props.onClose}>
Cancel
</Button>
<Button type="button" onClick={() => props.onSelect(selectedPath)} disabled={!canSelect}>
{hasToolFiles ? "Select" : "Select anyway"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
function DirectorySourceStatus(props: { readonly listing: CustomToolsDirectoryListing }) {
const verdict = directorySourceVerdict(props.listing);
if (verdict.type === "valid") {
return (
<p className="mt-2 text-xs text-foreground">
{verdict.message}:{" "}
{verdict.visibleTools.map((tool, index) => (
<span key={tool}>
{index > 0 ? ", " : ""}
<span className="font-mono">{tool}</span>
</span>
))}
{verdict.moreCount > 0 && (
<>
{", "}
<span className="font-mono">+{verdict.moreCount} more</span>
</>
)}
</p>
);
}
return <p className="mt-2 text-xs text-muted-foreground">{verdict.message}</p>;
}