daat-locus 0.4.0

A long-running local agent runtime with memory, workflows, apps, and sleep-time self-improvement.
import {
  ArrowLeftIcon,
  FileJsonIcon,
  FileTextIcon,
  FolderIcon,
  FolderOpenIcon,
  HardDriveIcon,
  LoaderCircleIcon,
} from "lucide-react";
import { useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";

import {
  Breadcrumb,
  BreadcrumbItem,
  BreadcrumbList,
  BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import { Button } from "@/components/ui/button";
import {
  Dialog,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Separator } from "@/components/ui/separator";
import { listDirs, type DirListResponse } from "@/lib/daemon-api";

function joinPath(base: string, name: string) {
  if (!base) return name;
  const separator = base.includes("\\") ? "\\" : "/";
  return `${base.replace(/[\\/]+$/, "")}${separator}${name}`;
}

function pathSegments(p: string): { label: string; full: string }[] {
  if (!p) return [];
  const parts = p.replace(/\\/g, "/").split("/").filter(Boolean);
  const segs: { label: string; full: string }[] = [];
  const separator = p.includes("\\") ? "\\" : "/";
  for (let i = 0; i < parts.length; i++) {
    const full =
      p.match(/^[A-Z]:/i) && i === 0
        ? parts[0] + "\\"
        : segs[i - 1]?.full.replace(/[\\/]+$/, "") + separator + parts[i];
    segs.push({ label: parts[i], full });
  }
  return segs;
}

export type BrowserDialogMode = "directory" | "file";

export function DirBrowserDialog({
  open,
  onOpenChange,
  onSelect,
  mode = "directory",
}: {
  open: boolean;
  onOpenChange: (open: boolean) => void;
  onSelect: (path: string) => void;
  mode?: BrowserDialogMode;
}) {
  const { t } = useTranslation();
  const [currentPath, setCurrentPath] = useState("");
  const [data, setData] = useState<DirListResponse | null>(null);
  const [loading, setLoading] = useState(false);
  const [selectedFile, setSelectedFile] = useState<string | null>(null);
  const [error, setError] = useState<string | null>(null);
  const [inputValue, setInputValue] = useState("");
  const abortRef = useRef<AbortController | null>(null);

  const firstLoadRef = useRef(false);

  const fetchDir = useCallback(async (path?: string) => {
    abortRef.current?.abort();
    const controller = new AbortController();
    abortRef.current = controller;
    setLoading(true);
    setError(null);
    try {
      const result = await listDirs({
        path,
        includeFiles: mode === "file",
        signal: controller.signal,
      });
      if (!controller.signal.aborted) {
        setData(result);
        setCurrentPath(result.path);
        setInputValue(result.path);
        setSelectedFile(null);
      }
    } catch (err) {
      if (!controller.signal.aborted) {
        setError(err instanceof Error ? err.message : String(err));
      }
    } finally {
      if (!controller.signal.aborted) {
        setLoading(false);
      }
    }
  }, [mode]);

  useEffect(() => {
    if (open && !firstLoadRef.current) {
      firstLoadRef.current = true;
      fetchDir();
    }
    if (!open) {
      firstLoadRef.current = false;
    }
  }, [open, fetchDir]);

  const segs = pathSegments(currentPath);

  return (
    <Dialog open={open} onOpenChange={onOpenChange}>
      <DialogContent className="max-w-lg">
        <DialogHeader>
          <DialogTitle>
            {mode === "file" ? t("dirBrowser.fileTitle") : t("dirBrowser.title")}
          </DialogTitle>
          <DialogDescription>
            {mode === "file"
              ? t("dirBrowser.fileDescription")
              : t("dirBrowser.description")}
          </DialogDescription>
        </DialogHeader>

        <div className="flex flex-col gap-3">
          {/* Breadcrumb */}
          <Breadcrumb>
            <BreadcrumbList>
              <BreadcrumbItem>
                <Button
                  variant="link"
                  size="sm"
                  className="h-auto p-0"
                  onClick={() => fetchDir()}
                >
                  <HardDriveIcon />
                  <span className="sr-only">{t("dirBrowser.root")}</span>
                </Button>
              </BreadcrumbItem>
              {segs.length > 0 && <BreadcrumbSeparator />}
              {segs.map((seg, i) => (
                <BreadcrumbItem key={seg.full}>
                  {i < segs.length - 1 ? (
                    <>
                      <Button
                        variant="link"
                        size="sm"
                        className="h-auto p-0"
                        onClick={() => fetchDir(seg.full)}
                      >
                        {seg.label}
                      </Button>
                      <BreadcrumbSeparator />
                    </>
                  ) : (
                    <span className="text-sm font-medium">{seg.label}</span>
                  )}
                </BreadcrumbItem>
              ))}
            </BreadcrumbList>
          </Breadcrumb>

          {/* Input */}
          <div className="flex gap-2">
            <Input
              placeholder={t("dirBrowser.pathPlaceholder")}
              value={inputValue}
              onChange={(e) => setInputValue(e.target.value)}
              onKeyDown={(e) => {
                if (e.key === "Enter") fetchDir(inputValue || undefined);
              }}
            />
            <Button
              type="button"
              variant="secondary"
              size="sm"
              onClick={() => fetchDir(inputValue || undefined)}
              disabled={loading}
            >
              {t("dirBrowser.go")}
            </Button>
          </div>
        </div>

        <Separator />

        {/* Directory list */}
        <ScrollArea className="h-52 rounded-md border">
          {loading && (
            <div className="flex items-center justify-center h-full gap-2 text-muted-foreground">
              <LoaderCircleIcon className="animate-spin" />
              <span className="text-sm">{t("dirBrowser.loading")}</span>
            </div>
          )}
          {error && !loading && (
            <div className="flex items-center justify-center h-full">
              <span className="text-sm text-destructive">{error}</span>
            </div>
          )}
          {!loading && !error && data && (
            <div className="flex flex-col">
              {data.parent != null && (
                <Button
                  variant="ghost"
                  className="justify-start rounded-none"
                  onClick={() => fetchDir(data.parent ?? undefined)}
                >
                  <ArrowLeftIcon />
                  <span>..</span>
                </Button>
              )}
              {data.entries.length === 0 && (
                <div className="flex items-center justify-center h-16">
                  <span className="text-sm text-muted-foreground">
                    {t("dirBrowser.noDirs")}
                  </span>
                </div>
              )}
              {data.entries.map((entry) => {
                const entryPath = joinPath(currentPath, entry.name);
                const isFile = entry.kind === "file";
                return (
                  <Button
                    key={entry.name}
                    type="button"
                    variant={selectedFile === entryPath ? "secondary" : "ghost"}
                    className="justify-start rounded-none"
                    onClick={() => {
                      if (isFile) {
                        setSelectedFile(entryPath);
                      } else {
                        fetchDir(entryPath);
                      }
                    }}
                  >
                    {isFile ? (
                      entry.name.toLowerCase().endsWith(".json") ? (
                        <FileJsonIcon data-icon="inline-start" />
                      ) : (
                        <FileTextIcon data-icon="inline-start" />
                      )
                    ) : (
                      <FolderIcon data-icon="inline-start" />
                    )}
                    <span className="truncate">{entry.name}</span>
                  </Button>
                );
              })}
            </div>
          )}
        </ScrollArea>

        <DialogFooter>
          <DialogClose asChild>
            <Button type="button" variant="outline">
              {t("dirBrowser.cancel")}
            </Button>
          </DialogClose>
          <Button
            type="button"
            onClick={() => {
              const selected = mode === "file" ? selectedFile : inputValue || currentPath;
              if (selected) onSelect(selected);
            }}
            disabled={mode === "file" ? !selectedFile : !inputValue && !currentPath}
          >
            <FolderOpenIcon data-icon="inline-start" />
            {mode === "file" ? t("dirBrowser.selectFile") : t("dirBrowser.select")}
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}