Ghost Shell

JavaScript NOASSERTION

Stars
0
Forks
0
Downloads
N/A
Open Issues
0
Files main

Repository Files

Loading file structure...
src/pages/sftp-tab.jsx
import React from "react";
import DashboardLayout from "@/layouts/dashboard";
import { Channel } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { openPath } from "@tauri-apps/plugin-opener";
import { invoke } from "@/lib/tauri";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
  Dialog,
  DialogContent,
  DialogHeader,
  DialogFooter,
  DialogTitle,
  DialogDescription,
} from "@/components/ui/dialog";
import { cn } from "@/lib/utils";
import {
  Folder,
  FolderOpen,
  File,
  FilePlus,
  ArrowLeft,
  Search,
  RefreshCw,
  FolderPlus,
  Trash2,
  Pencil,
  Loader2,
  Lock,
  X,
  Check,
  CheckSquare,
  Square,
  Eye,
  EyeOff,
  ChevronLeft,
  ChevronRight,
  HardDrive,
  Copy,
  Info,
  Network,
  Download,
  AlertTriangle,
} from "lucide-react";
import { ButtonGroup } from "@/components/ui/button-group";
import {
  InputGroup,
  InputGroupAddon,
  InputGroupInput,
} from "@/components/ui/input-group";

const TRANSFER_DISMISS_MS = 3500;

// Persistent SFTP Tab State to keep connections alive on React unmount (tab changes)
const sftpGlobalState = {
  pane1: null,
  pane2: null,
  transfers: [],
};

// Simple memory cache for directory listings (stale-while-revalidate)
const sftpDirectoryCache = new Map();

const SFTP_DRAG_MIME = "application/x-ghost-shell-sftp";

function joinRemotePath(dir, name) {
  if (!dir || dir === ".") {
    return name.startsWith("/") ? name : `/${name}`;
  }
  if (dir === "/") return `/${name}`;
  return `${dir.replace(/\/+$/, "")}/${name}`;
}

function readDragPayload(event, fallbackRef) {
  if (fallbackRef?.current) {
    return fallbackRef.current;
  }

  const candidates = [SFTP_DRAG_MIME, "text/plain", "text"];
  for (const type of candidates) {
    try {
      const raw = event.dataTransfer.getData(type);
      if (raw) return JSON.parse(raw);
    } catch {
      /* try next type */
    }
  }

  try {
    for (const type of event.dataTransfer.types || []) {
      const raw = event.dataTransfer.getData(type);
      if (raw?.includes("fromPane")) return JSON.parse(raw);
    }
  } catch {
    /* ignore malformed payloads */
  }

  return null;
}

const POINTER_DRAG_THRESHOLD = 6;

export default function SftpTab() {
  const dragPayloadRef = React.useRef(null);
  const pane1Ref = React.useRef(null);
  const pane2Ref = React.useRef(null);
  const pointerDragSessionRef = React.useRef(null);
  const dropTargetPaneRef = React.useRef(null);
  const suppressRowClickRef = React.useRef(false);
  const executePaneTransferRef = React.useRef(null);
  const activeTransferKeysRef = React.useRef(new Set());
  const transferDismissTimersRef = React.useRef(new Map());
  // Hosts database
  const [hosts, setHosts] = React.useState([]);
  const [searchHostQuery, setSearchHostQuery] = React.useState("");

  // Pane States initialized from global cache if available
  const [pane1, setPane1] = React.useState(
    () =>
      sftpGlobalState.pane1 || {
        id: "pane-1",
        host: null,
        showSelectHostList: false,
        path: "",
        history: [],
        historyIndex: 0,
        files: [],
        loading: false,
        error: "",
        filter: "",
        showHidden: false,
        selected: new Set(),
      },
  );

  const [pane2, setPane2] = React.useState(
    () =>
      sftpGlobalState.pane2 || {
        id: "pane-2",
        host: null,
        showSelectHostList: false,
        path: "",
        history: [],
        historyIndex: 0,
        files: [],
        loading: false,
        error: "",
        filter: "",
        showHidden: false,
        selected: new Set(),
      },
  );

  // Prompt / Credentials State
  const [authPrompt, setAuthPrompt] = React.useState({
    visible: false,
    paneId: "",
    host: null,
    type: "password", // "password" | "passphrase"
    value: "",
  });

  // Right-Click Context Menu State
  const menuRef = React.useRef(null);
  const [contextMenu, setContextMenu] = React.useState({
    visible: false,
    x: 0,
    y: 0,
    paneId: "",
    file: null,
  });

  // Dialog state — replaces native prompt()/confirm()/alert() which are
  // unreliable inside Tauri's WebView. One of:
  // "rename" | "newFolder" | "newFile" | "delete" | "alert"
  const [dialog, setDialog] = React.useState({
    type: null,
    paneId: "",
    file: null,
    value: "",
    title: "",
    message: "",
  });

  const closeDialog = React.useCallback(
    () => setDialog((prev) => ({ ...prev, type: null })),
    [],
  );

  const showAlert = React.useCallback((title, message) => {
    setDialog({
      type: "alert",
      paneId: "",
      file: null,
      value: "",
      title,
      message,
    });
  }, []);

  // Active transfers initialized from global cache if available
  const [transfers, setTransfers] = React.useState(
    () => sftpGlobalState.transfers || [],
  );
  const [dropTargetPaneId, setDropTargetPaneId] = React.useState(null);
  const [pointerDrag, setPointerDrag] = React.useState(null);

  pane1Ref.current = pane1;
  pane2Ref.current = pane2;

  const getPaneById = React.useCallback(
    (paneId) => (paneId === "pane-1" ? pane1Ref.current : pane2Ref.current),
    [],
  );

  const scheduleTransferRemoval = React.useCallback((transferId) => {
    const existing = transferDismissTimersRef.current.get(transferId);
    if (existing) window.clearTimeout(existing);

    const timer = window.setTimeout(() => {
      transferDismissTimersRef.current.delete(transferId);
      setTransfers((prev) => prev.filter((t) => t.id !== transferId));
    }, TRANSFER_DISMISS_MS);

    transferDismissTimersRef.current.set(transferId, timer);
  }, []);

  React.useEffect(() => {
    return () => {
      transferDismissTimersRef.current.forEach((timer) =>
        window.clearTimeout(timer),
      );
      transferDismissTimersRef.current.clear();
    };
  }, []);

  const finishTransfer = React.useCallback(
    (transferId, transferKey, status, error) => {
      if (transferKey) activeTransferKeysRef.current.delete(transferKey);
      setTransfers((prev) =>
        prev.map((t) =>
          t.id === transferId
            ? {
                ...t,
                status,
                percentage: status === "completed" ? 100 : t.percentage,
                error: error || t.error,
              }
            : t,
        ),
      );
      scheduleTransferRemoval(transferId);
    },
    [scheduleTransferRemoval],
  );

  const beginTransfer = React.useCallback((transferKey, transfer) => {
    if (transferKey && activeTransferKeysRef.current.has(transferKey)) {
      return null;
    }
    if (transferKey) activeTransferKeysRef.current.add(transferKey);
    setTransfers((prev) => [...prev, transfer]);
    return transfer.id;
  }, []);

  // Synchronize state updates to global state so they survive tab changes
  React.useEffect(() => {
    sftpGlobalState.pane1 = pane1;
  }, [pane1]);

  React.useEffect(() => {
    sftpGlobalState.pane2 = pane2;
  }, [pane2]);

  React.useEffect(() => {
    sftpGlobalState.transfers = transfers;
  }, [transfers]);

  // Fetch hosts list on load
  const loadHosts = React.useCallback(async () => {
    try {
      const res = await invoke("get_hosts");
      setHosts(res || []);
    } catch (err) {
      console.error("Failed to load hosts:", err);
    }
  }, []);

  React.useEffect(() => {
    loadHosts();
  }, [loadHosts]);

  // Load directory files for a specific pane
  const loadDir = React.useCallback(async (paneId, pathStr) => {
    const setPane = paneId === "pane-1" ? setPane1 : setPane2;
    const cacheKey = `${paneId}:${pathStr}`;
    const cached = sftpDirectoryCache.get(cacheKey);

    if (cached) {
      // Instantly load cached files (0ms UI latency)
      setPane((prev) => ({
        ...prev,
        files: cached.files,
        path: pathStr,
        loading: false,
        error: "",
      }));
    } else {
      // Show full-pane spinner overlay only if no cache is found
      setPane((prev) => ({ ...prev, loading: true, error: "" }));
    }

    try {
      const files = await invoke("sftp_list_dir", {
        connectionId: paneId,
        path: pathStr,
      });

      // Update cache
      sftpDirectoryCache.set(cacheKey, {
        files: files || [],
        timestamp: Date.now(),
      });

      setPane((prev) => ({
        ...prev,
        files: files || [],
        path: pathStr,
        loading: false,
        selected: new Set(),
        error: "",
      }));
    } catch (err) {
      const hasCache = sftpDirectoryCache.has(cacheKey);
      setPane((prev) => ({
        ...prev,
        loading: false,
        ...(hasCache ? {} : { error: String(err) }),
      }));
      if (hasCache) {
        console.warn(
          `Background directory refresh failed for ${pathStr}:`,
          err,
        );
      }
    }
  }, []);

  // Connect to a host
  const connectHost = async (paneId, host, overrideCredentials = null) => {
    const setPane = paneId === "pane-1" ? setPane1 : setPane2;

    setPane((prev) => ({ ...prev, loading: true, error: "" }));

    try {
      const homePath = await invoke("sftp_connect", {
        connectionId: paneId,
        hostId: host.id,
        password: overrideCredentials?.password || null,
        passphrase: overrideCredentials?.passphrase || null,
      });

      // Clear any prompts
      setAuthPrompt({
        visible: false,
        paneId: "",
        host: null,
        type: "password",
        value: "",
      });

      // Initialize path to remote home directory
      setPane((prev) => ({
        ...prev,
        host,
        path: homePath,
        history: [homePath],
        historyIndex: 0,
        selected: new Set(),
        showSelectHostList: false,
      }));

      loadDir(paneId, homePath);
    } catch (err) {
      const errMsg = String(err);
      if (errMsg.includes("NeedPassword")) {
        setAuthPrompt({
          visible: true,
          paneId,
          host,
          type: "password",
          value: "",
        });
        setPane((prev) => ({ ...prev, loading: false }));
      } else if (errMsg.includes("NeedPassphrase")) {
        setAuthPrompt({
          visible: true,
          paneId,
          host,
          type: "passphrase",
          value: "",
        });
        setPane((prev) => ({ ...prev, loading: false }));
      } else {
        setPane((prev) => ({
          ...prev,
          error: errMsg,
          loading: false,
        }));
      }
    }
  };

  const handlePromptSubmit = (e) => {
    e.preventDefault();
    if (!authPrompt.host) return;

    const creds = {};
    if (authPrompt.type === "password") {
      creds.password = authPrompt.value;
    } else {
      creds.passphrase = authPrompt.value;
    }

    connectHost(authPrompt.paneId, authPrompt.host, creds);
  };

  // Directory Navigation Helpers
  const navigateTo = (paneId, targetPath) => {
    const setPane = paneId === "pane-1" ? setPane1 : setPane2;
    const pane = paneId === "pane-1" ? pane1 : pane2;

    const newHistory = [
      ...pane.history.slice(0, pane.historyIndex + 1),
      targetPath,
    ];
    const newIndex = newHistory.length - 1;

    setPane((prev) => ({
      ...prev,
      history: newHistory,
      historyIndex: newIndex,
    }));

    loadDir(paneId, targetPath);
  };

  const goBack = (paneId) => {
    const pane = paneId === "pane-1" ? pane1 : pane2;
    const setPane = paneId === "pane-1" ? setPane1 : setPane2;

    if (pane.historyIndex > 0) {
      const newIndex = pane.historyIndex - 1;
      const targetPath = pane.history[newIndex];
      setPane((prev) => ({ ...prev, historyIndex: newIndex }));
      loadDir(paneId, targetPath);
    }
  };

  const goForward = (paneId) => {
    const pane = paneId === "pane-1" ? pane1 : pane2;
    const setPane = paneId === "pane-1" ? setPane1 : setPane2;

    if (pane.historyIndex < pane.history.length - 1) {
      const newIndex = pane.historyIndex + 1;
      const targetPath = pane.history[newIndex];
      setPane((prev) => ({ ...prev, historyIndex: newIndex }));
      loadDir(paneId, targetPath);
    }
  };

  // Drag and drop — ref + text/plain for WebView2 (Windows) / WKWebView (macOS) / WebKitGTK (Linux)
  const handleDragStart = (e, paneId, file) => {
    const payload = {
      fromPane: paneId,
      fileName: file.name,
      isDir: Boolean(file.is_dir),
    };

    dragPayloadRef.current = payload;
    const encoded = JSON.stringify(payload);

    e.dataTransfer.effectAllowed = "copy";
    e.dataTransfer.dropEffect = "copy";
    e.dataTransfer.setData("text/plain", encoded);
    e.dataTransfer.setData(SFTP_DRAG_MIME, encoded);

    if (e.dataTransfer.setDragImage && e.currentTarget instanceof HTMLElement) {
      e.dataTransfer.setDragImage(e.currentTarget, 16, 16);
    }
  };

  const handleDragEnd = () => {
    setDropTargetPaneId(null);
    window.setTimeout(() => {
      dragPayloadRef.current = null;
    }, 0);
  };

  const handleDragOver = (e, toPaneId) => {
    e.preventDefault();
    e.stopPropagation();
    if (e.dataTransfer) {
      e.dataTransfer.dropEffect = "copy";
    }
    setDropTargetPaneId(toPaneId);
  };

  const handleDragLeave = (e, paneId) => {
    e.stopPropagation();
    const next = e.relatedTarget;
    if (next instanceof Node && e.currentTarget.contains(next)) return;
    setDropTargetPaneId((prev) => (prev === paneId ? null : prev));
  };

  const startFileCopy = async (
    fromPaneId,
    fromPath,
    toPaneId,
    toPath,
    fileName,
  ) => {
    const fromPane = getPaneById(fromPaneId);
    const toPane = getPaneById(toPaneId);
    if (!fromPane?.host || !toPane?.host) return;

    const transferKey = `copy:${fromPaneId}:${fromPath}->${toPaneId}:${toPath}`;
    const transferId = Math.random().toString(36).substring(7);
    const channel = new Channel();

    const startedId = beginTransfer(transferKey, {
      id: transferId,
      kind: "copy",
      fileName,
      fromHost: fromPane.host.name || fromPane.host.address,
      toHost: toPane.host.name || toPane.host.address,
      percentage: 0,
      bytesMoved: 0,
      totalSize: 0,
      status: "running",
    });
    if (!startedId) return;

    sftpDirectoryCache.delete(`${toPaneId}:${toPane.path}`);

    channel.onmessage = (msg) => {
      setTransfers((prev) =>
        prev.map((t) =>
          t.id === transferId
            ? {
                ...t,
                percentage: msg.percentage,
                bytesMoved: msg.bytes_moved,
                totalSize: msg.total_size,
              }
            : t,
        ),
      );
    };

    try {
      await invoke("sftp_copy_file", {
        fromConnectionId: fromPaneId,
        fromPath,
        toConnectionId: toPaneId,
        toPath,
        progressChannel: channel,
      });

      finishTransfer(transferId, transferKey, "completed");
      loadDir(toPaneId, toPane.path);
    } catch (err) {
      finishTransfer(transferId, transferKey, "failed", String(err));
      showAlert("Transfer failed", String(err));
    }
  };

  const startDownload = async (paneId, remotePath, fileName, isDir) => {
    const pane = getPaneById(paneId);
    if (!pane?.host) return;

    const transferKey = `download:${paneId}:${remotePath}`;
    const transferId = Math.random().toString(36).substring(7);
    const channel = new Channel();
    const suggestedName = isDir ? `${fileName}.zip` : fileName;

    const startedId = beginTransfer(transferKey, {
      id: transferId,
      kind: "download",
      fileName,
      fromHost: pane.host.name || pane.host.address,
      toHost: "Local disk",
      percentage: 0,
      bytesMoved: 0,
      totalSize: 0,
      status: "running",
    });
    if (!startedId) return;

    channel.onmessage = (msg) => {
      setTransfers((prev) =>
        prev.map((t) =>
          t.id === transferId
            ? {
                ...t,
                percentage: msg.percentage,
                bytesMoved: msg.bytes_moved,
                totalSize: msg.total_size,
              }
            : t,
        ),
      );
    };

    try {
      await invoke("sftp_download", {
        connectionId: paneId,
        remotePath,
        isDir,
        suggestedName,
        progressChannel: channel,
      });

      finishTransfer(transferId, transferKey, "completed");
    } catch (err) {
      const message = String(err);
      finishTransfer(transferId, transferKey, "failed", message);
      if (!message.toLowerCase().includes("cancelled")) {
        showAlert("Download failed", message);
      }
    }
  };

  executePaneTransferRef.current = (fromPaneId, toPaneId, fileName, isDir) => {
    if (fromPaneId === toPaneId) return;

    const fromPane = getPaneById(fromPaneId);
    const toPane = getPaneById(toPaneId);

    if (!fromPane?.host || !toPane?.host) return;

    const fromPath = joinRemotePath(fromPane.path, fileName);
    const toPath = joinRemotePath(toPane.path, fileName);

    if (isDir) {
      showAlert(
        "Not supported",
        "Folder copying is not supported via drag and drop yet. Please copy files.",
      );
      return;
    }

    startFileCopy(fromPaneId, fromPath, toPaneId, toPath, fileName);
  };

  const executePaneTransfer = (...args) =>
    executePaneTransferRef.current?.(...args);

  const handleDrop = async (e, toPaneId) => {
    e.preventDefault();
    e.stopPropagation();
    setDropTargetPaneId(null);

    const payload = readDragPayload(e, dragPayloadRef);
    dragPayloadRef.current = null;

    if (!payload?.fromPane || !payload.fileName) return;

    executePaneTransfer(
      payload.fromPane,
      toPaneId,
      payload.fileName,
      Boolean(payload.isDir),
    );
  };

  const handleRowPointerDown = (e, paneId, file) => {
    if (e.button !== 0) return;
    pointerDragSessionRef.current = {
      paneId,
      file,
      startX: e.clientX,
      startY: e.clientY,
      active: false,
    };
  };

  React.useEffect(() => {
    const resetDragStyles = () => {
      document.body.style.cursor = "";
      document.body.style.userSelect = "";
    };

    const onMouseMove = (e) => {
      const session = pointerDragSessionRef.current;
      if (!session || e.buttons !== 1) return;

      if (!session.active) {
        const dist = Math.hypot(
          e.clientX - session.startX,
          e.clientY - session.startY,
        );
        if (dist < POINTER_DRAG_THRESHOLD) return;
        session.active = true;
        document.body.style.cursor = "grabbing";
        document.body.style.userSelect = "none";
      }

      setPointerDrag({
        fromPaneId: session.paneId,
        file: session.file,
        x: e.clientX,
        y: e.clientY,
      });

      const el = document.elementFromPoint(e.clientX, e.clientY);
      const paneEl = el?.closest("[data-sftp-pane]");
      const hoverPane = paneEl?.getAttribute("data-sftp-pane");
      const nextTarget =
        hoverPane && hoverPane !== session.paneId ? hoverPane : null;
      dropTargetPaneRef.current = nextTarget;
      setDropTargetPaneId(nextTarget);
    };

    const onMouseUp = () => {
      const session = pointerDragSessionRef.current;
      resetDragStyles();

      if (session?.active) {
        suppressRowClickRef.current = true;
        const toPaneId = dropTargetPaneRef.current;
        if (toPaneId) {
          executePaneTransferRef.current?.(
            session.paneId,
            toPaneId,
            session.file.name,
            Boolean(session.file.is_dir),
          );
        }
      }

      pointerDragSessionRef.current = null;
      dropTargetPaneRef.current = null;
      setPointerDrag(null);
      setDropTargetPaneId(null);
    };

    window.addEventListener("mousemove", onMouseMove);
    window.addEventListener("mouseup", onMouseUp);
    return () => {
      window.removeEventListener("mousemove", onMouseMove);
      window.removeEventListener("mouseup", onMouseUp);
      resetDragStyles();
    };
  }, []);

  // Context Menu Actions
  const handleContextMenu = (e, paneId, file) => {
    e.preventDefault();
    e.stopPropagation();
    setContextMenu({
      visible: true,
      x: e.clientX,
      y: e.clientY,
      paneId,
      file,
    });
  };

  // Right-click on empty space inside a connected pane → menu with no file
  // (New Folder / New File / Refresh / Select All …).
  const handlePaneContextMenu = (e, paneId) => {
    e.preventDefault();
    setContextMenu({
      visible: true,
      x: e.clientX,
      y: e.clientY,
      paneId,
      file: null,
    });
  };

  const closeContextMenu = () => {
    setContextMenu((prev) => ({ ...prev, visible: false }));
  };

  // Keep the context menu fully on-screen: clamp its position to the viewport
  // after it renders (so items near the bottom/right edge stay clickable).
  React.useLayoutEffect(() => {
    if (!contextMenu.visible || !menuRef.current) return;
    const el = menuRef.current;
    const { offsetWidth: w, offsetHeight: h } = el;
    const margin = 8;
    const left = Math.max(
      margin,
      Math.min(contextMenu.x, window.innerWidth - w - margin),
    );
    const top = Math.max(
      margin,
      Math.min(contextMenu.y, window.innerHeight - h - margin),
    );
    el.style.left = `${left}px`;
    el.style.top = `${top}px`;
  }, [contextMenu.visible, contextMenu.x, contextMenu.y, contextMenu.file]);

  React.useEffect(() => {
    const handleGlobalClick = () => closeContextMenu();
    window.addEventListener("click", handleGlobalClick);
    return () => window.removeEventListener("click", handleGlobalClick);
  }, []);

  // Listen for "edit and auto-sync" results emitted by the backend watcher.
  React.useEffect(() => {
    const unlisteners = [];

    listen("sftp://edit-synced", (event) => {
      const { connection_id: connectionId, remote_path: remotePath } =
        event.payload || {};
      const pane = getPaneById(connectionId);
      // If the saved file lives in the directory we're currently viewing, refresh it.
      if (pane?.host && remotePath) {
        const parent = remotePath.slice(0, remotePath.lastIndexOf("/")) || "/";
        if (parent === pane.path) {
          sftpDirectoryCache.delete(`${connectionId}:${pane.path}`);
          loadDir(connectionId, pane.path);
        }
      }
    }).then((un) => unlisteners.push(un));

    listen("sftp://edit-error", (event) => {
      const { name, error } = event.payload || {};
      showAlert(
        "Auto-sync failed",
        `Could not save ${name || "file"}: ${error || "unknown error"}`,
      );
    }).then((un) => unlisteners.push(un));

    return () => unlisteners.forEach((un) => un());
  }, [getPaneById, loadDir, showAlert]);

  // Open a remote file in the local system editor with live auto-sync (Termius
  // style). The backend downloads it to a temp file and re-uploads on every save.
  const startEdit = async (paneId, file) => {
    const pane = getPaneById(paneId);
    if (!pane?.host) return;

    const remotePath = joinRemotePath(pane.path, file.name);
    const transferKey = `edit:${paneId}:${remotePath}`;
    const transferId = Math.random().toString(36).substring(7);

    const startedId = beginTransfer(transferKey, {
      id: transferId,
      kind: "edit",
      fileName: file.name,
      fromHost: pane.host.name || pane.host.address,
      toHost: "Local editor",
      percentage: 100,
      bytesMoved: 0,
      totalSize: 0,
      status: "running",
    });

    try {
      const localPath = await invoke("sftp_edit_file", {
        connectionId: paneId,
        remotePath,
        fileName: file.name,
      });
      await openPath(localPath);
      if (startedId) finishTransfer(transferId, transferKey, "completed");
    } catch (err) {
      if (startedId)
        finishTransfer(transferId, transferKey, "failed", String(err));
      showAlert("Could not open file", String(err));
    }
  };

  const handleOpen = () => {
    const { paneId, file } = contextMenu;
    if (!file) return;
    const pane = paneId === "pane-1" ? pane1 : pane2;

    if (file.is_dir) {
      const targetPath =
        pane.path === "/" ? `/${file.name}` : `${pane.path}/${file.name}`;
      navigateTo(paneId, targetPath);
    } else {
      startEdit(paneId, file);
    }
    closeContextMenu();
  };

  const handleCopyTarget = () => {
    const { paneId, file } = contextMenu;
    if (!file) return;

    if (file.is_dir) {
      showAlert(
        "Not supported",
        "Folder copying between panes is not supported yet. Download the folder instead.",
      );
      closeContextMenu();
      return;
    }

    const fromPane = paneId === "pane-1" ? pane1 : pane2;
    const toPaneId = paneId === "pane-1" ? "pane-2" : "pane-1";
    const toPane = toPaneId === "pane-1" ? pane1 : pane2;

    if (!toPane.host) {
      showAlert(
        "Target not connected",
        "The target pane is not connected to a host.",
      );
      closeContextMenu();
      return;
    }

    const fromPath = joinRemotePath(fromPane.path, file.name);
    const toPath = joinRemotePath(toPane.path, file.name);

    startFileCopy(paneId, fromPath, toPaneId, toPath, file.name);
    closeContextMenu();
  };

  const handleDownload = () => {
    const { paneId, file } = contextMenu;
    if (!file) return;

    const pane = paneId === "pane-1" ? pane1 : pane2;
    const remotePath = joinRemotePath(pane.path, file.name);
    startDownload(paneId, remotePath, file.name, Boolean(file.is_dir));
    closeContextMenu();
  };

  // The handlers below open a dialog; the actual SFTP call runs on confirm.
  const handleCreateFolder = () => {
    const { paneId } = contextMenu;
    setDialog({
      type: "newFolder",
      paneId,
      file: null,
      value: "",
      title: "",
      message: "",
    });
    closeContextMenu();
  };

  const handleCreateFile = () => {
    const { paneId } = contextMenu;
    setDialog({
      type: "newFile",
      paneId,
      file: null,
      value: "",
      title: "",
      message: "",
    });
    closeContextMenu();
  };

  const handleDelete = () => {
    const { paneId, file } = contextMenu;
    if (!file) return;
    setDialog({
      type: "delete",
      paneId,
      file,
      value: "",
      title: "",
      message: "",
    });
    closeContextMenu();
  };

  const handleRename = () => {
    const { paneId, file } = contextMenu;
    if (!file) return;
    setDialog({
      type: "rename",
      paneId,
      file,
      value: file.name,
      title: "",
      message: "",
    });
    closeContextMenu();
  };

  // Confirm executors invoked from the dialog footer buttons.
  const confirmCreateFolder = async () => {
    const { paneId, value } = dialog;
    const folderName = value.trim();
    if (!folderName) return;
    const pane = getPaneById(paneId);
    closeDialog();

    sftpDirectoryCache.delete(`${paneId}:${pane.path}`);
    const targetPath = joinRemotePath(pane.path, folderName);
    try {
      await invoke("sftp_create_dir", {
        connectionId: paneId,
        path: targetPath,
      });
      loadDir(paneId, pane.path);
    } catch (err) {
      showAlert("Failed to create folder", String(err));
    }
  };

  const confirmCreateFile = async () => {
    const { paneId, value } = dialog;
    const fileName = value.trim();
    if (!fileName) return;
    const pane = getPaneById(paneId);
    closeDialog();

    sftpDirectoryCache.delete(`${paneId}:${pane.path}`);
    const targetPath = joinRemotePath(pane.path, fileName);
    try {
      await invoke("sftp_create_file", {
        connectionId: paneId,
        path: targetPath,
      });
      loadDir(paneId, pane.path);
    } catch (err) {
      showAlert("Failed to create file", String(err));
    }
  };

  const confirmDelete = async () => {
    const { paneId, file } = dialog;
    if (!file) return;
    const pane = getPaneById(paneId);
    closeDialog();

    sftpDirectoryCache.delete(`${paneId}:${pane.path}`);
    const targetPath = joinRemotePath(pane.path, file.name);
    try {
      await invoke("sftp_delete", {
        connectionId: paneId,
        path: targetPath,
        isDir: file.is_dir,
      });
      loadDir(paneId, pane.path);
    } catch (err) {
      showAlert("Failed to delete", String(err));
    }
  };

  const confirmRename = async () => {
    const { paneId, file, value } = dialog;
    if (!file) return;
    const newName = value.trim();
    const pane = getPaneById(paneId);

    if (!newName || newName === file.name) {
      closeDialog();
      return;
    }
    closeDialog();

    sftpDirectoryCache.delete(`${paneId}:${pane.path}`);
    const srcPath = joinRemotePath(pane.path, file.name);
    const destPath = joinRemotePath(pane.path, newName);
    try {
      await invoke("sftp_rename", {
        connectionId: paneId,
        src: srcPath,
        dest: destPath,
      });
      loadDir(paneId, pane.path);
    } catch (err) {
      showAlert("Failed to rename", String(err));
    }
  };

  // Helper formatting size bytes
  const formatBytes = (bytes) => {
    if (bytes === 0) return "--";
    const k = 1024;
    const sizes = ["B", "KB", "MB", "GB", "TB"];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
  };

  const formatUnixTime = (secs) => {
    if (!secs) return "--";
    return new Date(secs * 1000).toLocaleString();
  };

  // Render selection screen or file view for a pane
  const renderPane = (paneId) => {
    const pane = paneId === "pane-1" ? pane1 : pane2;
    const setPane = paneId === "pane-1" ? setPane1 : setPane2;
    const activeHost = pane.host;

    // 1. Connection/Loading Screen
    if (pane.loading && !activeHost) {
      return (
        <div className="flex flex-col items-center justify-center h-full bg-background text-foreground p-6 select-none animate-in fade-in duration-200">
          <Loader2 className="animate-spin size-9 text-primary mb-3" />
          <h3 className="text-xs font-semibold">Connecting to SFTP Host...</h3>
          <p className="text-[10px] text-muted-foreground">
            Establishing secure subsystem tunnel
          </p>
        </div>
      );
    }

    // 2. Unconnected Initial Placeholder
    if (!activeHost && !pane.showSelectHostList) {
      return (
        <div className="flex flex-col items-center justify-center h-full text-foreground p-6 select-none text-center animate-in fade-in duration-200">
          <div className="w-14 h-14 rounded-full bg-primary/10 border border-primary/20 flex items-center justify-center mb-4 text-primary">
            <Network className="size-6" />
          </div>
          <h3 className="text-sm font-bold text-foreground">Connect to host</h3>
          <p className="text-xs text-muted-foreground max-w-xs mt-1.5 leading-relaxed">
            Start by connecting to a saved host to manage your files with SFTP.
          </p>
          <Button
            onClick={() =>
              setPane((prev) => ({ ...prev, showSelectHostList: true }))
            }
            className="mt-5 cursor-pointer"
          >
            Select host
          </Button>
        </div>
      );
    }

    // 3. Select Host List view
    if (!activeHost && pane.showSelectHostList) {
      const filteredHosts = hosts.filter(
        (h) =>
          h.name.toLowerCase().includes(searchHostQuery.toLowerCase()) ||
          h.address.toLowerCase().includes(searchHostQuery.toLowerCase()),
      );

      return (
        <div className="flex flex-col h-full bg-background border border-border rounded-lg p-5 text-foreground select-none animate-in fade-in duration-200">
          <div className="flex items-center justify-between mb-5">
            <div className="flex items-center gap-2">
              <Button
                variant="ghost"
                size="icon-sm"
                className="cursor-pointer"
                onClick={() =>
                  setPane((prev) => ({ ...prev, showSelectHostList: false }))
                }
              >
                <ArrowLeft className="size-4" />
              </Button>
              <div>
                <h2 className="text-sm font-semibold">Select Host</h2>
                <span className="text-xs text-muted-foreground">
                  Choose a saved connection
                </span>
              </div>
            </div>
            <div className="relative w-44">
              <Search className="absolute left-2.5 top-2.5 size-3.5 text-muted-foreground" />
              <Input
                placeholder="Search hosts..."
                className="pl-8 h-8 text-xs bg-sidebar"
                value={searchHostQuery}
                onChange={(e) => setSearchHostQuery(e.target.value)}
              />
            </div>
          </div>

          <div className="flex-1 overflow-y-auto space-y-2 pr-1">
            {filteredHosts.length === 0 ? (
              <div className="flex flex-col items-center justify-center h-48 border border-dashed rounded-lg border-border text-muted-foreground p-4">
                <HardDrive className="size-7 mb-2 opacity-50 text-primary" />
                <p className="text-xs font-semibold">No hosts found</p>
                <p className="text-xs">Add hosts from the Hosts page first.</p>
              </div>
            ) : (
              filteredHosts.map((h) => (
                <div
                  key={h.id}
                  onClick={() => connectHost(paneId, h)}
                  className="flex items-center gap-3 p-3 bg-sidebar hover:bg-muted/40 border border-border rounded-lg cursor-pointer transition-colors group"
                >
                  <div className="size-9 rounded-md bg-primary/10 border border-primary/20 flex items-center justify-center text-primary font-bold text-xs shrink-0">
                    {h.os?.toLowerCase().includes("ubuntu") ? "U" : "S"}
                  </div>
                  <div className="flex-1 min-w-0">
                    <h4 className="text-xs font-semibold text-foreground group-hover:text-primary transition-colors truncate">
                      {h.name || h.address}
                    </h4>
                    <p className="text-xs text-muted-foreground truncate">
                      {h.username}@{h.address}:{h.port}
                    </p>
                  </div>
                  <ChevronRight className="size-3.5 text-muted-foreground group-hover:translate-x-0.5 transition-transform" />
                </div>
              ))
            )}
          </div>
        </div>
      );
    }

    // 4. Connected SFTP Directory list view
    const filteredFiles = pane.files.filter((f) => {
      if (!pane.showHidden && f.name.startsWith(".")) return false;
      return f.name.toLowerCase().includes(pane.filter.toLowerCase());
    });

    return (
      <div
        data-sftp-pane={paneId}
        className={cn(
          "flex flex-col h-full bg-background text-foreground select-none relative animate-in fade-in duration-200",
          dropTargetPaneId === paneId && "ring-2 ring-inset ring-primary/40",
        )}
        onDragOver={(e) => handleDragOver(e, paneId)}
        onDragLeave={(e) => handleDragLeave(e, paneId)}
        onDrop={(e) => handleDrop(e, paneId)}
      >
        {dropTargetPaneId === paneId && (
          <div className="pointer-events-none absolute inset-0 z-20 bg-primary/5 border-2 border-dashed border-primary/30 flex items-center justify-center">
            <span className="text-xs font-medium text-primary bg-background/90 px-3 py-1 rounded-md border border-primary/20">
              Drop to copy here
            </span>
          </div>
        )}

        {/* Full-Pane Loading Screen Overlay */}
        {pane.loading && pane.files.length === 0 && (
          <div className="absolute inset-0 bg-background/70 backdrop-blur-[0.5px] flex flex-col items-center justify-center z-30 pointer-events-auto">
            <Loader2 className="animate-spin size-8 text-primary mb-2" />
            <span className="text-xs text-muted-foreground font-medium">
              Loading folder...
            </span>
          </div>
        )}

        {/* Navigation / Control Header */}
        <div className="flex items-center justify-between p-3 border-b border-border bg-sidebar shrink-0">
          <div className="flex items-center gap-2 flex-1 min-w-0">
            {/* Back / Forward History */}
            <ButtonGroup>
              <Button
                variant="outline"
                size="icon-xs"
                disabled={pane.historyIndex === 0}
                onClick={() => goBack(paneId)}
              >
                <ChevronLeft className="size-3.5" />
              </Button>
              <Button
                variant="outline"
                size="icon-xs"
                disabled={pane.historyIndex === pane.history.length - 1}
                onClick={() => goForward(paneId)}
              >
                <ChevronRight className="size-3.5" />
              </Button>
            </ButtonGroup>

            <InputGroup className="max-h-6">
              <InputGroupAddon>
                <Folder className="size-3.5 text-primary shrink-0" />
              </InputGroupAddon>
              <InputGroupInput
                className="text-xs!"
                value={pane.path}
                onChange={(e) => setPane({ ...pane, path: e.target.value })}
                onKeyDown={(e) => {
                  if (e.key === "Enter") {
                    navigateTo(paneId, pane.path);
                  }
                }}
              />
            </InputGroup>
          </div>

          {/* Filtering and Actions */}
          <div className="flex items-center gap-2 ml-2">
            <ButtonGroup>
              <InputGroup className="max-h-6">
                <InputGroupAddon>
                  <RefreshCw className="size-3.5" />
                </InputGroupAddon>
                <InputGroupInput
                  placeholder="Filter..."
                  className="text-xs w-30 placeholder:text-xs"
                  value={pane.filter}
                  onChange={(e) => setPane({ ...pane, filter: e.target.value })}
                />
              </InputGroup>
              <Button
                size="icon-xs"
                className="cursor-pointer border-y border-primary"
                onClick={() => loadDir(paneId, pane.path)}
                disabled={pane.loading}
              >
                <RefreshCw
                  className={`size-3.5 ${pane.loading ? "animate-spin" : ""}`}
                />
              </Button>
              <Button
                variant="destructive"
                size="icon-xs"
                className="cursor-pointer border-destructive/40"
                onClick={() => {
                  invoke("sftp_disconnect", { connectionId: paneId });
                  setPane((prev) => ({ ...prev, host: null, files: [] }));
                }}
              >
                <X className="size-3.5" />
              </Button>
            </ButtonGroup>
          </div>
        </div>

        {/* Directory Listing table */}
        <div
          className="flex-1 overflow-auto min-h-0 relative"
          onDragOver={(e) => handleDragOver(e, paneId)}
          onDrop={(e) => handleDrop(e, paneId)}
          onContextMenu={(e) => handlePaneContextMenu(e, paneId)}
        >
          {pane.error ? (
            <div className="p-4 bg-destructive/10 border border-destructive/20 text-destructive text-xs m-3 rounded-lg flex flex-col gap-2 leading-relaxed">
              <span className="font-semibold text-sm">Connection Error</span>
              <span>{pane.error}</span>
              <Button
                variant="destructive"
                size="sm"
                className="w-fit cursor-pointer"
                onClick={() => loadDir(paneId, pane.path)}
              >
                Retry
              </Button>
            </div>
          ) : (
            <table className="w-full text-left text-xs border-collapse">
              <thead className="bg-muted text-muted-foreground uppercase text-[10px] font-semibold sticky top-0 z-20 border-b border-border">
                <tr>
                  <th className="py-2.5 px-4">Name</th>
                  <th className="py-2.5 px-4">Date Modified</th>
                  <th className="py-2.5 px-4">Size</th>
                  <th className="py-2.5 px-4 w-20">Kind</th>
                </tr>
              </thead>
              <tbody>
                {/* Parent directory navigate dot row */}
                {pane.path !== "/" && pane.path !== "" && (
                  <tr
                    onDoubleClick={() => {
                      const parts = pane.path.split("/").filter(Boolean);
                      parts.pop();
                      const parent = "/" + parts.join("/");
                      navigateTo(paneId, parent);
                    }}
                    className="border-b border-border/10 hover:bg-sidebar/35 cursor-pointer text-muted-foreground"
                  >
                    <td className="py-3 px-4 flex items-center gap-2 font-semibold">
                      <Folder className="size-4 text-blue-400 shrink-0" />
                      <span>..</span>
                    </td>
                    <td className="py-3 px-4">--</td>
                    <td className="py-3 px-4">--</td>
                    <td className="py-3 px-4 text-muted-foreground/60">
                      parent
                    </td>
                  </tr>
                )}

                {filteredFiles.map((f) => {
                  const isSelected = pane.selected.has(f.name);
                  return (
                    <tr
                      key={f.name}
                      onMouseDown={(e) => handleRowPointerDown(e, paneId, f)}
                      onContextMenu={(e) => handleContextMenu(e, paneId, f)}
                      onDoubleClick={() => {
                        if (f.is_dir) {
                          const target =
                            pane.path === "/"
                              ? `/${f.name}`
                              : `${pane.path}/${f.name}`;
                          navigateTo(paneId, target);
                        }
                      }}
                      onClick={() => {
                        if (suppressRowClickRef.current) {
                          suppressRowClickRef.current = false;
                          return;
                        }
                        const nextSelected = new Set(pane.selected);
                        if (nextSelected.has(f.name)) {
                          nextSelected.delete(f.name);
                        } else {
                          nextSelected.add(f.name);
                        }
                        setPane((prev) => ({
                          ...prev,
                          selected: nextSelected,
                        }));
                      }}
                      className={cn(
                        "border-b border-border/50 hover:bg-muted/30 cursor-grab active:cursor-grabbing transition-colors",
                        isSelected && "bg-primary/10",
                      )}
                    >
                      <td className="py-2.5 px-4 font-medium">
                        <div className="flex items-center gap-3">
                          {f.is_dir ? (
                            <Folder className="size-4.5 text-primary shrink-0" />
                          ) : (
                            <File className="size-4.5 text-blue-500 shrink-0" />
                          )}
                          <div className="flex flex-col min-w-0">
                            <span className="text-xs font-semibold truncate text-foreground leading-normal">
                              {f.name}
                            </span>
                            <span className="text-[9px] text-muted-foreground/80 font-mono leading-none pt-0.5">
                              {f.permissions}
                            </span>
                          </div>
                        </div>
                      </td>
                      <td className="py-2.5 px-4 text-muted-foreground font-mono">
                        {formatUnixTime(f.modified)}
                      </td>
                      <td className="py-2.5 px-4 text-muted-foreground font-mono">
                        {f.is_dir ? "--" : formatBytes(f.size)}
                      </td>
                      <td className="py-2.5 px-4 text-muted-foreground/60">
                        {f.is_dir ? "folder" : "file"}
                      </td>
                    </tr>
                  );
                })}

                {filteredFiles.length === 0 && (
                  <tr>
                    <td
                      colSpan={4}
                      className="text-center py-8 text-muted-foreground text-xs"
                    >
                      Directory is empty
                    </td>
                  </tr>
                )}
              </tbody>
            </table>
          )}
        </div>
      </div>
    );
  };

  return (
    <DashboardLayout
      sidebar={false}
      className="p-0 flex flex-col h-full min-h-0 select-none overflow-hidden"
    >
      {pointerDrag && (
        <div
          className="fixed z-[100] pointer-events-none flex items-center gap-2 rounded-md border border-primary/30 bg-popover px-2.5 py-1.5 text-xs font-medium text-foreground shadow-lg"
          style={{ left: pointerDrag.x + 14, top: pointerDrag.y + 14 }}
        >
          {pointerDrag.file.is_dir ? (
            <Folder className="size-3.5 text-blue-400 shrink-0" />
          ) : (
            <File className="size-3.5 text-muted-foreground shrink-0" />
          )}
          <span className="max-w-[220px] truncate">
            {pointerDrag.file.name}
          </span>
        </div>
      )}

      <div className="flex-1 grid grid-cols-1 md:grid-cols-2 h-full min-h-0 bg-background divide-x divide-border">
        <div className="h-full min-h-0 overflow-hidden">
          {renderPane("pane-1")}
        </div>
        <div className="h-full min-h-0 overflow-hidden">
          {renderPane("pane-2")}
        </div>
      </div>

      {/* Transfers bottom section */}
      {transfers.length > 0 && (
        <div className="bg-sidebar border-t border-border p-3 max-h-36 overflow-y-auto shrink-0 select-none">
          <div className="flex items-center justify-between mb-2 pb-1 border-b border-border">
            <span className="text-xs font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-1.5">
              <Info className="size-3 text-primary" /> Active File Transfers
            </span>
            <Button
              variant="ghost"
              className="h-4 text-[9px] px-1 text-muted-foreground cursor-pointer"
              onClick={() => setTransfers([])}
            >
              Clear Transfers
            </Button>
          </div>
          <div className="space-y-2.5">
            {transfers.map((t) => (
              <div key={t.id} className="text-[10px] text-foreground space-y-1">
                <div className="flex justify-between font-semibold">
                  <span className="truncate max-w-[300px]">
                    {t.kind === "download" ? (
                      <>
                        Downloading{" "}
                        <span className="text-primary font-mono">
                          {t.fileName}
                        </span>{" "}
                        from{" "}
                        <span className="text-muted-foreground">
                          {t.fromHost}
                        </span>
                      </>
                    ) : t.kind === "edit" ? (
                      <>
                        Opening{" "}
                        <span className="text-primary font-mono">
                          {t.fileName}
                        </span>{" "}
                        for editing
                      </>
                    ) : (
                      <>
                        Transferring{" "}
                        <span className="text-primary font-mono">
                          {t.fileName}
                        </span>{" "}
                        ({t.fromHost} → {t.toHost})
                      </>
                    )}
                  </span>
                  <span className="font-mono">
                    {t.status === "completed" ? (
                      <span className="text-emerald-400 font-bold flex items-center gap-0.5">
                        <Check className="size-3" />{" "}
                        {t.kind === "edit" ? "Opened" : "Done"}
                      </span>
                    ) : t.status === "failed" ? (
                      <span
                        className="text-destructive font-bold"
                        title={t.error || undefined}
                      >
                        Failed
                      </span>
                    ) : t.kind === "edit" ? (
                      <span className="text-primary font-bold">Opening…</span>
                    ) : (
                      `${t.percentage}% (${formatBytes(t.bytesMoved)} of ${formatBytes(t.totalSize)})`
                    )}
                  </span>
                </div>
                <div className="w-full bg-sidebar/85 rounded-full h-1.5 border border-border/30 overflow-hidden">
                  <div
                    className={`h-full transition-all duration-100 ${
                      t.status === "completed"
                        ? "bg-emerald-500"
                        : t.status === "failed"
                          ? "bg-destructive"
                          : "bg-primary"
                    }`}
                    style={{ width: `${t.percentage}%` }}
                  ></div>
                </div>
              </div>
            ))}
          </div>
        </div>
      )}

      {/* Prompt credentials Modal */}
      {authPrompt.visible && (
        <div className="fixed inset-0 bg-background/80 backdrop-blur-sm flex items-center justify-center z-50 animate-in fade-in duration-200">
          <form
            onSubmit={handlePromptSubmit}
            className="border bg-sidebar p-5 rounded-xl max-w-sm w-full shadow-lg space-y-4 m-4 animate-in zoom-in-95 duration-200"
          >
            <h3 className="text-sm font-semibold flex items-center gap-2">
              <Lock className="size-4 text-primary" /> Authentication required
            </h3>
            <div className="space-y-1.5">
              <p className="text-xs text-foreground">
                Enter target{" "}
                {authPrompt.type === "password"
                  ? "password"
                  : "private key passphrase"}{" "}
                for{" "}
                <span className="font-semibold text-primary">
                  {authPrompt.host.name || authPrompt.host.address}
                </span>
                :
              </p>
              <Input
                type="password"
                placeholder={
                  authPrompt.type === "password" ? "Password" : "Passphrase"
                }
                value={authPrompt.value}
                onChange={(e) =>
                  setAuthPrompt({ ...authPrompt, value: e.target.value })
                }
                className="h-9 text-xs focus:ring-primary/40"
                autoFocus
              />
            </div>
            <div className="flex gap-2 justify-end pt-1">
              <Button
                type="button"
                variant="outline"
                size="sm"
                className="text-xs cursor-pointer"
                onClick={() => {
                  const setPane =
                    authPrompt.paneId === "pane-1" ? setPane1 : setPane2;
                  setPane((prev) => ({ ...prev, host: null, error: "" }));
                  setAuthPrompt({
                    visible: false,
                    paneId: "",
                    host: null,
                    type: "password",
                    value: "",
                  });
                }}
              >
                Cancel
              </Button>
              <Button
                type="submit"
                size="sm"
                className="text-xs cursor-pointer"
              >
                Unlock & Connect
              </Button>
            </div>
          </form>
        </div>
      )}

      {/* Custom Context Menu */}
      {contextMenu.visible &&
        (() => {
          const menuPane = getPaneById(contextMenu.paneId);
          const setMenuPane =
            contextMenu.paneId === "pane-1" ? setPane1 : setPane2;
          const fileCount = menuPane?.files?.length || 0;
          const allSelected =
            fileCount > 0 && menuPane.selected?.size === fileCount;
          const itemClass =
            "px-3 py-1.5 text-xs text-foreground hover:bg-muted text-left font-medium cursor-pointer flex items-center gap-2.5";

          return (
            <div
              ref={menuRef}
              style={{ top: contextMenu.y, left: contextMenu.x }}
              onClick={(e) => e.stopPropagation()}
              className="fixed z-50 bg-popover border border-border rounded-lg shadow-lg w-52 py-1.5 animate-in fade-in zoom-in-95 duration-100 flex flex-col"
            >
              {contextMenu.file && (
                <>
                  <button onClick={handleOpen} className={itemClass}>
                    {contextMenu.file.is_dir ? (
                      <FolderOpen className="size-3.5 text-primary shrink-0" />
                    ) : (
                      <Pencil className="size-3.5 text-primary shrink-0" />
                    )}
                    {contextMenu.file.is_dir ? "Open" : "Open & edit"}
                  </button>
                  <button onClick={handleDownload} className={itemClass}>
                    <Download className="size-3.5 text-primary shrink-0" />
                    Download{contextMenu.file.is_dir ? " as ZIP" : ""}
                  </button>
                  <button onClick={handleCopyTarget} className={itemClass}>
                    <Copy className="size-3.5 text-primary shrink-0" />
                    Copy to other pane
                  </button>
                  <button onClick={handleRename} className={itemClass}>
                    <Pencil className="size-3.5 text-primary shrink-0" />
                    Rename
                  </button>
                  <button
                    onClick={handleDelete}
                    className="px-3 py-1.5 text-xs text-destructive hover:bg-destructive/10 text-left font-medium cursor-pointer flex items-center gap-2.5"
                  >
                    <Trash2 className="size-3.5 shrink-0" />
                    Delete
                  </button>
                  <hr className="border-border/40 my-1" />
                </>
              )}

              <button
                onClick={() => {
                  loadDir(contextMenu.paneId, menuPane.path);
                  closeContextMenu();
                }}
                className={itemClass}
              >
                <RefreshCw className="size-3.5 text-primary shrink-0" />
                Refresh
              </button>
              <button onClick={handleCreateFolder} className={itemClass}>
                <FolderPlus className="size-3.5 text-primary shrink-0" />
                New Folder
              </button>
              <button onClick={handleCreateFile} className={itemClass}>
                <FilePlus className="size-3.5 text-primary shrink-0" />
                New File
              </button>
              <button
                onClick={() => {
                  setMenuPane((prev) => ({
                    ...prev,
                    showHidden: !prev.showHidden,
                  }));
                  closeContextMenu();
                }}
                className={itemClass}
              >
                {menuPane?.showHidden ? (
                  <EyeOff className="size-3.5 text-primary shrink-0" />
                ) : (
                  <Eye className="size-3.5 text-primary shrink-0" />
                )}
                {menuPane?.showHidden
                  ? "Hide Hidden Files"
                  : "Show Hidden Files"}
              </button>
              <button
                onClick={() => {
                  setMenuPane((prev) => ({
                    ...prev,
                    selected: allSelected
                      ? new Set()
                      : new Set(prev.files.map((f) => f.name)),
                  }));
                  closeContextMenu();
                }}
                className={itemClass}
              >
                {allSelected ? (
                  <Square className="size-3.5 text-primary shrink-0" />
                ) : (
                  <CheckSquare className="size-3.5 text-primary shrink-0" />
                )}
                {allSelected ? "Deselect All" : "Select All"}
              </button>
              <hr className="border-border/40 my-1.5" />
              <button
                onClick={closeContextMenu}
                className="px-3 py-1.5 text-xs text-muted-foreground hover:bg-muted text-left font-medium cursor-pointer flex items-center gap-2.5"
              >
                <X className="size-3.5 shrink-0" />
                Close
              </button>
            </div>
          );
        })()}

      {/* Rename / New Folder / New File / Delete / Alert dialogs */}
      <Dialog
        open={!!dialog.type}
        onOpenChange={(open) => {
          if (!open) closeDialog();
        }}
      >
        <DialogContent showCloseButton={false}>
          {dialog.type === "rename" && (
            <form
              onSubmit={(e) => {
                e.preventDefault();
                confirmRename();
              }}
              className="space-y-4"
            >
              <DialogHeader>
                <DialogTitle className="flex items-center gap-2 text-base">
                  <Pencil className="size-4 text-primary" /> Rename
                </DialogTitle>
                <DialogDescription>
                  Renaming{" "}
                  <span className="font-semibold text-foreground">
                    {dialog.file?.name}
                  </span>
                </DialogDescription>
              </DialogHeader>
              <Input
                autoFocus
                value={dialog.value}
                onChange={(e) =>
                  setDialog((prev) => ({ ...prev, value: e.target.value }))
                }
                className="h-9 text-xs"
                placeholder="New name"
              />
              <DialogFooter className="gap-2">
                <Button
                  type="button"
                  variant="outline"
                  size="sm"
                  className="cursor-pointer"
                  onClick={closeDialog}
                >
                  Cancel
                </Button>
                <Button type="submit" size="sm" className="cursor-pointer">
                  Rename
                </Button>
              </DialogFooter>
            </form>
          )}

          {(dialog.type === "newFolder" || dialog.type === "newFile") && (
            <form
              onSubmit={(e) => {
                e.preventDefault();
                dialog.type === "newFolder"
                  ? confirmCreateFolder()
                  : confirmCreateFile();
              }}
              className="space-y-4"
            >
              <DialogHeader>
                <DialogTitle className="flex items-center gap-2 text-base">
                  {dialog.type === "newFolder" ? (
                    <FolderPlus className="size-4 text-primary" />
                  ) : (
                    <FilePlus className="size-4 text-primary" />
                  )}
                  {dialog.type === "newFolder" ? "New Folder" : "New File"}
                </DialogTitle>
                <DialogDescription>
                  Enter a name for the new{" "}
                  {dialog.type === "newFolder" ? "folder" : "file"}.
                </DialogDescription>
              </DialogHeader>
              <Input
                autoFocus
                value={dialog.value}
                onChange={(e) =>
                  setDialog((prev) => ({ ...prev, value: e.target.value }))
                }
                className="h-9 text-xs"
                placeholder={
                  dialog.type === "newFolder" ? "Folder name" : "File name"
                }
              />
              <DialogFooter className="gap-2">
                <Button
                  type="button"
                  variant="outline"
                  size="sm"
                  className="cursor-pointer"
                  onClick={closeDialog}
                >
                  Cancel
                </Button>
                <Button type="submit" size="sm" className="cursor-pointer">
                  Create
                </Button>
              </DialogFooter>
            </form>
          )}

          {dialog.type === "delete" && (
            <div className="space-y-4">
              <DialogHeader>
                <DialogTitle className="flex items-center gap-2 text-base text-destructive">
                  <Trash2 className="size-4" /> Delete{" "}
                  {dialog.file?.is_dir ? "folder" : "file"}
                </DialogTitle>
                <DialogDescription>
                  Are you sure you want to delete{" "}
                  <span className="font-semibold text-foreground">
                    {dialog.file?.name}
                  </span>
                  ? This action cannot be undone.
                </DialogDescription>
              </DialogHeader>
              <DialogFooter className="gap-2">
                <Button
                  variant="outline"
                  size="sm"
                  className="cursor-pointer"
                  onClick={closeDialog}
                >
                  Cancel
                </Button>
                <Button
                  variant="destructive"
                  size="sm"
                  className="cursor-pointer"
                  onClick={confirmDelete}
                >
                  Delete
                </Button>
              </DialogFooter>
            </div>
          )}

          {dialog.type === "alert" && (
            <div className="space-y-4">
              <DialogHeader>
                <DialogTitle className="flex items-center gap-2 text-base">
                  <AlertTriangle className="size-4 text-amber-500" />{" "}
                  {dialog.title || "Notice"}
                </DialogTitle>
                <DialogDescription className="break-words">
                  {dialog.message}
                </DialogDescription>
              </DialogHeader>
              <DialogFooter>
                <Button
                  size="sm"
                  className="cursor-pointer"
                  onClick={closeDialog}
                >
                  OK
                </Button>
              </DialogFooter>
            </div>
          )}
        </DialogContent>
      </Dialog>
    </DashboardLayout>
  );
}