let state = {
  panes: {
    left: {
      currentPath: "/Volumes",
      showHidden: false,
      selectedItem: null,
      selectedItems: [],
      visibleItems: [],
      currentRowIndex: -1,
      selectionAnchorIndex: null,
      pendingSelectionPath: null,
      returnFocusName: null,
    },
    right: {
      currentPath: "/Volumes",
      showHidden: false,
      selectedItem: null,
      selectedItems: [],
      visibleItems: [],
      currentRowIndex: -1,
      selectionAnchorIndex: null,
      pendingSelectionPath: null,
      returnFocusName: null,
    },
  },
  activePane: "left",
  selectedTaskId: null,
  lastTaskCount: 0,
};
const ROW_JUMP_STEP = 10;
let wildcardDialogMode = "select";
let editorState = {
  path: null,
  originalContent: "",
  modified: null,
};
let monacoState = {
  module: null,
  loadPromise: null,
  editor: null,
  model: null,
  resizeHandler: null,
};
let moveState = {
  source: null,
  destination: "",
};
let renameState = {
  source: null,
  name: "",
  submitAction: null,
};
let deleteConfirmState = {
  resolver: null,
};
let contextMenuState = {
  open: false,
  pane: "left",
  items: [],
  anchorPath: null,
};
let batchMoveState = {
  destinationBase: "",
  count: 0,
};
let imageViewerState = {
  scale: 1,
  fitScale: 1,
  path: null,
  resizeHandler: null,
};
let uploadState = {
  active: false,
  targetPath: "",
  files: [],
  index: 0,
  overwriteAll: false,
  skipAll: false,
  successfulCount: 0,
  skippedCount: 0,
  cancelled: false,
  conflictResolver: null,
  cancelRequested: false,
};
let downloadProgressState = {
  active: false,
  archiveLabel: "",
  totalItems: 0,
  requestKey: null,
  mode: null,
  taskId: null,
  cancelRequested: false,
};
let folderUploadPlanState = {
  targetPane: "left",
  targetPath: "",
  rootFolderName: "",
  entries: [],
  fileCount: 0,
  subfolderCount: 0,
};
let folderUploadPickerInput = null;
let settingsState = {
  activeTab: "general",
  logsLoaded: false,
  tasksLoaded: false,
  logsPollTimer: null,
  lastHistoryRenderKey: "",
  lastTasksRenderKey: "",
  showThumbnails: false,
  preferredStartupPathLeft: null,
  preferredStartupPathRight: null,
  selectedTheme: "default",
  selectedColorMode: "dark",
  zipDownloadLimits: null,
};
let headerTaskState = {
  activeItems: [],
  visibleItems: [],
  recentItems: [],
  popoverOpen: false,
  pollTimer: null,
  lastRenderKey: "",
  knownStatuses: {},
  recentExpiryMs: 4000,
  paneRefreshPromise: null,
};
// The header chip/popover reflects user-visible file operations, not every task-backed file action.
const ACTIVE_OPERATION_OPERATIONS = new Set(["copy", "move", "duplicate", "delete"]);
const ACTIVE_TASK_STATUSES = new Set(["queued", "running", "cancelling"]);
const TERMINAL_OPERATION_STATUSES = new Set(["completed", "cancelled", "failed"]);
const VALID_THEME_FAMILIES = [
  "default",
  "macos-soft",
  "midnight",
  "graphite",
  "windows11",
  "commander-electric",
  "nord-arctic",
  "catppuccin-soft",
  "fluent-neon",
];
const VALID_COLOR_MODES = ["dark", "light"];
const VIRTUAL_SOURCES = [
  { path: "/Volumes", label: "Volumes" },
  { path: "/Clients", label: "Clients" },
];
let searchState = {
  pane: "left",
  path: "/Volumes",
  query: "",
};
function effectiveThemeKey(theme, colorMode) {
  const family = VALID_THEME_FAMILIES.includes(theme) ? theme : "default";
  const mode = VALID_COLOR_MODES.includes(colorMode) ? colorMode : "dark";
  return `${family}-${mode}`;
}

function currentColorMode() {
  return document.documentElement.dataset.colorMode === "light" ? "light" : "dark";
}

function applyTheme(theme, colorMode) {
  const family = VALID_THEME_FAMILIES.includes(theme) ? theme : "default";
  const mode = VALID_COLOR_MODES.includes(colorMode) ? colorMode : "dark";
  const nextTheme = effectiveThemeKey(family, mode);
  document.documentElement.dataset.themeFamily = family;
  document.documentElement.dataset.colorMode = mode;
  document.documentElement.dataset.theme = nextTheme;
  const icon = document.getElementById("theme-toggle-icon");
  const button = document.getElementById("theme-toggle");
  if (icon) {
    icon.textContent = mode === "dark" ? "☾" : "☀";
  }
  if (button) {
    button.setAttribute("aria-label", `Switch to ${mode === "dark" ? "light" : "dark"} mode`);
    button.setAttribute("title", `Switch to ${mode === "dark" ? "light" : "dark"} mode`);
  }
  if (monacoState.module) {
    monacoState.module.editor.setTheme(mode === "light" ? "vs" : "vs-dark");
  }
}

async function toggleTheme() {
  const current = settingsState.selectedColorMode === "light" ? "light" : "dark";
  const next = current === "dark" ? "light" : "dark";
  try {
    const data = await saveSettings({ selected_color_mode: next });
    applyTheme(data.selected_theme, data.selected_color_mode);
  } catch (err) {
    setError("actions-error", `Theme: ${err.message}`);
  }
}

function paneState(pane) {
  return state.panes[pane];
}

function otherPane(pane) {
  return pane === "left" ? "right" : "left";
}

function activePaneState() {
  return paneState(state.activePane);
}

function sourceRootForPath(path) {
  const normalized = (path || "").trim();
  if (normalized === "/Clients" || normalized.startsWith("/Clients/")) {
    return "/Clients";
  }
  return "/Volumes";
}

function isRemoteBrowsePath(path) {
  return sourceRootForPath(path) === "/Clients";
}

function syncSourceSwitchers() {
  ["left", "right"].forEach((pane) => {
    const container = document.getElementById(`${pane}-source-switcher`);
    if (!container) {
      return;
    }
    const activeSource = sourceRootForPath(paneState(pane).currentPath);
    [...container.querySelectorAll("button[data-source-path]")].forEach((button) => {
      const isActive = button.dataset.sourcePath === activeSource;
      button.disabled = isActive;
      button.setAttribute("aria-pressed", isActive ? "true" : "false");
    });
  });
}

function ensureSourceSwitchers() {
  ["left", "right"].forEach((pane) => {
    const toolbar = document.querySelector(`#${pane}-pane .pane-topbar`);
    if (!toolbar || document.getElementById(`${pane}-source-switcher`)) {
      return;
    }
    const container = document.createElement("div");
    container.id = `${pane}-source-switcher`;
    container.className = "pane-source-switcher";
    VIRTUAL_SOURCES.forEach((source) => {
      const button = createButton(source.label, () => {
        setActivePane(pane);
        navigateTo(pane, source.path);
      });
      button.type = "button";
      button.dataset.sourcePath = source.path;
      container.append(button);
    });
    toolbar.prepend(container);
  });
  syncSourceSwitchers();
}

function setStatus(msg) {
  document.getElementById("status").textContent = msg;
}

function headerTaskElements() {
  return {
    container: document.getElementById("header-task-chip-container"),
    chipButton: document.getElementById("header-task-chip-btn"),
    chipLabel: document.getElementById("header-task-chip-label"),
    popover: document.getElementById("header-task-popover"),
    popoverList: document.getElementById("header-task-popover-list"),
    logsButton: document.getElementById("header-task-logs-btn"),
  };
}

function setError(id, msg) {
  if (id === "actions-error") {
    document.getElementById(id).textContent = "";
    if (msg) {
      openFeedbackModal(msg);
    } else {
      closeFeedbackModal();
    }
    return;
  }
  document.getElementById(id).textContent = msg || "";
}

function setActionError(action, err) {
  setError("actions-error", `${action}: ${err.message}`);
}

function showActionSummary(action, successes, failures, firstError) {
  const base = `${action}: ${successes} success, ${failures} failed`;
  if (firstError) {
    setError("actions-error", `${base}. First error: ${firstError}`);
  } else {
    setError("actions-error", "");
  }
  setStatus(base);
}

function viewerElements() {
  return {
    overlay: document.getElementById("viewer-modal"),
    title: document.getElementById("viewer-title"),
    fileName: document.getElementById("viewer-file-name"),
    filePath: document.getElementById("viewer-file-path"),
    error: document.getElementById("viewer-error"),
    content: document.getElementById("viewer-content"),
    closeButton: document.getElementById("viewer-close-btn"),
  };
}

function editorElements() {
  return {
    overlay: document.getElementById("editor-modal"),
    title: document.getElementById("editor-title"),
    fileName: document.getElementById("editor-file-name"),
    filePath: document.getElementById("editor-file-path"),
    error: document.getElementById("editor-error"),
    host: document.getElementById("editor-host"),
    closeButton: document.getElementById("editor-close-btn"),
    saveButton: document.getElementById("editor-save-btn"),
    cancelButton: document.getElementById("editor-cancel-btn"),
  };
}

function videoElements() {
  return {
    overlay: document.getElementById("video-modal"),
    title: document.getElementById("video-title"),
    fileName: document.getElementById("video-file-name"),
    filePath: document.getElementById("video-file-path"),
    error: document.getElementById("video-error"),
    player: document.getElementById("video-player"),
    closeButton: document.getElementById("video-close-btn"),
  };
}

function pdfElements() {
  return {
    overlay: document.getElementById("pdf-modal"),
    title: document.getElementById("pdf-title"),
    fileName: document.getElementById("pdf-file-name"),
    filePath: document.getElementById("pdf-file-path"),
    error: document.getElementById("pdf-error"),
    frame: document.getElementById("pdf-frame"),
    closeButton: document.getElementById("pdf-close-btn"),
  };
}

function imageElements() {
  return {
    overlay: document.getElementById("image-modal"),
    title: document.getElementById("image-title"),
    fileName: document.getElementById("image-file-name"),
    filePath: document.getElementById("image-file-path"),
    error: document.getElementById("image-error"),
    viewport: document.getElementById("image-viewport"),
    image: document.getElementById("image-viewer-img"),
    closeButton: document.getElementById("image-close-btn"),
    zoomInButton: document.getElementById("image-zoom-in-btn"),
    zoomOutButton: document.getElementById("image-zoom-out-btn"),
    resetButton: document.getElementById("image-reset-btn"),
  };
}

function moveElements() {
  return {
    overlay: document.getElementById("move-popup"),
    source: document.getElementById("move-source"),
    input: document.getElementById("move-input"),
    error: document.getElementById("move-error"),
    applyButton: document.getElementById("move-apply-btn"),
    cancelButton: document.getElementById("move-cancel-btn"),
    closeButton: document.getElementById("move-close-btn"),
  };
}

function renameElements() {
  return {
    overlay: document.getElementById("rename-popup"),
    title: document.getElementById("rename-title"),
    label: document.getElementById("rename-label"),
    input: document.getElementById("rename-input"),
    error: document.getElementById("rename-error"),
    applyButton: document.getElementById("rename-apply-btn"),
    cancelButton: document.getElementById("rename-cancel-btn"),
    closeButton: document.getElementById("rename-close-btn"),
  };
}

function batchMoveElements() {
  return {
    overlay: document.getElementById("batch-move-popup"),
    count: document.getElementById("batch-move-count"),
    destination: document.getElementById("batch-move-destination"),
    error: document.getElementById("batch-move-error"),
    applyButton: document.getElementById("batch-move-apply-btn"),
    cancelButton: document.getElementById("batch-move-cancel-btn"),
  };
}

function deleteConfirmElements() {
  return {
    overlay: document.getElementById("delete-confirm-modal"),
    title: document.getElementById("delete-confirm-title"),
    message: document.getElementById("delete-confirm-message"),
    path: document.getElementById("delete-confirm-path"),
    error: document.getElementById("delete-confirm-error"),
    applyButton: document.getElementById("delete-confirm-apply-btn"),
    cancelButton: document.getElementById("delete-confirm-cancel-btn"),
  };
}

function feedbackElements() {
  return {
    overlay: document.getElementById("feedback-modal"),
    message: document.getElementById("feedback-message"),
    closeButton: document.getElementById("feedback-close-btn"),
  };
}

function downloadModalElements() {
  return {
    overlay: document.getElementById("download-modal"),
    target: document.getElementById("download-modal-target"),
    currentFile: document.getElementById("download-modal-current-file"),
    count: document.getElementById("download-modal-count"),
    progressBar: document.getElementById("download-modal-progress-bar"),
    status: document.getElementById("download-modal-status"),
    logsButton: document.getElementById("download-modal-logs-btn"),
    cancelButton: document.getElementById("download-modal-cancel-btn"),
    closeButton: document.getElementById("download-modal-close-btn"),
  };
}

function contextMenuElements() {
  return {
    menu: document.getElementById("context-menu"),
    scope: document.getElementById("context-menu-scope"),
    target: document.getElementById("context-menu-target"),
    openButton: document.getElementById("context-menu-open-btn"),
    editButton: document.getElementById("context-menu-edit-btn"),
    downloadButton: document.getElementById("context-menu-download-btn"),
    renameButton: document.getElementById("context-menu-rename-btn"),
    duplicateButton: document.getElementById("context-menu-duplicate-btn"),
    copyButton: document.getElementById("context-menu-copy-btn"),
    moveButton: document.getElementById("context-menu-move-btn"),
    deleteButton: document.getElementById("context-menu-delete-btn"),
    propertiesButton: document.getElementById("context-menu-properties-btn"),
  };
}

function isOpenableSelection(item) {
  if (!item) {
    return false;
  }
  if (item.kind === "directory") {
    return true;
  }
  return isImageSelection(item) || isVideoSelection(item);
}

function isTextPreviewSelection(item) {
  if (!item || item.kind !== "file") {
    return false;
  }
  const lower = (item.name || "").toLowerCase();
  if (lower === "dockerfile" || lower === "containerfile") {
    return true;
  }
  return [".txt", ".log", ".ini", ".cfg", ".conf", ".md", ".yml", ".yaml", ".json", ".js", ".py", ".css", ".html"].some((suffix) =>
    lower.endsWith(suffix)
  );
}

function isRemoteViewableSelection(item) {
  return isImageSelection(item) || isTextPreviewSelection(item);
}

function isZipDownloadSelection(items) {
  return items.length > 1 || (items.length === 1 && items[0].kind === "directory");
}

function zipDownloadRequestKey(paths) {
  return paths.join("\n");
}

function singleFileDownloadRequestKey(path) {
  return path;
}

function selectedItemCountLabel(totalItems) {
  return `${totalItems} selected item${totalItems === 1 ? "" : "s"}`;
}

function archiveTaskStatusLabel(status) {
  switch (status) {
    case "requested":
      return "Requested";
    case "preparing":
      return "Preparing";
    case "ready":
      return "Ready";
    case "failed":
      return "Failed";
    case "cancelled":
      return "Cancelled";
    default:
      return "Preparing";
  }
}

function archiveTaskCountText(task) {
  if (typeof task.done_items === "number" && typeof task.total_items === "number") {
    return `${task.done_items}/${task.total_items} top-level items`;
  }
  if (typeof task.total_items === "number") {
    return `0/${task.total_items} top-level items`;
  }
  if (task.status === "requested") {
    return "Waiting for archive worker";
  }
  return "Preparing archive";
}

function archiveTaskCurrentItemText(task) {
  if (task.current_item) {
    return `Current: ${task.current_item}`;
  }
  if (task.status === "requested") {
    return `Selection: ${selectedItemCountLabel(downloadProgressState.totalItems)}`;
  }
  if (task.status === "ready") {
    return `Prepared ${selectedItemCountLabel(downloadProgressState.totalItems)}`;
  }
  return `Selection: ${selectedItemCountLabel(downloadProgressState.totalItems)}`;
}

function archiveTaskProgressPercent(task) {
  if (task.status === "ready") {
    return 100;
  }
  if (typeof task.done_items === "number" && typeof task.total_items === "number" && task.total_items > 0) {
    return Math.max(0, Math.min(100, Math.round((task.done_items / task.total_items) * 100)));
  }
  return 0;
}

function isDownloadModalOpen() {
  return !downloadModalElements().overlay.classList.contains("hidden");
}

function setDownloadModalVisible(visible) {
  const elements = downloadModalElements();
  if (!elements.overlay) {
    return;
  }
  elements.overlay.classList.toggle("hidden", !visible);
}

function updateDownloadModalDisplay(info) {
  const elements = downloadModalElements();
  if (!elements.overlay) {
    return;
  }
  elements.target.textContent = info.targetText || "";
  elements.currentFile.textContent = info.currentFileText || "";
  elements.count.textContent = info.countText || "";
  elements.status.textContent = info.statusText || "";
  elements.progressBar.style.width = `${Math.max(0, Math.min(100, info.percent || 0))}%`;
  elements.logsButton.classList.toggle("hidden", !info.logsVisible);
  elements.cancelButton.disabled = !!info.cancelDisabled;
  elements.cancelButton.classList.toggle("hidden", !info.cancelVisible);
  elements.closeButton.disabled = !!info.active;
  elements.closeButton.classList.toggle("hidden", !!info.active);
}

function openZipDownloadModal(selectedItems) {
  const requestPaths = selectedItems.map((item) => item.path);
  downloadProgressState.active = true;
  downloadProgressState.mode = "archive";
  downloadProgressState.archiveLabel = "ZIP archive";
  downloadProgressState.totalItems = selectedItems.length;
  downloadProgressState.requestKey = zipDownloadRequestKey(requestPaths);
  downloadProgressState.taskId = null;
  downloadProgressState.cancelRequested = false;
  setDownloadModalVisible(true);
  updateDownloadModalDisplay({
    active: true,
    targetText: "Archive download requested",
    currentFileText: `Selection: ${selectedItemCountLabel(selectedItems.length)}`,
    countText: "Waiting for archive task",
    statusText: "Requested",
    percent: 0,
    logsVisible: true,
    cancelVisible: true,
    cancelDisabled: true,
  });
}

function openSingleFileDownloadModal(selectedItem) {
  downloadProgressState.active = true;
  downloadProgressState.mode = "single_file";
  downloadProgressState.archiveLabel = selectedItem.name;
  downloadProgressState.totalItems = 1;
  downloadProgressState.requestKey = singleFileDownloadRequestKey(selectedItem.path);
  downloadProgressState.taskId = null;
  downloadProgressState.cancelRequested = false;
  setDownloadModalVisible(true);
  updateDownloadModalDisplay({
    active: true,
    targetText: `File download requested: ${selectedItem.name}`,
    currentFileText: `File: ${selectedItem.path}`,
    countText: "Direct file download",
    statusText: "Requesting download...",
    percent: 0,
    logsVisible: true,
    cancelVisible: false,
  });
}

function markZipDownloadReady(fileName) {
  downloadProgressState.active = false;
  downloadProgressState.cancelRequested = false;
  downloadProgressState.archiveLabel = fileName || "ZIP archive";
  updateDownloadModalDisplay({
    active: false,
    targetText: `Archive ready: ${downloadProgressState.archiveLabel}`,
    currentFileText: `Prepared ${selectedItemCountLabel(downloadProgressState.totalItems)}`,
    countText: "Browser download requested",
    statusText: "Ready",
    percent: 100,
    logsVisible: true,
    cancelVisible: false,
  });
  window.setTimeout(closeDownloadModal, 480);
}

function markZipDownloadFailed(err) {
  downloadProgressState.active = false;
  downloadProgressState.cancelRequested = false;
  updateDownloadModalDisplay({
    active: false,
    targetText: "Archive prepare failed",
    currentFileText: `Selection: ${selectedItemCountLabel(downloadProgressState.totalItems)}`,
    countText: "Archive task failed",
    statusText: `Failed: ${err.message || "Archive download failed"}`,
    percent: 0,
    logsVisible: true,
    cancelVisible: false,
  });
}

function markZipDownloadCancelled() {
  downloadProgressState.active = false;
  downloadProgressState.cancelRequested = false;
  updateDownloadModalDisplay({
    active: false,
    targetText: "Archive prepare cancelled",
    currentFileText: `Selection: ${selectedItemCountLabel(downloadProgressState.totalItems)}`,
    countText: "Archive task cancelled",
    statusText: "Cancelled",
    percent: 0,
    logsVisible: true,
    cancelVisible: false,
  });
}

function markSingleFileDownloadRequested(fileName, path) {
  downloadProgressState.active = false;
  updateDownloadModalDisplay({
    active: false,
    targetText: `File download requested: ${fileName}`,
    currentFileText: `File: ${path}`,
    countText: "Browser download requested",
    statusText: "Download requested",
    percent: 0,
    logsVisible: true,
    cancelVisible: false,
  });
  window.setTimeout(closeDownloadModal, 480);
}

function markSingleFileDownloadFailed(err, selectedItem) {
  downloadProgressState.active = false;
  updateDownloadModalDisplay({
    active: false,
    targetText: "File download failed",
    currentFileText: `File: ${selectedItem.path}`,
    countText: "Direct file download",
    statusText: `Failed: ${err.message || "Download failed"}`,
    percent: 0,
    logsVisible: true,
    cancelVisible: false,
  });
}

function updateZipDownloadTaskProgress(task) {
  if (!downloadProgressState.active) {
    return;
  }
  updateDownloadModalDisplay({
    active: true,
    targetText: "Archive download task",
    currentFileText: archiveTaskCurrentItemText(task),
    countText: archiveTaskCountText(task),
    statusText: downloadProgressState.cancelRequested ? "Cancelling download..." : archiveTaskStatusLabel(task.status),
    percent: archiveTaskProgressPercent(task),
    logsVisible: true,
    cancelVisible: true,
    cancelDisabled: !downloadProgressState.taskId || downloadProgressState.cancelRequested,
  });
}

function sleep(ms) {
  return new Promise((resolve) => window.setTimeout(resolve, ms));
}

async function waitForArchiveDownloadReady(taskId) {
  while (true) {
    const task = await getTaskRequest(taskId);
    if (task.status === "ready") {
      return task;
    }
    if (task.status === "cancelled") {
      const err = new Error("Archive download was cancelled");
      err.code = "download_cancelled";
      throw err;
    }
    if (task.status === "failed") {
      const err = new Error(task.error_message || "Archive download failed");
      err.code = task.error_code || null;
      throw err;
    }
    updateZipDownloadTaskProgress(task);
    await sleep(250);
  }
}

function closeDownloadModal() {
  if (downloadProgressState.active) {
    return;
  }
  downloadProgressState.mode = null;
  downloadProgressState.archiveLabel = "";
  downloadProgressState.totalItems = 0;
  downloadProgressState.requestKey = null;
  downloadProgressState.taskId = null;
  downloadProgressState.cancelRequested = false;
  updateDownloadModalDisplay({
    active: false,
    targetText: "",
    currentFileText: "",
    countText: "",
    statusText: "",
    percent: 0,
    logsVisible: false,
    cancelVisible: false,
  });
  setDownloadModalVisible(false);
}

function isContextMenuOpen() {
  return contextMenuState.open && !contextMenuElements().menu.classList.contains("hidden");
}

function closeContextMenu() {
  const elements = contextMenuElements();
  contextMenuState.open = false;
  contextMenuState.pane = "left";
  contextMenuState.items = [];
  contextMenuState.anchorPath = null;
  if (!elements.menu) {
    return;
  }
  elements.menu.classList.add("hidden");
  elements.scope.textContent = "";
  elements.target.textContent = "";
}

function openContextMenu(pane, entry, event) {
  if (!entry || entry.isParent) {
    return;
  }
  const elements = contextMenuElements();
  const selectedItems = paneState(pane).selectedItems || [];
  const selectedPathsSet = new Set(selectedItems.map((item) => item.path));
  const items = selectedPathsSet.has(entry.path)
    ? selectedItems.map((item) => ({ ...item }))
    : [selectedEntryFromItem(entry)];
  const remoteSelection = items.some((item) => isRemoteBrowsePath(item.path));

  contextMenuState.open = true;
  contextMenuState.pane = pane;
  contextMenuState.items = items;
  contextMenuState.anchorPath = entry.path;

  const isMulti = items.length > 1;
  const openableSingle =
    items.length === 1 && (remoteSelection ? items[0].kind === "directory" || isRemoteViewableSelection(items[0]) : isOpenableSelection(items[0]));
  const editableSingle = items.length === 1 && !remoteSelection && isEditableSelection(items[0]);
  const downloadableSelection = items.length === 1 && items[0].kind === "file";
  elements.scope.textContent = isMulti ? "Multi-selection" : "Single item";
  elements.target.textContent = isMulti ? `${items.length} selected items` : entry.name;
  elements.openButton.classList.toggle("hidden", isMulti);
  elements.openButton.disabled = !openableSingle;
  elements.editButton.classList.toggle("hidden", isMulti || items.length !== 1 || items[0].kind !== "file" || remoteSelection);
  elements.editButton.disabled = !editableSingle;
  elements.downloadButton.classList.remove("hidden");
  elements.downloadButton.disabled = !downloadableSelection;
  elements.renameButton.classList.toggle("hidden", isMulti || remoteSelection);
  elements.duplicateButton.classList.remove("hidden");
  elements.duplicateButton.disabled = remoteSelection || items.length === 0;
  elements.copyButton.classList.remove("hidden");
  elements.copyButton.disabled = remoteSelection || items.length === 0;
  elements.moveButton.classList.remove("hidden");
  elements.moveButton.disabled = remoteSelection || items.length === 0;
  elements.deleteButton.classList.remove("hidden");
  elements.deleteButton.disabled = remoteSelection || items.length === 0;
  elements.propertiesButton.classList.remove("hidden");
  elements.propertiesButton.disabled = items.length === 0;

  const menuWidth = 220;
  const menuHeight = 120;
  const x = Math.min(event.clientX, window.innerWidth - menuWidth - 12);
  const y = Math.min(event.clientY, window.innerHeight - menuHeight - 12);
  elements.menu.style.left = `${Math.max(8, x)}px`;
  elements.menu.style.top = `${Math.max(8, y)}px`;
  elements.menu.classList.remove("hidden");
}

function applyContextMenuSelection() {
  if (!contextMenuState.items.length) {
    return false;
  }
  const pane = contextMenuState.pane;
  const model = paneState(pane);
  setActivePane(pane);
  model.selectedItems = contextMenuState.items.map((item) => ({ ...item }));
  model.selectedItem = model.selectedItems.length > 0 ? model.selectedItems[model.selectedItems.length - 1] : null;
  const anchorPath = contextMenuState.anchorPath;
  if (anchorPath) {
    const currentIndex = model.visibleItems.findIndex((item) => !item.isParent && item.path === anchorPath);
    if (currentIndex >= 0) {
      model.currentRowIndex = currentIndex;
      setSelectionAnchor(pane, currentIndex);
    }
  }
  renderPaneItems(pane);
  return true;
}

function startContextMenuRename() {
  if (!applyContextMenuSelection()) {
    closeContextMenu();
    return;
  }
  closeContextMenu();
  openRenamePopup();
}

function startContextMenuDelete() {
  if (!applyContextMenuSelection()) {
    closeContextMenu();
    return;
  }
  closeContextMenu();
  deleteSelected();
}

function startContextMenuMove() {
  if (!applyContextMenuSelection()) {
    closeContextMenu();
    return;
  }
  closeContextMenu();
  openF6Flow();
}

function startContextMenuCopy() {
  if (contextMenuElements().copyButton?.disabled) {
    return;
  }
  if (!applyContextMenuSelection()) {
    closeContextMenu();
    return;
  }
  closeContextMenu();
  startCopySelected();
}

async function startDuplicateSelected() {
  const sourcePane = state.activePane;
  const selectedItems = [...paneState(sourcePane).selectedItems];
  if (selectedItems.length === 0) {
    return;
  }
  setError("actions-error", "");
  try {
    const result = await createDuplicateTask(selectedItems.map((item) => item.path));
    state.selectedTaskId = result.task_id;
    await refreshTasksSnapshot();
    await Promise.all([loadBrowsePane("left"), loadBrowsePane("right")]);
    showActionSummary("Duplicate", 1, 0, null);
  } catch (err) {
    showActionSummary("Duplicate", 0, 1, err.message);
  }
}

function startContextMenuDuplicate() {
  if (contextMenuElements().duplicateButton?.disabled) {
    return;
  }
  if (!applyContextMenuSelection()) {
    closeContextMenu();
    return;
  }
  closeContextMenu();
  startDuplicateSelected();
}

function startContextMenuOpen() {
  if (contextMenuElements().openButton?.disabled) {
    return;
  }
  if (!applyContextMenuSelection()) {
    closeContextMenu();
    return;
  }
  closeContextMenu();
  openCurrentDirectory();
}

function startContextMenuEdit() {
  if (contextMenuElements().editButton?.disabled) {
    return;
  }
  if (!applyContextMenuSelection()) {
    closeContextMenu();
    return;
  }
  closeContextMenu();
  openEditor();
}

async function startDownloadSelected() {
  const selectedItems = activePaneState().selectedItems;
  if (selectedItems.length === 0) {
    return;
  }
  const zipDownload = isZipDownloadSelection(selectedItems);
  const selectedPaths = selectedItems.map((item) => item.path);
  const selected = selectedItems[0];
  const requestKey = zipDownload ? zipDownloadRequestKey(selectedPaths) : singleFileDownloadRequestKey(selected.path);
  if (downloadProgressState.active && downloadProgressState.requestKey === requestKey) {
    setStatus(zipDownload ? "Preparing download..." : "Requesting download...");
    return;
  }
  if (zipDownload) {
    openZipDownloadModal(selectedItems);
    setStatus("Preparing download...");
  } else {
    openSingleFileDownloadModal(selected);
    setStatus("Requesting download...");
  }
  try {
    if (zipDownload) {
      const created = await createArchiveDownloadTask(selectedPaths);
      downloadProgressState.taskId = created.task_id;
      updateZipDownloadTaskProgress({
        status: created.status || "requested",
        current_item: null,
        done_items: 0,
        total_items: selectedItems.length,
      });
      const task = await waitForArchiveDownloadReady(created.task_id);
      startArchiveDownload(task.id, task.destination);
      markZipDownloadReady(task.destination);
      setStatus(`Download started: ${task.destination}`);
      return;
    }
    let fileName = selected.name;
    if (isRemoteBrowsePath(selected.path)) {
      fileName = startDirectSingleFileDownload(selected.path, selected.name).fileName || selected.name;
    } else {
      const response = await downloadFileRequest(selectedPaths);
      const url = URL.createObjectURL(response.blob);
      const anchor = document.createElement("a");
      anchor.href = url;
      anchor.download = response.fileName || selected.name;
      document.body.append(anchor);
      anchor.click();
      anchor.remove();
      URL.revokeObjectURL(url);
      fileName = anchor.download || selected.name;
    }
    markSingleFileDownloadRequested(fileName, selected.path);
    setStatus(`Download requested: ${fileName}`);
  } catch (err) {
    if (zipDownload) {
      if (err.code === "download_cancelled") {
        markZipDownloadCancelled();
        setStatus("Download cancelled");
      } else {
        markZipDownloadFailed(err);
        setStatus("Download failed");
      }
    } else {
      markSingleFileDownloadFailed(err, selected);
      setActionError("Download", err);
    }
  }
}

function startContextMenuDownload() {
  if (contextMenuElements().downloadButton?.disabled) {
    return;
  }
  if (!applyContextMenuSelection()) {
    closeContextMenu();
    return;
  }
  closeContextMenu();
  startDownloadSelected();
}

function startContextMenuProperties() {
  if (contextMenuElements().propertiesButton?.disabled) {
    return;
  }
  if (!applyContextMenuSelection()) {
    closeContextMenu();
    return;
  }
  closeContextMenu();
  openInfo();
}

function settingsElements() {
  return {
    overlay: document.getElementById("settings-modal"),
    closeButton: document.getElementById("settings-close-btn"),
    generalTab: document.getElementById("settings-general-tab"),
    interfaceTab: document.getElementById("settings-interface-tab"),
    downloadsTab: document.getElementById("settings-downloads-tab"),
    logsTab: document.getElementById("settings-logs-tab"),
    generalPanel: document.getElementById("settings-general-panel"),
    interfacePanel: document.getElementById("settings-interface-panel"),
    downloadsPanel: document.getElementById("settings-downloads-panel"),
    showThumbnailsInput: document.getElementById("settings-show-thumbnails"),
    startupPathLeftInput: document.getElementById("settings-startup-path-left"),
    startupPathRightInput: document.getElementById("settings-startup-path-right"),
    generalError: document.getElementById("settings-general-error"),
    generalSaveButton: document.getElementById("settings-general-save-btn"),
    selectedThemeInput: document.getElementById("settings-selected-theme"),
    interfaceError: document.getElementById("settings-interface-error"),
    interfaceSaveButton: document.getElementById("settings-interface-save-btn"),
    downloadMaxItems: document.getElementById("settings-download-max-items"),
    downloadMaxTotalSize: document.getElementById("settings-download-max-total-size"),
    downloadMaxFileSize: document.getElementById("settings-download-max-file-size"),
    downloadScanTimeout: document.getElementById("settings-download-scan-timeout"),
    downloadSymlinkPolicy: document.getElementById("settings-download-symlink-policy"),
    logsPanel: document.getElementById("settings-logs-panel"),
    tasksList: document.getElementById("settings-tasks-list"),
    logsList: document.getElementById("settings-logs-list"),
    logsError: document.getElementById("settings-logs-error"),
  };
}

function searchElements() {
  return {
    overlay: document.getElementById("search-modal"),
    closeButton: document.getElementById("search-close-btn"),
    context: document.getElementById("search-context"),
    input: document.getElementById("search-input"),
    error: document.getElementById("search-error"),
    results: document.getElementById("search-results"),
  };
}

function infoElements() {
  return {
    overlay: document.getElementById("info-modal"),
    title: document.getElementById("info-title"),
    closeButton: document.getElementById("info-close-btn"),
    error: document.getElementById("info-error"),
    grid: document.getElementById("info-grid"),
  };
}

function uploadElements() {
  return {
    menu: document.getElementById("upload-menu"),
    button: document.getElementById("upload-btn"),
    menuToggle: document.getElementById("upload-menu-toggle"),
    menuPopup: document.getElementById("upload-menu-popup"),
    folderButton: document.getElementById("upload-folder-btn"),
    input: document.getElementById("upload-input"),
  };
}

function uploadModalElements() {
  return {
    overlay: document.getElementById("upload-modal"),
    target: document.getElementById("upload-modal-target"),
    currentFile: document.getElementById("upload-modal-current-file"),
    count: document.getElementById("upload-modal-count"),
    progressBar: document.getElementById("upload-modal-progress-bar"),
    status: document.getElementById("upload-modal-status"),
    cancelButton: document.getElementById("upload-modal-cancel-btn"),
  };
}

function isFeedbackModalOpen() {
  return !feedbackElements().overlay.classList.contains("hidden");
}

function openFeedbackModal(message) {
  const elements = feedbackElements();
  if (!elements.overlay) {
    return;
  }
  elements.message.textContent = message || "";
  elements.overlay.classList.remove("hidden");
}

function closeFeedbackModal() {
  const elements = feedbackElements();
  if (!elements.overlay) {
    return;
  }
  elements.message.textContent = "";
  elements.overlay.classList.add("hidden");
}

function setUploadModalVisible(visible) {
  const elements = uploadModalElements();
  if (!elements.overlay) {
    return;
  }
  elements.overlay.classList.toggle("hidden", !visible);
}

function updateUploadModalDisplay(info) {
  const elements = uploadModalElements();
  if (!elements.overlay) {
    return;
  }
  const total = info.total || 0;
  elements.target.textContent = `Uploading to: ${info.targetPath}`;
  elements.currentFile.textContent = info.currentFileName
    ? `Current file: ${info.currentFileName}`
    : info.rootFolderName
      ? `Folder: ${info.rootFolderName}`
      : "Preparing files";
  elements.count.textContent = total ? `${Math.min(info.index, total)}/${total} files` : "";
  const percent = total ? Math.min(100, Math.round((info.index / total) * 100)) : 0;
  elements.progressBar.style.width = `${percent}%`;
  if (info.statusText) {
    elements.status.textContent = info.statusText;
  } else if (!elements.status.textContent) {
    elements.status.textContent = "";
  }
  elements.cancelButton.disabled = !uploadState.active;
}

function setUploadModalStatus(msg) {
  const elements = uploadModalElements();
  if (!elements.overlay) {
    return;
  }
  elements.status.textContent = msg || "";
}

function requestUploadCancel() {
  uploadState.cancelRequested = true;
  const elements = uploadModalElements();
  if (elements.cancelButton) {
    elements.cancelButton.disabled = true;
  }
  setUploadModalStatus("Cancel requested; finishing current file...");
}

function ensureFolderUploadPicker() {
  if (folderUploadPickerInput) {
    return folderUploadPickerInput;
  }
  const input = document.createElement("input");
  input.type = "file";
  input.multiple = true;
  input.hidden = true;
  input.setAttribute("webkitdirectory", "");
  input.setAttribute("directory", "");
  input.onchange = handleFolderSelection;
  document.body.append(input);
  folderUploadPickerInput = input;
  return folderUploadPickerInput;
}

function uploadConflictElements() {
  return {
    overlay: document.getElementById("upload-conflict-modal"),
    title: document.getElementById("upload-conflict-title"),
    target: document.getElementById("upload-conflict-target"),
    fileName: document.getElementById("upload-conflict-file-name"),
    message: document.getElementById("upload-conflict-message"),
    overwriteButton: document.getElementById("upload-conflict-overwrite-btn"),
    overwriteAllButton: document.getElementById("upload-conflict-overwrite-all-btn"),
    skipButton: document.getElementById("upload-conflict-skip-btn"),
    skipAllButton: document.getElementById("upload-conflict-skip-all-btn"),
    cancelButton: document.getElementById("upload-conflict-cancel-btn"),
  };
}

function ensureUploadConflictModal() {
  if (document.getElementById("upload-conflict-modal")) {
    return uploadConflictElements();
  }
  const overlay = document.createElement("div");
  overlay.id = "upload-conflict-modal";
  overlay.className = "popup-overlay hidden";

  const card = document.createElement("div");
  card.className = "popup-card";
  card.setAttribute("role", "dialog");
  card.setAttribute("aria-modal", "true");
  card.setAttribute("aria-labelledby", "upload-conflict-title");

  const title = document.createElement("h3");
  title.id = "upload-conflict-title";
  title.textContent = "Upload conflict";

  const target = document.createElement("div");
  target.id = "upload-conflict-target";
  target.className = "popup-meta";

  const fileName = document.createElement("div");
  fileName.id = "upload-conflict-file-name";
  fileName.className = "popup-meta";

  const message = document.createElement("div");
  message.id = "upload-conflict-message";
  message.className = "popup-meta";

  const actions = document.createElement("div");
  actions.className = "popup-actions";

  const overwriteButton = createButton("Overwrite", () => resolveUploadConflict("overwrite"));
  overwriteButton.id = "upload-conflict-overwrite-btn";
  const overwriteAllButton = createButton("Overwrite all", () => resolveUploadConflict("overwrite_all"));
  overwriteAllButton.id = "upload-conflict-overwrite-all-btn";
  const skipButton = createButton("Skip", () => resolveUploadConflict("skip"));
  skipButton.id = "upload-conflict-skip-btn";
  const skipAllButton = createButton("Skip all", () => resolveUploadConflict("skip_all"));
  skipAllButton.id = "upload-conflict-skip-all-btn";
  const cancelButton = createButton("Cancel", () => resolveUploadConflict("cancel"));
  cancelButton.id = "upload-conflict-cancel-btn";

  actions.append(overwriteButton, overwriteAllButton, skipButton, skipAllButton, cancelButton);
  card.append(title, target, fileName, message, actions);
  overlay.append(card);
  document.body.append(overlay);
  return uploadConflictElements();
}

async function apiRequest(method, url, body) {
  const options = { method, headers: {} };
  if (body !== undefined) {
    options.headers["Content-Type"] = "application/json";
    options.body = JSON.stringify(body);
  }
  const response = await fetch(url, options);
  const data = await response.json().catch(() => ({}));
  if (!response.ok) {
    throw createApiError(response, data);
  }
  return data;
}

function createApiError(response, data) {
  const error = data.error || {};
  const err = new Error(error.message || `HTTP ${response.status}`);
  err.code = error.code || null;
  err.status = response.status;
  err.details = error.details || {};
  return err;
}

async function downloadFileRequest(paths) {
  const params = new URLSearchParams();
  for (const path of paths) {
    params.append("path", path);
  }
  const response = await fetch(`/api/files/download?${params.toString()}`);
  if (!response.ok) {
    const data = await response.json().catch(() => ({}));
    throw createApiError(response, data);
  }
  const disposition = response.headers.get("content-disposition") || "";
  const match = disposition.match(/filename=\"([^\"]+)\"/);
  return {
    blob: await response.blob(),
    fileName: match ? match[1] : null,
  };
}

function startDirectSingleFileDownload(path, fallbackName) {
  const anchor = document.createElement("a");
  anchor.href = `/api/files/download?${new URLSearchParams({ path }).toString()}`;
  anchor.download = fallbackName || "";
  document.body.append(anchor);
  anchor.click();
  anchor.remove();
  return {
    fileName: anchor.download || fallbackName || null,
  };
}

async function createArchiveDownloadTask(paths) {
  return apiRequest("POST", "/api/files/download/archive-prepare", { paths });
}

async function createDuplicateTask(paths) {
  return apiRequest("POST", "/api/files/duplicate", { paths });
}

async function getTaskRequest(taskId) {
  return apiRequest("GET", `/api/tasks/${encodeURIComponent(taskId)}`);
}

async function cancelArchiveDownloadTask(taskId) {
  return apiRequest("POST", `/api/files/download/archive/${encodeURIComponent(taskId)}/cancel`);
}

function startArchiveDownload(taskId, fileName) {
  const anchor = document.createElement("a");
  anchor.href = `/api/files/download/archive/${encodeURIComponent(taskId)}`;
  anchor.download = fileName || "";
  document.body.append(anchor);
  anchor.click();
  anchor.remove();
}

async function requestArchiveDownloadCancel() {
  if (!downloadProgressState.active || !downloadProgressState.taskId || downloadProgressState.cancelRequested) {
    return;
  }
  downloadProgressState.cancelRequested = true;
  updateDownloadModalDisplay({
    active: true,
    targetText: "Preparing download...",
    currentFileText: `Selection: ${selectedItemCountLabel(downloadProgressState.totalItems)}`,
    countText: "Zip download cancellation requested",
    statusText: "Cancelling download...",
    percent: 55,
    cancelVisible: true,
    cancelDisabled: true,
  });
  try {
    await cancelArchiveDownloadTask(downloadProgressState.taskId);
  } catch (err) {
    if (err.code !== "download_not_cancellable") {
      downloadProgressState.cancelRequested = false;
      throw err;
    }
  }
}

async function uploadFileRequest(targetPath, file, overwrite = false) {
  const formData = new FormData();
  formData.append("target_path", targetPath);
  formData.append("overwrite", overwrite ? "true" : "false");
  formData.append("file", file, file.name);

  const response = await fetch("/api/files/upload", {
    method: "POST",
    body: formData,
  });
  const data = await response.json().catch(() => ({}));
  if (!response.ok) {
    throw createApiError(response, data);
  }
  return data;
}

async function refreshTasksSnapshot() {
  try {
    const data = await apiRequest("GET", "/api/tasks");
    applyTaskSnapshot(data.items);
  } catch (_) {
    // Task list panel is not visible in current UI; silently keep flow stable.
  }
}

function createButton(text, onClick) {
  const button = document.createElement("button");
  button.textContent = text;
  button.onclick = onClick;
  return button;
}

function closeUploadMenu() {
  const elements = uploadElements();
  if (!elements.menuPopup || !elements.menuToggle) {
    return;
  }
  elements.menuPopup.classList.add("hidden");
  elements.menuToggle.setAttribute("aria-expanded", "false");
}

function toggleUploadMenu() {
  const elements = uploadElements();
  if (!elements.menuPopup || !elements.menuToggle) {
    return;
  }
  const nextOpen = elements.menuPopup.classList.contains("hidden");
  elements.menuPopup.classList.toggle("hidden", !nextOpen);
  elements.menuToggle.setAttribute("aria-expanded", nextOpen ? "true" : "false");
}

function resetUploadProgress() {
  const elements = uploadElements();
  uploadState.active = false;
  uploadState.targetPath = "";
  uploadState.files = [];
  uploadState.index = 0;
  uploadState.overwriteAll = false;
  uploadState.skipAll = false;
  uploadState.successfulCount = 0;
  uploadState.skippedCount = 0;
  uploadState.cancelled = false;
  uploadState.conflictResolver = null;
  uploadState.cancelRequested = false;
  elements.button.disabled = false;
  setUploadModalVisible(false);
  setUploadModalStatus("");
}

function showFolderUploadPlan(plan) {
  folderUploadPlanState = plan;
  updateUploadModalDisplay({
    targetPath: `${plan.targetPath}/${plan.rootFolderName}`,
    rootFolderName: plan.rootFolderName,
    total: plan.fileCount,
    index: 0,
  });
  setUploadModalVisible(true);
  setUploadModalStatus("Preparing folder upload...");
}

function updateUploadProgress() {
  const elements = uploadElements();
  const total = uploadState.files.length;
  const currentFile = uploadState.files[uploadState.index] || null;
  elements.target.textContent = `Upload to: ${uploadState.targetPath}`;
  elements.currentFile.textContent = currentFile
    ? `Uploading ${total} file${total === 1 ? "" : "s"} - Current file: ${currentFile.name}`
    : `Uploading ${total} file${total === 1 ? "" : "s"}`;
  elements.count.textContent = total > 0 ? `${Math.min(uploadState.index + 1, total)}/${total} files` : "";
  elements.button.disabled = uploadState.active;
  setUploadProgressVisible(uploadState.active);
}

function openUploadPicker() {
  if (uploadState.active) {
    return;
  }
  closeUploadMenu();
  uploadState.targetPath = activePaneState().currentPath;
  const elements = uploadElements();
  elements.input.value = "";
  elements.input.click();
}

function openFolderPicker() {
  if (uploadState.active) {
    return;
  }
  closeUploadMenu();
  folderUploadPlanState = {
    targetPane: state.activePane,
    targetPath: activePaneState().currentPath,
    rootFolderName: "",
    entries: [],
    fileCount: 0,
    subfolderCount: 0,
  };
  const input = ensureFolderUploadPicker();
  input.value = "";
  input.click();
}

function isUploadConflictOpen() {
  const overlay = document.getElementById("upload-conflict-modal");
  return Boolean(overlay) && !overlay.classList.contains("hidden");
}

function resolveUploadConflict(choice) {
  const resolver = uploadState.conflictResolver;
  if (!resolver) {
    return;
  }
  uploadState.conflictResolver = null;
  const elements = uploadConflictElements();
  elements.overlay.classList.add("hidden");
  resolver(choice);
}

function promptUploadConflict(fileName, targetPath, message) {
  const elements = ensureUploadConflictModal();
  elements.title.textContent = "Upload conflict";
  elements.target.textContent = `Upload to: ${targetPath}`;
  elements.fileName.textContent = `File: ${fileName}`;
  elements.message.textContent = message || "Target path already exists.";
  elements.overlay.classList.remove("hidden");
  return new Promise((resolve) => {
    uploadState.conflictResolver = resolve;
  });
}

function countPlannedSubfolders(relativePaths) {
  const folderPaths = new Set();
  relativePaths.forEach((relativePath) => {
    const parts = relativePath.split("/").filter(Boolean);
    if (parts.length <= 1) {
      return;
    }
    let current = "";
    for (let index = 0; index < parts.length - 1; index += 1) {
      current = current ? `${current}/${parts[index]}` : parts[index];
      folderPaths.add(current);
    }
  });
  return folderPaths.size;
}

function buildFolderUploadPlan(files, targetPath) {
  if (!files.length) {
    return null;
  }
  const plannedEntries = files.map((file) => {
    const webkitRelativePath = String(file.webkitRelativePath || "").replace(/\\/g, "/");
    const parts = webkitRelativePath.split("/").filter(Boolean);
    return {
      file,
      webkitRelativePath,
      rootFolderName: parts[0] || "",
      relativePath: parts.length > 1 ? parts.slice(1).join("/") : file.name,
    };
  });
  const rootFolderName = plannedEntries[0].rootFolderName;
  if (!rootFolderName) {
    throw new Error("Folder picker did not return a usable folder structure");
  }
  if (plannedEntries.some((entry) => entry.rootFolderName !== rootFolderName || !entry.relativePath)) {
    throw new Error("Folder picker returned multiple roots or invalid relative paths");
  }
  return {
    targetPane: folderUploadPlanState.targetPane || state.activePane,
    targetPath,
    rootFolderName,
    entries: plannedEntries.map((entry) => ({
      file: entry.file,
      name: entry.file.name,
      size: entry.file.size,
      relativePath: entry.relativePath,
    })),
    fileCount: plannedEntries.length,
    subfolderCount: countPlannedSubfolders(plannedEntries.map((entry) => entry.relativePath)),
  };
}

function folderDirectoryPaths(plan) {
  const paths = new Set([`${plan.targetPath}/${plan.rootFolderName}`]);
  plan.entries.forEach((entry) => {
    const parts = entry.relativePath.split("/").filter(Boolean);
    if (parts.length <= 1) {
      return;
    }
    let current = `${plan.targetPath}/${plan.rootFolderName}`;
    for (let index = 0; index < parts.length - 1; index += 1) {
      current = `${current}/${parts[index]}`;
      paths.add(current);
    }
  });
  return Array.from(paths).sort((left, right) => left.split("/").length - right.split("/").length);
}

async function ensureFolderDirectoryExists(path) {
  const segments = path.split("/");
  const name = segments.pop();
  const parentPath = segments.join("/");
  try {
    await apiRequest("POST", "/api/files/mkdir", {
      parent_path: parentPath,
      name,
    });
    return;
  } catch (err) {
    if (err.code !== "already_exists") {
      throw err;
    }
  }
  await apiRequest("GET", `/api/browse?${new URLSearchParams({ path }).toString()}`);
}

async function executeFolderUploadPlan(plan) {
  uploadState.active = true;
  uploadState.targetPath = `${plan.targetPath}/${plan.rootFolderName}`;
  uploadState.overwriteAll = false;
  uploadState.skipAll = false;
  uploadState.successfulCount = 0;
  uploadState.skippedCount = 0;
  uploadState.cancelled = false;
  uploadState.files = plan.entries.map((entry) => entry.file);
  uploadState.index = 0;
  setError("actions-error", "");
  showFolderUploadPlan(plan);

  try {
    const directories = folderDirectoryPaths(plan);
    for (const directoryPath of directories) {
      if (uploadState.cancelRequested) {
        uploadState.cancelled = true;
        break;
      }
      await ensureFolderDirectoryExists(directoryPath);
    }
    setUploadModalStatus("");

    outer:
    for (let index = 0; index < plan.entries.length; index += 1) {
      if (uploadState.cancelRequested) {
        uploadState.cancelled = true;
        break;
      }
      const entry = plan.entries[index];
      const relativeParts = entry.relativePath.split("/").filter(Boolean);
      const fileName = relativeParts[relativeParts.length - 1];
      const parentSegments = relativeParts.slice(0, -1);
      const targetPath = parentSegments.length
        ? `${plan.targetPath}/${plan.rootFolderName}/${parentSegments.join("/")}`
        : `${plan.targetPath}/${plan.rootFolderName}`;

      uploadState.index = index;
      updateUploadModalDisplay({
        targetPath: `${plan.targetPath}/${plan.rootFolderName}`,
        rootFolderName: plan.rootFolderName,
        total: plan.fileCount,
        index: index + 1,
        currentFileName: entry.relativePath,
      });
      let overwrite = uploadState.overwriteAll;
      while (true) {
        try {
          await uploadFileRequest(targetPath, entry.file, overwrite);
          uploadState.successfulCount += 1;
          break;
        } catch (err) {
          if (err.code !== "already_exists") {
            throw err;
          }
          if (uploadState.skipAll) {
            uploadState.skippedCount += 1;
            break;
          }
          const choice = await promptUploadConflict(fileName, targetPath, err.message);
          if (choice === "overwrite") {
            overwrite = true;
            continue;
          }
          if (choice === "overwrite_all") {
            uploadState.overwriteAll = true;
            overwrite = true;
            continue;
          }
          if (choice === "skip") {
            uploadState.skippedCount += 1;
            break;
          }
          if (choice === "skip_all") {
            uploadState.skipAll = true;
            uploadState.skippedCount += 1;
            break;
          }
          uploadState.cancelRequested = true;
          uploadState.cancelled = true;
          break outer;
        }
      }
    }
    if (uploadState.successfulCount > 0 || uploadState.skippedCount > 0) {
      await loadBrowsePane(plan.targetPane);
    }
    if (uploadState.cancelled) {
      setStatus(`Folder upload: ${uploadState.successfulCount}/${plan.fileCount} file${plan.fileCount === 1 ? "" : "s"} uploaded before cancel`);
    } else if (uploadState.skippedCount > 0) {
      setStatus(`Folder upload: ${uploadState.successfulCount} uploaded, ${uploadState.skippedCount} skipped`);
    } else {
      setStatus(`Folder upload: ${uploadState.successfulCount} file${uploadState.successfulCount === 1 ? "" : "s"} uploaded`);
    }
  } catch (err) {
    if (uploadState.successfulCount > 0 || uploadState.skippedCount > 0) {
      await loadBrowsePane(plan.targetPane);
    }
    setActionError("Folder upload", err);
  } finally {
    resetUploadProgress();
  }
}

async function handleFolderSelection(event) {
  const files = Array.from(event.target.files || []);
  event.target.value = "";
  if (files.length === 0) {
    return;
  }
  try {
    const targetPath = folderUploadPlanState.targetPath || activePaneState().currentPath;
    const plan = buildFolderUploadPlan(files, targetPath);
    if (!plan) {
      return;
    }
    showFolderUploadPlan(plan);
    setError("actions-error", "");
    setStatus(`Folder upload: preparing ${plan.rootFolderName} (${plan.fileCount} file${plan.fileCount === 1 ? "" : "s"})`);
    await executeFolderUploadPlan(plan);
  } catch (err) {
    setUploadProgressVisible(false);
    setActionError("Folder upload", err);
  }
}

async function handleUploadSelection(event) {
  const files = Array.from(event.target.files || []);
  event.target.value = "";
  if (files.length === 0) {
    return;
  }

  const targetPath = uploadState.targetPath || activePaneState().currentPath;
  uploadState.active = true;
  uploadState.targetPath = targetPath;
  uploadState.files = files;
  uploadState.index = 0;
  uploadState.overwriteAll = false;
  uploadState.skipAll = false;
  uploadState.successfulCount = 0;
  uploadState.skippedCount = 0;
  uploadState.cancelled = false;
  uploadState.cancelRequested = false;
  setError("actions-error", "");
  setUploadModalVisible(true);
  updateUploadModalDisplay({
    targetPath,
    rootFolderName: "",
    total: files.length,
    index: 0,
  });

  try {
    outer:
    for (let index = 0; index < files.length; index += 1) {
      if (uploadState.cancelRequested) {
        uploadState.cancelled = true;
        break;
      }
      uploadState.index = index;
      updateUploadModalDisplay({
        targetPath,
        rootFolderName: "",
        total: files.length,
        index: index + 1,
        currentFileName: files[index].name,
      });
      let overwrite = uploadState.overwriteAll;
      while (true) {
        try {
          await uploadFileRequest(targetPath, files[index], overwrite);
          uploadState.successfulCount += 1;
          break;
        } catch (err) {
          if (err.code !== "already_exists") {
            throw err;
          }
          if (uploadState.skipAll) {
            uploadState.skippedCount += 1;
            break;
          }
          const choice = await promptUploadConflict(files[index].name, targetPath, err.message);
          if (choice === "overwrite") {
            overwrite = true;
            continue;
          }
          if (choice === "overwrite_all") {
            uploadState.overwriteAll = true;
            overwrite = true;
            continue;
          }
          if (choice === "skip") {
            uploadState.skippedCount += 1;
            break;
          }
          if (choice === "skip_all") {
            uploadState.skipAll = true;
            uploadState.skippedCount += 1;
            break;
          }
          uploadState.cancelRequested = true;
          uploadState.cancelled = true;
          break outer;
        }
      }
    }
    if (uploadState.successfulCount > 0) {
      await loadBrowsePane(state.activePane);
    }
    if (uploadState.cancelled) {
      setStatus(`Upload: ${uploadState.successfulCount}/${files.length} file${files.length === 1 ? "" : "s"} uploaded before cancel`);
    } else if (uploadState.skippedCount > 0) {
      setStatus(`Upload: ${uploadState.successfulCount} uploaded, ${uploadState.skippedCount} skipped`);
    } else {
      setStatus(`Upload: ${uploadState.successfulCount} file${uploadState.successfulCount === 1 ? "" : "s"} uploaded`);
    }
  } catch (err) {
    if (uploadState.successfulCount > 0) {
      await loadBrowsePane(state.activePane);
    }
    setActionError("Upload", err);
    setUploadModalStatus(err.message);
  } finally {
    resetUploadProgress();
  }
}

function setActivePane(pane) {
  state.activePane = pane;
  document.getElementById("left-pane").classList.toggle("active-pane", pane === "left");
  document.getElementById("right-pane").classList.toggle("active-pane", pane === "right");
  updateActionButtons();
}

function setSelectedItem(pane, item) {
  const model = paneState(pane);
  model.selectedItem = item;
  model.selectedItems = item ? [item] : [];
  updateActionButtons();
}

function clearSelectionAnchor(pane) {
  paneState(pane).selectionAnchorIndex = null;
}

function setSelectionAnchor(pane, index) {
  paneState(pane).selectionAnchorIndex = Number.isInteger(index) ? index : null;
}

function selectedPaths(pane) {
  return paneState(pane).selectedItems.map((item) => item.path);
}

function setSingleSelection(pane, item) {
  setSelectedItem(pane, item);
}

function setSingleSelectionAtIndex(pane, item, index) {
  setSingleSelection(pane, item);
  setSelectionAnchor(pane, index);
}

function toggleSelection(pane, item) {
  const model = paneState(pane);
  const index = model.selectedItems.findIndex((selected) => selected.path === item.path);
  if (index >= 0) {
    const removed = model.selectedItems[index];
    model.selectedItems.splice(index, 1);
    if (model.selectedItem && model.selectedItem.path === removed.path) {
      model.selectedItem = model.selectedItems.length > 0 ? model.selectedItems[model.selectedItems.length - 1] : null;
    }
  } else {
    model.selectedItems.push(item);
    model.selectedItem = item;
  }
  updateActionButtons();
}

function toggleSelectionAtIndex(pane, item, index) {
  toggleSelection(pane, item);
  setSelectionAnchor(pane, index);
}

function selectedEntryFromItem(entry) {
  return { path: entry.path, name: entry.name, kind: entry.kind };
}

function setRangeSelection(pane, anchorIndex, currentIndex) {
  const model = paneState(pane);
  if (!Array.isArray(model.visibleItems) || model.visibleItems.length === 0) {
    return;
  }
  const start = Math.max(0, Math.min(anchorIndex, currentIndex));
  const end = Math.min(model.visibleItems.length - 1, Math.max(anchorIndex, currentIndex));
  model.selectedItems = model.visibleItems
    .slice(start, end + 1)
    .filter((entry) => !entry.isParent)
    .map((entry) => selectedEntryFromItem(entry));
  const current = model.visibleItems[currentIndex];
  model.selectedItem = current && !current.isParent ? selectedEntryFromItem(current) : (model.selectedItems[model.selectedItems.length - 1] || null);
  updateActionButtons();
}

function isMacLike() {
  const platform = navigator.platform || navigator.userAgent || "";
  return /Mac|iPhone|iPad|iPod/i.test(platform);
}

function isToggleModifierClick(event) {
  if (isMacLike()) {
    return !!event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey;
  }
  return !!event.ctrlKey && !event.metaKey && !event.shiftKey && !event.altKey;
}

function currentRowItem(pane) {
  const model = paneState(pane);
  if (!Array.isArray(model.visibleItems) || model.visibleItems.length === 0) {
    return null;
  }
  if (model.currentRowIndex < 0 || model.currentRowIndex >= model.visibleItems.length) {
    return null;
  }
  return model.visibleItems[model.currentRowIndex];
}

function thumbnailsEnabled() {
  return !!settingsState.showThumbnails;
}

function isThumbnailCandidate(entry) {
  if (!entry || entry.kind !== "file") {
    return false;
  }
  const lower = (entry.name || "").toLowerCase();
  return lower.endsWith(".jpg") || lower.endsWith(".jpeg") || lower.endsWith(".png") || lower.endsWith(".webp");
}

function iconTypeForEntry(entry) {
  if (!entry) {
    return "file";
  }
  if (entry.kind === "directory") {
    return "folder";
  }
  const name = entry.name || "";
  const lower = name.toLowerCase();

  if (lower === "dockerfile" || lower === "containerfile") {
    return "docker";
  }
  if (["jpg", "jpeg", "png", "webp", "gif", "bmp", "avif"].some((ext) => lower.endsWith(`.${ext}`))) {
    return "image";
  }
  if (["mp4", "mkv", "mov", "avi", "webm"].some((ext) => lower.endsWith(`.${ext}`))) {
    return "video";
  }
  if (lower.endsWith(".pdf")) {
    return "pdf";
  }
  if (["md", "markdown"].some((ext) => lower.endsWith(`.${ext}`))) {
    return "markdown";
  }
  if (lower.endsWith(".json")) {
    return "json";
  }
  if (["yaml", "yml"].some((ext) => lower.endsWith(`.${ext}`))) {
    return "yaml";
  }
  if (lower.endsWith(".css")) {
    return "css";
  }
  if (["js", "mjs", "cjs"].some((ext) => lower.endsWith(`.${ext}`))) {
    return "javascript";
  }
  if (["ts", "tsx"].some((ext) => lower.endsWith(`.${ext}`))) {
    return "typescript";
  }
  if (["html", "htm"].some((ext) => lower.endsWith(`.${ext}`))) {
    return "html";
  }
  if (lower.endsWith(".xml")) {
    return "xml";
  }
  if (["sh", "bash", "zsh", "fish"].some((ext) => lower.endsWith(`.${ext}`))) {
    return "shell";
  }
  if (lower.endsWith(".py")) {
    return "python";
  }
  if (["txt", "log", "ini", "cfg", "conf"].some((ext) => lower.endsWith(`.${ext}`))) {
    return "text";
  }
  return "file";
}

function mediaIconSvg(type) {
  const icons = {
    folder: '<svg viewBox="0 0 24 24" class="entry-media-svg is-filled" aria-hidden="true"><path d="M3.5 7.5a2 2 0 0 1 2-2h4l2 2h7a2 2 0 0 1 2 2v7a2.5 2.5 0 0 1-2.5 2.5H6a2.5 2.5 0 0 1-2.5-2.5z"/><path d="M3.5 9h17" class="entry-media-detail"/></svg>',
    file: '<svg viewBox="0 0 24 24" class="entry-media-svg is-filled" aria-hidden="true"><path d="M7 3.5h6l4 4v12a1.5 1.5 0 0 1-1.5 1.5h-8A1.5 1.5 0 0 1 6 19.5V5A1.5 1.5 0 0 1 7.5 3.5z"/><path d="M13 3.5V8h4.5" class="entry-media-detail"/></svg>',
    image: '<svg viewBox="0 0 24 24" class="entry-media-svg is-filled" aria-hidden="true"><rect x="4" y="5" width="16" height="14" rx="2"/><circle cx="9" cy="10" r="1.6" class="entry-media-detail-solid"/><path d="M6.5 17l4.2-4.3 2.8 2.8 1.9-2L18.5 17z" class="entry-media-detail"/></svg>',
    video: '<svg viewBox="0 0 24 24" class="entry-media-svg is-filled" aria-hidden="true"><rect x="4" y="6" width="11" height="12" rx="2"/><path d="M16 10.2l4-2.2v8l-4-2.2z"/><path d="M9.2 10l3.8 2.1-3.8 2.1z" class="entry-media-detail-solid"/></svg>',
    pdf: '<svg viewBox="0 0 24 24" class="entry-media-svg is-filled" aria-hidden="true"><path d="M7 3.5h6l4 4v12a1.5 1.5 0 0 1-1.5 1.5h-8A1.5 1.5 0 0 1 6 19.5V5A1.5 1.5 0 0 1 7.5 3.5z"/><path d="M8 16h8M8.2 12.4h2.1a1.4 1.4 0 1 0 0-2.8H8.2zM12.2 9.6v5.6h1.6a1.9 1.9 0 0 0 0-3.8h-1.6M17 9.6h-2.6v5.6M14.8 12.2h1.7" class="entry-media-detail"/></svg>',
    text: '<svg viewBox="0 0 24 24" class="entry-media-svg" aria-hidden="true"><path d="M7 3.5h6l4 4v12a1.5 1.5 0 0 1-1.5 1.5h-8A1.5 1.5 0 0 1 6 19.5V5A1.5 1.5 0 0 1 7.5 3.5z"/><path d="M9 10h6"/><path d="M9 13h6"/><path d="M9 16h4"/></svg>',
    markdown: '<svg viewBox="0 0 24 24" class="entry-media-svg" aria-hidden="true"><path d="M7 3.5h6l4 4v12a1.5 1.5 0 0 1-1.5 1.5h-8A1.5 1.5 0 0 1 6 19.5V5A1.5 1.5 0 0 1 7.5 3.5z"/><path d="M8.5 15v-4l1.8 2 1.7-2v4"/><path d="M13.6 11.2h2.6"/><path d="M14.9 10.2v5.2"/><path d="M13.9 14.5l1 1 1-1"/></svg>',
    json: '<svg viewBox="0 0 24 24" class="entry-media-svg" aria-hidden="true"><path d="M7 3.5h6l4 4v12a1.5 1.5 0 0 1-1.5 1.5h-8A1.5 1.5 0 0 1 6 19.5V5A1.5 1.5 0 0 1 7.5 3.5z"/><path d="M10 9c-.9.6-1.3 1.5-1.3 3s.4 2.4 1.3 3"/><path d="M14 9c.9.6 1.3 1.5 1.3 3s-.4 2.4-1.3 3"/><circle cx="12" cy="12" r=".8" fill="currentColor" stroke="none"/></svg>',
    yaml: '<svg viewBox="0 0 24 24" class="entry-media-svg" aria-hidden="true"><path d="M7 3.5h6l4 4v12a1.5 1.5 0 0 1-1.5 1.5h-8A1.5 1.5 0 0 1 6 19.5V5A1.5 1.5 0 0 1 7.5 3.5z"/><path d="M8.5 9.5l2 2.5v3"/><path d="M12 9.5l2 2.5"/><path d="M14.5 12.8V15"/><path d="M8.5 16h7"/></svg>',
    css: '<svg viewBox="0 0 24 24" class="entry-media-svg" aria-hidden="true"><path d="M7 3.5h6l4 4v12a1.5 1.5 0 0 1-1.5 1.5h-8A1.5 1.5 0 0 1 6 19.5V5A1.5 1.5 0 0 1 7.5 3.5z"/><path d="M14.5 9.8a2.4 2.4 0 1 0 0 4.8"/><path d="M9.5 9.8a2.4 2.4 0 1 1 0 4.8"/></svg>',
    javascript: '<svg viewBox="0 0 24 24" class="entry-media-svg" aria-hidden="true"><path d="M7 3.5h6l4 4v12a1.5 1.5 0 0 1-1.5 1.5h-8A1.5 1.5 0 0 1 6 19.5V5A1.5 1.5 0 0 1 7.5 3.5z"/><path d="M10.4 9.7v4.8c0 1-.5 1.5-1.4 1.5"/><path d="M13.8 9.8c1 0 1.8.4 1.8 1.3 0 .8-.6 1.1-1.5 1.3-.8.2-1.4.4-1.4 1.2 0 .7.6 1.2 1.6 1.2"/></svg>',
    typescript: '<svg viewBox="0 0 24 24" class="entry-media-svg" aria-hidden="true"><path d="M7 3.5h6l4 4v12a1.5 1.5 0 0 1-1.5 1.5h-8A1.5 1.5 0 0 1 6 19.5V5A1.5 1.5 0 0 1 7.5 3.5z"/><path d="M8.5 9.8h4"/><path d="M10.5 9.8v5.4"/><path d="M14.2 9.8c.9 0 1.7.4 1.7 1.3 0 .8-.6 1.1-1.4 1.3-.8.2-1.4.4-1.4 1.2 0 .7.6 1.2 1.6 1.2"/></svg>',
    html: '<svg viewBox="0 0 24 24" class="entry-media-svg" aria-hidden="true"><path d="M7 3.5h6l4 4v12a1.5 1.5 0 0 1-1.5 1.5h-8A1.5 1.5 0 0 1 6 19.5V5A1.5 1.5 0 0 1 7.5 3.5z"/><path d="M9.5 10.2l-1.7 1.8 1.7 1.8"/><path d="M14.5 10.2l1.7 1.8-1.7 1.8"/><path d="M13.1 9.5l-2.2 5"/></svg>',
    xml: '<svg viewBox="0 0 24 24" class="entry-media-svg" aria-hidden="true"><path d="M7 3.5h6l4 4v12a1.5 1.5 0 0 1-1.5 1.5h-8A1.5 1.5 0 0 1 6 19.5V5A1.5 1.5 0 0 1 7.5 3.5z"/><path d="M9.4 10.3l-1.5 1.7 1.5 1.7"/><path d="M14.6 10.3l1.5 1.7-1.5 1.7"/><path d="M11.8 9.5l.7 5"/></svg>',
    shell: '<svg viewBox="0 0 24 24" class="entry-media-svg" aria-hidden="true"><rect x="4" y="6" width="16" height="12" rx="2"/><path d="M8 10l2 2-2 2"/><path d="M12 15h4"/></svg>',
    python: '<svg viewBox="0 0 24 24" class="entry-media-svg" aria-hidden="true"><path d="M12 5c-3.8 0-3.6 1.7-3.6 1.7V9h7.3V6.7S15.8 5 12 5z"/><circle cx="10" cy="7" r=".8" fill="currentColor" stroke="none"/><path d="M12 19c3.8 0 3.6-1.7 3.6-1.7V15H8.3v2.3S8.2 19 12 19z"/><circle cx="14" cy="17" r=".8" fill="currentColor" stroke="none"/><path d="M8.3 9v3H7c-1.8 0-1.8 1.7-1.8 1.7S5 17 8.7 17"/><path d="M15.7 15v-3H17c1.8 0 1.8-1.7 1.8-1.7S19 7 15.3 7"/></svg>',
    docker: '<svg viewBox="0 0 24 24" class="entry-media-svg" aria-hidden="true"><path d="M7 10h2v2H7z"/><path d="M10 10h2v2h-2z"/><path d="M13 10h2v2h-2z"/><path d="M10 7.5h2V10h-2z"/><path d="M13 7.5h2V10h-2z"/><path d="M16.2 10.7c.4-1 .3-2-.3-2.8.9-.1 1.8.2 2.4.8.6.6.9 1.4.8 2.3.7.1 1.3.7 1.6 1.3-.4.4-1 .7-1.6.8-.5 2.3-2.3 3.8-5 3.8H9.2c-2.2 0-4-1.8-4-4v-1.2h11z"/></svg>',
  };
  return icons[type] || icons.file;
}

function createMediaSlot(entry) {
  const slot = document.createElement("span");
  slot.className = "entry-media-slot";
  if (thumbnailsEnabled() && isThumbnailCandidate(entry)) {
    const image = document.createElement("img");
    image.className = "entry-thumbnail";
    image.loading = "lazy";
    image.alt = "";
    image.src = `/api/files/thumbnail?${new URLSearchParams({ path: entry.path }).toString()}`;
    slot.append(image);
    return slot;
  }

  const icon = document.createElement("span");
  const iconType = iconTypeForEntry(entry);
  icon.className = `entry-media-icon ${iconType}`;
  icon.setAttribute("aria-hidden", "true");
  icon.innerHTML = mediaIconSvg(iconType);
  slot.append(icon);
  return slot;
}

function createSelectionSlot(pane, entry, index) {
  const slot = document.createElement("span");
  slot.className = "entry-select-slot";

  if (entry.isParent) {
    const placeholder = document.createElement("span");
    placeholder.className = "entry-select-toggle is-disabled";
    placeholder.setAttribute("aria-hidden", "true");
    const indicator = document.createElement("span");
    indicator.className = "entry-select-indicator";
    placeholder.append(indicator);
    slot.append(placeholder);
    return slot;
  }

  const button = document.createElement("button");
  button.type = "button";
  button.className = "entry-select-toggle";
  const selected = selectedPaths(pane).includes(entry.path);
  if (selected) {
    button.classList.add("is-selected");
  }
  button.setAttribute("aria-label", `${selected ? "Deselect" : "Select"} ${entry.name}`);
  button.setAttribute("aria-pressed", selected ? "true" : "false");
  const indicator = document.createElement("span");
  indicator.className = "entry-select-indicator";
  button.append(indicator);
  button.onclick = (event) => {
    event.preventDefault();
    event.stopPropagation();
    setActivePane(pane);
    paneState(pane).currentRowIndex = index;
    toggleSelectionAtIndex(pane, selectedEntryFromItem(entry), index);
    renderPaneItems(pane);
  };
  slot.append(button);
  return slot;
}

async function loadSettings() {
  const data = await apiRequest("GET", "/api/settings");
  settingsState.showThumbnails = !!data.show_thumbnails;
  settingsState.preferredStartupPathLeft = data.preferred_startup_path_left || null;
  settingsState.preferredStartupPathRight = data.preferred_startup_path_right || null;
  settingsState.selectedTheme = VALID_THEME_FAMILIES.includes(data.selected_theme) ? data.selected_theme : "default";
  settingsState.selectedColorMode = VALID_COLOR_MODES.includes(data.selected_color_mode) ? data.selected_color_mode : "dark";
  settingsState.zipDownloadLimits = data.zip_download_limits || null;
  const elements = settingsElements();
  if (elements.showThumbnailsInput) {
    elements.showThumbnailsInput.checked = settingsState.showThumbnails;
  }
  if (elements.startupPathLeftInput) {
    elements.startupPathLeftInput.value = settingsState.preferredStartupPathLeft || "";
  }
  if (elements.startupPathRightInput) {
    elements.startupPathRightInput.value = settingsState.preferredStartupPathRight || "";
  }
  if (elements.selectedThemeInput) {
    elements.selectedThemeInput.value = settingsState.selectedTheme;
  }
  renderDownloadSettings();
}

async function saveSettings(update) {
  const data = await apiRequest("POST", "/api/settings", update);
  settingsState.showThumbnails = !!data.show_thumbnails;
  settingsState.preferredStartupPathLeft = data.preferred_startup_path_left || null;
  settingsState.preferredStartupPathRight = data.preferred_startup_path_right || null;
  settingsState.selectedTheme = VALID_THEME_FAMILIES.includes(data.selected_theme) ? data.selected_theme : "default";
  settingsState.selectedColorMode = VALID_COLOR_MODES.includes(data.selected_color_mode) ? data.selected_color_mode : "dark";
  settingsState.zipDownloadLimits = data.zip_download_limits || null;
  const elements = settingsElements();
  if (elements.showThumbnailsInput) {
    elements.showThumbnailsInput.checked = settingsState.showThumbnails;
  }
  if (elements.startupPathLeftInput) {
    elements.startupPathLeftInput.value = settingsState.preferredStartupPathLeft || "";
  }
  if (elements.startupPathRightInput) {
    elements.startupPathRightInput.value = settingsState.preferredStartupPathRight || "";
  }
  if (elements.selectedThemeInput) {
    elements.selectedThemeInput.value = settingsState.selectedTheme;
  }
  renderDownloadSettings();
  applyTheme(settingsState.selectedTheme, settingsState.selectedColorMode);
  renderPaneItems("left");
  renderPaneItems("right");
  return data;
}

function updateActionButtons() {
  const selectedItems = activePaneState().selectedItems;
  const count = selectedItems.length;
  const hasSelection = count > 0;
  const exactlyOne = count === 1;
  const allFiles = hasSelection && selectedItems.every((item) => item.kind === "file");
  const remoteBrowse = isRemoteBrowsePath(activePaneState().currentPath);
  const remoteViewable = exactlyOne && isRemoteViewableSelection(selectedItems[0] || null);
  document.getElementById("view-btn").disabled = remoteBrowse ? !remoteViewable : !exactlyOne || !allFiles;
  document.getElementById("edit-btn").disabled = remoteBrowse || !exactlyOne || !allFiles || !isEditableSelection(selectedItems[0] || null);
  document.getElementById("rename-btn").disabled = remoteBrowse || !exactlyOne;
  document.getElementById("delete-btn").disabled = remoteBrowse || !hasSelection;
  document.getElementById("copy-btn").disabled = remoteBrowse || !hasSelection;
  document.getElementById("move-btn").disabled = remoteBrowse || !hasSelection;
  document.getElementById("mkdir-btn").disabled = remoteBrowse;
  document.getElementById("upload-btn").disabled = remoteBrowse;
  document.getElementById("upload-menu-toggle").disabled = remoteBrowse;
  document.getElementById("upload-folder-btn").disabled = remoteBrowse;
}

function isEditableSelection(item) {
  if (!item || item.kind !== "file") {
    return false;
  }
  const name = item.name || "";
  const lower = name.toLowerCase();
  if (lower === "dockerfile" || lower === "containerfile") {
    return true;
  }
  return [".txt", ".log", ".md", ".yml", ".yaml", ".json", ".js", ".py", ".css", ".html", ".conf"].some((suffix) => lower.endsWith(suffix));
}

function formatBinarySize(bytes) {
  const value = Number(bytes);
  if (!Number.isFinite(value) || value < 0) {
    return "-";
  }
  if (value < 1024) {
    return `${value} B`;
  }
  const units = ["KiB", "MiB", "GiB", "TiB"];
  let scaled = value;
  let unitIndex = -1;
  do {
    scaled /= 1024;
    unitIndex += 1;
  } while (scaled >= 1024 && unitIndex < units.length - 1);
  const digits = scaled >= 10 || unitIndex === 0 ? 0 : 1;
  return `${scaled.toFixed(digits)} ${units[unitIndex]}`;
}

function formatSeconds(seconds) {
  const value = Number(seconds);
  if (!Number.isFinite(value) || value < 0) {
    return "-";
  }
  return `${value % 1 === 0 ? value.toFixed(0) : value.toFixed(1)} seconds`;
}

function formatSymlinkPolicy(policy) {
  return policy === "not_allowed" ? "Rejected / not allowed" : (policy || "-");
}

function renderDownloadSettings() {
  const elements = settingsElements();
  const limits = settingsState.zipDownloadLimits || {};
  if (elements.downloadMaxItems) {
    elements.downloadMaxItems.textContent = limits.max_items ? `${limits.max_items} items` : "-";
  }
  if (elements.downloadMaxTotalSize) {
    elements.downloadMaxTotalSize.textContent = formatBinarySize(limits.max_total_input_bytes);
  }
  if (elements.downloadMaxFileSize) {
    elements.downloadMaxFileSize.textContent = formatBinarySize(limits.max_individual_file_bytes);
  }
  if (elements.downloadScanTimeout) {
    elements.downloadScanTimeout.textContent = formatSeconds(limits.scan_timeout_seconds);
  }
  if (elements.downloadSymlinkPolicy) {
    elements.downloadSymlinkPolicy.textContent = formatSymlinkPolicy(limits.symlink_policy);
  }
}

function monacoLanguageForName(name) {
  const lower = (name || "").toLowerCase();
  if (lower === "dockerfile" || lower === "containerfile") {
    return "dockerfile";
  }
  if ([".js", ".mjs", ".cjs"].some((suffix) => lower.endsWith(suffix))) {
    return "javascript";
  }
  if ([".ts", ".tsx"].some((suffix) => lower.endsWith(suffix))) {
    return "typescript";
  }
  if (lower.endsWith(".json")) {
    return "json";
  }
  if (lower.endsWith(".css")) {
    return "css";
  }
  if ([".html", ".htm"].some((suffix) => lower.endsWith(suffix))) {
    return "html";
  }
  if (lower.endsWith(".xml")) {
    return "xml";
  }
  if ([".yml", ".yaml"].some((suffix) => lower.endsWith(suffix))) {
    return "yaml";
  }
  if (lower.endsWith(".py")) {
    return "python";
  }
  if ([".sh", ".bash", ".zsh", ".fish"].some((suffix) => lower.endsWith(suffix))) {
    return "shell";
  }
  if ([".md", ".markdown"].some((suffix) => lower.endsWith(suffix))) {
    return "markdown";
  }
  if ([".txt", ".log", ".ini", ".cfg", ".conf"].some((suffix) => lower.endsWith(suffix))) {
    return "plaintext";
  }
  return "plaintext";
}

function isVideoSelection(item) {
  if (!item || item.kind !== "file") {
    return false;
  }
  const lower = (item.name || "").toLowerCase();
  return lower.endsWith(".mp4") || lower.endsWith(".mkv");
}

function isPdfSelection(item) {
  if (!item || item.kind !== "file") {
    return false;
  }
  return (item.name || "").toLowerCase().endsWith(".pdf");
}

function isImageSelection(item) {
  if (!item || item.kind !== "file") {
    return false;
  }
  const lower = (item.name || "").toLowerCase();
  return [".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".avif"].some((suffix) => lower.endsWith(suffix));
}

function currentImageScale() {
  return Number.isFinite(imageViewerState.scale) ? imageViewerState.scale : 1;
}

function applyImageScale() {
  const image = imageElements().image;
  image.style.transform = `scale(${currentImageScale()})`;
}

function resetImageViewerState() {
  imageViewerState = {
    scale: 1,
    fitScale: 1,
    path: null,
    resizeHandler: null,
  };
}

function currentParentPath(path) {
  const normalized = (path || "").trim();
  if (!normalized) {
    return null;
  }
  if (normalized === "/Volumes" || normalized === "/Clients") {
    return null;
  }
  if (normalized.startsWith("/")) {
    const segments = normalized.split("/").filter(Boolean);
    if (segments.length <= 1) {
      return null;
    }
    if (segments.length === 2) {
      return `/${segments[0]}`;
    }
    return `/${segments.slice(0, -1).join("/")}`;
  }
  if (!normalized.includes("/")) {
    return null;
  }
  const segments = normalized.split("/");
  if (segments.length === 2) {
    return segments[0];
  }
  return segments.slice(0, -1).join("/");
}

function navigateToParent(pane) {
  const model = paneState(pane);
  const childPath = model.currentPath;
  const parentPath = currentParentPath(childPath);
  if (!parentPath) {
    return;
  }
  prepareParentReturnRestore(pane, childPath);
  navigateTo(pane, parentPath);
}

function prepareParentReturnRestore(pane, childPath) {
  const model = paneState(pane);
  model.returnFocusName = baseName(childPath);
}

function baseName(path) {
  const index = path.lastIndexOf("/");
  return index >= 0 ? path.slice(index + 1) : path;
}

function rootKeyFromPath(path) {
  const normalized = (path || "").trim();
  if (!normalized) {
    return null;
  }
  if (normalized.startsWith("/")) {
    const segments = normalized.split("/").filter(Boolean);
    if (segments.length < 2) {
      return normalized;
    }
    return `/${segments[0]}/${segments[1]}`;
  }
  return normalized.split("/")[0];
}

function isNestedPath(sourcePath, destinationPath) {
  const source = (sourcePath || "").replace(/\/+$/, "");
  const destination = (destinationPath || "").replace(/\/+$/, "");
  if (!source || !destination) {
    return false;
  }
  return destination.startsWith(`${source}/`);
}

function uniqueRootKeysForItems(items) {
  return [...new Set(items.map((item) => rootKeyFromPath(item.path)).filter(Boolean))];
}

function renderBreadcrumbs(pane, path) {
  const nav = document.getElementById(`${pane}-breadcrumbs`);
  nav.innerHTML = "";
  const normalized = (path || "").trim();
  const isHostPath = normalized.startsWith("/");
  const parts = normalized.split("/").filter(Boolean);
  if (isHostPath) {
    const rootTarget = parts.length > 0 ? `/${parts[0]}` : "/Volumes";
    const rootCrumb = createButton("/", () => {
      setActivePane(pane);
      navigateTo(pane, rootTarget);
    });
    rootCrumb.type = "button";
    rootCrumb.onclick = (ev) => {
      ev.preventDefault();
      ev.stopPropagation();
      setActivePane(pane);
      navigateTo(pane, rootTarget);
    };
    nav.append(rootCrumb);
    if (parts.length > 0) {
      const sep = document.createElement("span");
      sep.textContent = "/";
      nav.append(sep);
    }
  }
  let aggregate = isHostPath ? "" : "";
  for (let i = 0; i < parts.length; i += 1) {
    aggregate = isHostPath ? `/${parts.slice(0, i + 1).join("/")}` : (i === 0 ? parts[i] : `${aggregate}/${parts[i]}`);
    const crumbPath = aggregate;
    const crumb = createButton(parts[i], () => {
      setActivePane(pane);
      navigateTo(pane, crumbPath);
    });
    crumb.type = "button";
    crumb.onclick = (ev) => {
      ev.preventDefault();
      ev.stopPropagation();
      setActivePane(pane);
      navigateTo(pane, crumbPath);
    };
    nav.append(crumb);
    if (i < parts.length - 1) {
      const sep = document.createElement("span");
      sep.textContent = "/";
      nav.append(sep);
    }
  }
}

function formatModified(isoString) {
  if (!isoString) {
    return "-";
  }
  const d = new Date(isoString);
  if (Number.isNaN(d.getTime())) {
    return isoString;
  }
  const date = d.toLocaleDateString(undefined, { year: "2-digit", month: "2-digit", day: "2-digit" });
  const time = d.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" });
  return `${date} ${time}`;
}

function formatFileSize(bytes) {
  if (typeof bytes !== "number" || !Number.isFinite(bytes) || bytes < 0) {
    return "-";
  }
  if (bytes < 1024) {
    return `${bytes} B`;
  }
  if (bytes < 1024 ** 2) {
    return `${Math.round(bytes / 1024)} KB`;
  }
  if (bytes < 1024 ** 3) {
    return `${(bytes / (1024 ** 2)).toFixed(1)} MB`;
  }
  if (bytes < 1024 ** 4) {
    return `${(bytes / (1024 ** 3)).toFixed(1)} GB`;
  }
  return `${(bytes / (1024 ** 4)).toFixed(1)} TB`;
}

function createBrowseItem(pane, entry, kind, index) {
  const li = document.createElement("li");
  li.className = "selectable";
  li.dataset.path = entry.path;
  const paths = selectedPaths(pane);
  const model = paneState(pane);
  if (paths.includes(entry.path)) {
    li.classList.add("is-selected");
  }
  if (model.currentRowIndex >= 0 && model.visibleItems[model.currentRowIndex]?.path === entry.path) {
    li.classList.add("is-current-row");
  }

  li.onclick = () => {
    setActivePane(pane);
    setSingleSelection(pane, { path: entry.path, name: entry.name, kind });
    loadBrowsePane(pane);
  };

  const name = document.createElement("span");
  name.className = `entry-name ${kind === "directory" ? "entry-dir" : "entry-file"}`;
  name.append(createSelectionSlot(pane, { ...entry, kind }, index));
  name.append(createMediaSlot({ ...entry, kind }));

  if (kind === "directory") {
    const open = document.createElement("button");
    open.className = "dir-link";
    open.textContent = `${entry.name}/`;
    open.type = "button";
    open.onclick = (ev) => {
      ev.stopPropagation();
      setActivePane(pane);
      navigateTo(pane, entry.path);
    };
    open.classList.add("entry-label");
    name.append(open);
  } else {
    const fileName = document.createElement("span");
    fileName.textContent = entry.name;
    fileName.className = "entry-label";
    fileName.onclick = (ev) => {
      ev.stopPropagation();
      setActivePane(pane);
      setSingleSelection(pane, { path: entry.path, name: entry.name, kind });
      loadBrowsePane(pane);
    };
    name.append(fileName);
  }
  li.append(name);

  const size = document.createElement("span");
  size.className = "entry-size";
  size.textContent = kind === "directory" ? "-" : formatFileSize(entry.size);
  li.append(size);

  const modified = document.createElement("span");
  modified.className = "entry-modified";
  modified.textContent = formatModified(entry.modified);
  li.append(modified);
  return li;
}

function scrollCurrentRowIntoView(pane) {
  const model = paneState(pane);
  if (model.currentRowIndex < 0) {
    return;
  }
  const list = document.getElementById(`${pane}-items`);
  const row = list.querySelector(`li[data-row-index="${model.currentRowIndex}"]`);
  if (row) {
    row.scrollIntoView({ block: "nearest" });
  }
}

function updatePaneFocusLine(pane) {
  const model = paneState(pane);
  const focusLine = document.getElementById(`${pane}-focus-line`);
  const nameEl = document.getElementById(`${pane}-focus-name`);
  const selectedEl = document.getElementById(`${pane}-focus-selected`);
  if (!focusLine || !nameEl || !selectedEl) {
    return;
  }
  let label = "—";
  if (!Array.isArray(model.visibleItems) || model.currentRowIndex < 0 || model.currentRowIndex >= model.visibleItems.length) {
    label = "—";
  } else {
    const item = model.visibleItems[model.currentRowIndex];
    if (item) {
      label = item.isParent ? "../" : (item.name || "—");
    }
  }
  nameEl.textContent = label;

  const selectedCount = Array.isArray(model.selectedItems) ? model.selectedItems.length : 0;
  if (selectedCount > 0) {
    selectedEl.textContent = `Selected: ${selectedCount} ${selectedCount === 1 ? "item" : "items"}`;
    selectedEl.classList.remove("hidden");
  } else {
    selectedEl.textContent = "";
    selectedEl.classList.add("hidden");
  }
}

function renderPaneItems(pane) {
  closeContextMenu();
  const model = paneState(pane);
  const items = document.getElementById(`${pane}-items`);
  items.innerHTML = "";

  if (!Array.isArray(model.visibleItems) || model.visibleItems.length === 0) {
    model.currentRowIndex = -1;
    updatePaneFocusLine(pane);
    updateActionButtons();
    return;
  }
  if (model.currentRowIndex < 0 || model.currentRowIndex >= model.visibleItems.length) {
    model.currentRowIndex = 0;
  }

  model.visibleItems.forEach((entry, index) => {
    if (entry.isParent) {
      const up = document.createElement("li");
      up.className = "selectable";
      up.dataset.rowIndex = String(index);
      up.dataset.path = entry.path;
      if (index === model.currentRowIndex) {
        up.classList.add("is-current-row");
      }
      up.onclick = () => {
        setActivePane(pane);
        model.currentRowIndex = index;
        clearSelectionAnchor(pane);
        renderPaneItems(pane);
      };
      up.oncontextmenu = (event) => {
        event.preventDefault();
      };
      const upNameCell = document.createElement("span");
      upNameCell.className = "entry-name entry-dir";
      upNameCell.append(createSelectionSlot(pane, { ...entry, isParent: true }, index));
      upNameCell.append(createMediaSlot({ name: "..", path: entry.path, kind: "directory" }));
      const upName = document.createElement("button");
      upName.type = "button";
      upName.className = "dir-link";
      upName.textContent = "../";
      upName.onclick = (ev) => {
        ev.stopPropagation();
        setActivePane(pane);
        navigateToParent(pane);
      };
      upName.classList.add("entry-label");
      upNameCell.append(upName);
      up.append(upNameCell);
      const upSize = document.createElement("span");
      upSize.className = "entry-size";
      upSize.textContent = "-";
      up.append(upSize);
      const upModified = document.createElement("span");
      upModified.className = "entry-modified";
      upModified.textContent = "-";
      up.append(upModified);
      items.append(up);
      return;
    }

    const row = createBrowseItem(pane, entry, entry.kind, index);
    row.dataset.rowIndex = String(index);
    if (index === model.currentRowIndex) {
      row.classList.add("is-current-row");
    }
    row.onclick = () => {
      setActivePane(pane);
      model.currentRowIndex = index;
      setSingleSelectionAtIndex(pane, { path: entry.path, name: entry.name, kind: entry.kind }, index);
      renderPaneItems(pane);
    };
    const dirLink = row.querySelector(".dir-link");
    if (dirLink) {
      dirLink.onclick = (ev) => {
        ev.stopPropagation();
        setActivePane(pane);
        navigateTo(pane, entry.path);
      };
    }
    const fileName = row.querySelector(".entry-file .entry-label");
    if (fileName) {
      fileName.onclick = (ev) => {
        ev.stopPropagation();
        setActivePane(pane);
        model.currentRowIndex = index;
        if (isToggleModifierClick(ev)) {
          toggleSelectionAtIndex(pane, { path: entry.path, name: entry.name, kind: entry.kind }, index);
        } else {
          setSingleSelectionAtIndex(pane, { path: entry.path, name: entry.name, kind: entry.kind }, index);
        }
        renderPaneItems(pane);
      };
    }
    row.onclick = (ev) => {
      setActivePane(pane);
      model.currentRowIndex = index;
      if (isToggleModifierClick(ev)) {
        toggleSelectionAtIndex(pane, { path: entry.path, name: entry.name, kind: entry.kind }, index);
      } else {
        setSingleSelectionAtIndex(pane, { path: entry.path, name: entry.name, kind: entry.kind }, index);
      }
      renderPaneItems(pane);
    };
    row.oncontextmenu = (event) => {
      event.preventDefault();
      event.stopPropagation();
      setActivePane(pane);
      openContextMenu(pane, entry, event);
    };
    if (entry.kind === "file" && isImageSelection({ path: entry.path, name: entry.name, kind: entry.kind })) {
      row.ondblclick = (ev) => {
        ev.stopPropagation();
        setActivePane(pane);
        model.currentRowIndex = index;
        setSingleSelectionAtIndex(pane, { path: entry.path, name: entry.name, kind: entry.kind }, index);
        renderPaneItems(pane);
        openImageViewer();
      };
    } else if (entry.kind === "file" && isVideoSelection({ path: entry.path, name: entry.name, kind: entry.kind })) {
      row.ondblclick = (ev) => {
        ev.stopPropagation();
        setActivePane(pane);
        model.currentRowIndex = index;
        setSingleSelectionAtIndex(pane, { path: entry.path, name: entry.name, kind: entry.kind }, index);
        renderPaneItems(pane);
        openVideoViewer();
      };
    } else if (entry.kind === "file" && isPdfSelection({ path: entry.path, name: entry.name, kind: entry.kind })) {
      row.ondblclick = (ev) => {
        ev.stopPropagation();
        setActivePane(pane);
        model.currentRowIndex = index;
        setSingleSelectionAtIndex(pane, { path: entry.path, name: entry.name, kind: entry.kind }, index);
        renderPaneItems(pane);
        openPdfViewer();
      };
    }
    items.append(row);
  });
  updatePaneFocusLine(pane);
  updateActionButtons();
}

async function loadBrowsePane(pane) {
  setError(`${pane}-browse-error`, "");
  try {
    const model = paneState(pane);
    const query = new URLSearchParams({
      path: model.currentPath,
      show_hidden: String(model.showHidden),
    });
    const data = await apiRequest("GET", `/api/browse?${query.toString()}`);
    model.currentPath = data.path;
    syncSourceSwitchers();
    renderBreadcrumbs(pane, data.path);

    const visibleItems = [];
    const parent = currentParentPath(data.path);
    if (parent) {
      visibleItems.push({ path: parent, name: "..", kind: "directory", isParent: true });
    }
    for (const entry of data.directories) {
      visibleItems.push({ ...entry, kind: "directory" });
    }
    for (const entry of data.files) {
      visibleItems.push({ ...entry, kind: "file" });
    }
    model.visibleItems = visibleItems;

    const visiblePaths = new Set(visibleItems.filter((item) => !item.isParent).map((item) => item.path));
    model.selectedItems = model.selectedItems.filter((item) => visiblePaths.has(item.path));
    if (model.selectedItem && !visiblePaths.has(model.selectedItem.path)) {
      model.selectedItem = model.selectedItems.length > 0 ? model.selectedItems[model.selectedItems.length - 1] : null;
    }

    if (model.pendingSelectionPath) {
      const pendingIndex = visibleItems.findIndex((item) => !item.isParent && item.path === model.pendingSelectionPath);
      if (pendingIndex >= 0) {
        const pendingItem = visibleItems[pendingIndex];
        model.currentRowIndex = pendingIndex;
        setSingleSelectionAtIndex(pane, selectedEntryFromItem(pendingItem), pendingIndex);
      }
      model.pendingSelectionPath = null;
    }

    renderPaneItems(pane);
    restoreParentReturnFocus(pane, visibleItems);
    scrollCurrentRowIntoView(pane);
    setStatus(`Loaded ${pane}: ${data.path}`);
  } catch (err) {
    setError(`${pane}-browse-error`, `Browse: ${err.message}`);
  }
}

function restoreParentReturnFocus(pane, visibleItems) {
  const model = paneState(pane);
  const returningFromChildName = model.returnFocusName;
  model.returnFocusName = null;
  if (!returningFromChildName) {
    return;
  }
  const returnIndex = visibleItems.findIndex((item) => !item.isParent && item.name === returningFromChildName);
  if (returnIndex < 0) {
    return;
  }
  const returnItem = visibleItems[returnIndex];
  model.currentRowIndex = returnIndex;
  renderPaneItems(pane);
}

function navigateTo(pane, path) {
  closeContextMenu();
  const model = paneState(pane);
  model.currentPath = path;
  model.currentRowIndex = 0;
  clearSelectionAnchor(pane);
  setSelectedItem(pane, null);
  syncSourceSwitchers();
  updateActionButtons();
  loadBrowsePane(pane);
}

async function createFolderForPane(pane) {
  setActivePane(pane);
  const name = await new Promise((resolve) => {
    openTextInputModal({
      title: "Create Folder",
      label: "Folder name",
      applyText: "Create",
      initialValue: "",
      onSubmit: async (rawValue, elements, cancelled) => {
        if (cancelled) {
          resolve("");
          return true;
        }
        const value = rawValue.trim();
        elements.error.textContent = "";
        if (!value) {
          elements.error.textContent = "Folder name is required";
          return false;
        }
        resolve(value);
        return true;
      },
    });
  });
  if (!name) {
    return;
  }
  setError(`${pane}-browse-error`, "");
  try {
    await apiRequest("POST", "/api/files/mkdir", {
      parent_path: paneState(pane).currentPath,
      name,
    });
    await loadBrowsePane(pane);
  } catch (err) {
    setError(`${pane}-browse-error`, `Create folder: ${err.message}`);
  }
}

async function createFolderForActivePane() {
  await createFolderForPane(state.activePane);
}

async function renameSelected() {
  const pane = state.activePane;
  const selectedItems = paneState(pane).selectedItems;
  if (selectedItems.length !== 1) {
    return;
  }
  openRenamePopup();
}

function closeDeleteConfirmModal() {
  const elements = deleteConfirmElements();
  const resolver = deleteConfirmState.resolver;
  deleteConfirmState.resolver = null;
  elements.error.textContent = "";
  elements.title.textContent = "Delete folder and contents?";
  elements.message.textContent = "This will permanently delete the folder and all files and subfolders inside it.";
  elements.path.textContent = "";
  elements.applyButton.textContent = "Delete";
  elements.overlay.classList.add("hidden");
  if (typeof resolver === "function") {
    resolver(false);
  }
}

function openConfirmModal({ title, message, path, applyText = "Confirm" }) {
  const elements = deleteConfirmElements();
  elements.error.textContent = "";
  elements.title.textContent = title;
  elements.message.textContent = message;
  elements.path.textContent = path || "";
  elements.applyButton.textContent = applyText;
  elements.overlay.classList.remove("hidden");
  elements.applyButton.focus();
  return new Promise((resolve) => {
    deleteConfirmState.resolver = resolve;
  });
}

async function executeDeleteItems(pane, items, recursivePaths) {
  try {
    let result;
    if (items.length > 1) {
      result = await apiRequest("POST", "/api/files/delete", {
        paths: items.map((item) => item.path),
        recursive_paths: Array.from(recursivePaths),
      });
      setStatus("Delete: operation started");
    } else {
      const item = items[0];
      result = await apiRequest("POST", "/api/files/delete", {
        path: item.path,
        recursive: recursivePaths.has(item.path),
      });
      setStatus("Delete: started");
    }
    state.selectedTaskId = result.task_id;
    await refreshTasksSnapshot();
  } catch (err) {
    setActionError("Delete", err);
    return;
  }
  setSelectedItem(pane, null);
  await loadBrowsePane(pane);
}

async function submitDeleteConfirmModal() {
  const resolver = deleteConfirmState.resolver;
  if (typeof resolver !== "function") {
    return;
  }
  deleteConfirmState.resolver = null;
  const elements = deleteConfirmElements();
  elements.error.textContent = "";
  elements.title.textContent = "Delete folder and contents?";
  elements.message.textContent = "This will permanently delete the folder and all files and subfolders inside it.";
  elements.path.textContent = "";
  elements.applyButton.textContent = "Delete";
  elements.overlay.classList.add("hidden");
  resolver(true);
}

async function collectDeleteRecursivePaths(selectedItems) {
  const recursivePaths = new Set();
  for (const item of selectedItems) {
    if (item.kind !== "directory") {
      continue;
    }
    const query = new URLSearchParams({
      path: item.path,
      show_hidden: "true",
    });
    const data = await apiRequest("GET", `/api/browse?${query.toString()}`);
    if ((data.directories && data.directories.length > 0) || (data.files && data.files.length > 0)) {
      recursivePaths.add(item.path);
    }
  }
  return recursivePaths;
}

async function deleteSelected() {
  const pane = state.activePane;
  const selectedItems = [...paneState(pane).selectedItems];
  if (selectedItems.length === 0) {
    return;
  }
  setError("actions-error", "");
  try {
    const recursivePaths = await collectDeleteRecursivePaths(selectedItems);
    if (recursivePaths.size > 0) {
      const confirmed = await openConfirmModal({
        title: selectedItems.length === 1 ? "Delete folder and contents?" : "Delete selected items and folder contents?",
        message: selectedItems.length === 1
          ? "This will permanently delete the folder and all files and subfolders inside it."
          : `This will permanently delete ${selectedItems.length} selected items, including all files and subfolders inside the selected folders.`,
        path: selectedItems.length === 1 ? selectedItems[0].path : `${selectedItems.length} selected items`,
        applyText: "Delete",
      });
      if (!confirmed) {
        return;
      }
      await executeDeleteItems(pane, selectedItems, recursivePaths);
      return;
    }
    const confirmed = await openConfirmModal({
      title: selectedItems.length === 1 ? "Delete item?" : "Delete selected items?",
      message: selectedItems.length === 1
        ? "This will permanently delete the selected item."
        : `This will permanently delete ${selectedItems.length} selected items.`,
      path: selectedItems.length === 1 ? selectedItems[0].path : `${selectedItems.length} selected items`,
      applyText: "Delete",
    });
    if (!confirmed) {
      return;
    }
    await executeDeleteItems(pane, selectedItems, new Set());
  } catch (err) {
    setActionError("Delete", err);
  }
}

function defaultDestination(sourcePath, targetBasePath) {
  const sourceName = baseName(sourcePath);
  return `${targetBasePath}/${sourceName}`;
}

async function startCopySelected() {
  const sourcePane = state.activePane;
  const destinationPane = otherPane(sourcePane);
  const selectedItems = [...paneState(sourcePane).selectedItems];
  if (selectedItems.length === 0) {
    return;
  }
  const baseDestination = paneState(destinationPane).currentPath;
  setError("actions-error", "");
  try {
    let result;
    if (selectedItems.length > 1) {
      result = await apiRequest("POST", "/api/files/copy", {
        sources: selectedItems.map((item) => item.path),
        destination_base: baseDestination,
      });
      setStatus("Copy: operation started");
    } else {
      const item = selectedItems[0];
      const destination = defaultDestination(item.path, baseDestination);
      result = await apiRequest("POST", "/api/files/copy", {
        source: item.path,
        destination,
      });
      setStatus("Copy: started");
    }
    state.selectedTaskId = result.task_id;
    await refreshTasksSnapshot();
  } catch (err) {
    setActionError("Copy", err);
    return;
  }
  await Promise.all([loadBrowsePane("left"), loadBrowsePane("right")]);
}

async function startMoveSelected() {
  await executeMoveSelection(paneState(otherPane(state.activePane)).currentPath);
}

async function executeMoveSelection(baseDestination) {
  const sourcePane = state.activePane;
  const selectedItems = [...paneState(sourcePane).selectedItems];
  if (selectedItems.length === 0) {
    return;
  }
  setError("actions-error", "");

  if (selectedItems.length > 1) {
    const result = await apiRequest("POST", "/api/files/move", {
      sources: selectedItems.map((item) => item.path),
      destination_base: baseDestination,
    });
    state.selectedTaskId = result.task_id;
    await refreshTasksSnapshot();
    setSelectedItem(sourcePane, null);
    await Promise.all([loadBrowsePane("left"), loadBrowsePane("right")]);
    setStatus("Move: operation started");
    return;
  }

  const item = selectedItems[0];
  const destination = defaultDestination(item.path, baseDestination);
  if (item.kind !== "file") {
    setActionError("Move", new Error("Only files are supported for single-item move"));
    return;
  }
  const result = await apiRequest("POST", "/api/files/move", {
    source: item.path,
    destination,
  });
  state.selectedTaskId = result.task_id;
  await refreshTasksSnapshot();
  setSelectedItem(sourcePane, null);
  await Promise.all([loadBrowsePane("left"), loadBrowsePane("right")]);
  setStatus("Move: started");
}

async function addBookmark() {
  const pane = state.activePane;
  const path = paneState(pane).currentPath;
  const label = await new Promise((resolve) => {
    openTextInputModal({
      title: "Add Bookmark",
      label: "Bookmark label",
      applyText: "Add",
      initialValue: path,
      onSubmit: async (rawValue, elements, cancelled) => {
        if (cancelled) {
          resolve("");
          return true;
        }
        const value = rawValue.trim();
        elements.error.textContent = "";
        if (!value) {
          elements.error.textContent = "Bookmark label is required";
          return false;
        }
        resolve(value);
        return true;
      },
    });
  });
  if (!label) {
    return;
  }
  setError("actions-error", "");
  try {
    await apiRequest("POST", "/api/bookmarks", { path, label });
    setStatus(`Bookmark added for ${path}`);
  } catch (err) {
    setActionError("Add bookmark", err);
  }
}

function shouldHandleShortcut(target) {
  if (!target || !(target instanceof Element)) {
    return true;
  }
  if (target.closest("[contenteditable='true']")) {
    return false;
  }
  const control = target.closest("input, textarea, select, button");
  if (!control) {
    return true;
  }
  if (control.tagName === "INPUT" && control.type === "checkbox") {
    return false;
  }
  return false;
}

function actionButton(id) {
  return document.getElementById(id);
}

function triggerActionButton(id) {
  const button = actionButton(id);
  if (!button || button.disabled) {
    return false;
  }
  button.click();
  return true;
}

function actionShortcutHandled(event) {
  const altOnly = event.altKey && !event.ctrlKey && !event.metaKey && !event.shiftKey;
  const noModifiers = !event.altKey && !event.ctrlKey && !event.metaKey && !event.shiftKey;

  if (noModifiers) {
    if (event.key === "F1") {
      return triggerActionButton("settings-btn");
    }
    if (event.key === "F2") {
      return triggerActionButton("rename-btn");
    }
    if (event.key === "F3") {
      return triggerActionButton("view-btn");
    }
    if (event.key === "F4") {
      return triggerActionButton("edit-btn");
    }
    if (event.key === "F5") {
      return triggerActionButton("copy-btn");
    }
    if (event.key === "F6") {
      return openF6Flow();
    }
    if (event.key === "F7") {
      return triggerActionButton("mkdir-btn");
    }
    if (event.key === "F8") {
      return triggerActionButton("delete-btn");
    }
  }

  if (altOnly) {
    const key = event.key.toLowerCase();
    if (key === "3") {
      return triggerActionButton("view-btn");
    }
    if (key === "4") {
      return triggerActionButton("edit-btn");
    }
    if (key === "5") {
      return triggerActionButton("copy-btn");
    }
    if (key === "6") {
      return openF6Flow();
    }
    if (key === "7") {
      return triggerActionButton("mkdir-btn");
    }
    if (key === "8") {
      return triggerActionButton("delete-btn");
    }
  }

  return false;
}

function wildcardPopupElements() {
  return {
    overlay: document.getElementById("wildcard-popup"),
    title: document.getElementById("wildcard-popup-title"),
    meta: document.getElementById("wildcard-popup-meta"),
    input: document.getElementById("wildcard-pattern-input"),
    error: document.getElementById("wildcard-popup-error"),
    applyButton: document.getElementById("wildcard-apply-btn"),
    cancelButton: document.getElementById("wildcard-cancel-btn"),
  };
}

function isSettingsOpen() {
  return !settingsElements().overlay.classList.contains("hidden");
}

function isWildcardPopupOpen() {
  return !wildcardPopupElements().overlay.classList.contains("hidden");
}

function isMovePopupOpen() {
  return !moveElements().overlay.classList.contains("hidden");
}

function isRenamePopupOpen() {
  return !renameElements().overlay.classList.contains("hidden");
}

function isBatchMovePopupOpen() {
  return !batchMoveElements().overlay.classList.contains("hidden");
}

function isDeleteConfirmModalOpen() {
  return !deleteConfirmElements().overlay.classList.contains("hidden");
}

function isUploadConflictModalOpen() {
  return isUploadConflictOpen();
}

function isViewerOpen() {
  return !viewerElements().overlay.classList.contains("hidden");
}

function isVideoOpen() {
  return !videoElements().overlay.classList.contains("hidden");
}

function isEditorOpen() {
  return !editorElements().overlay.classList.contains("hidden");
}

async function loadMonacoModule() {
  if (monacoState.module) {
    return monacoState.module;
  }
  if (!monacoState.loadPromise) {
    monacoState.loadPromise = import("https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/+esm")
      .then((module) => {
        monacoState.module = module;
        module.editor.setTheme(currentColorMode() === "light" ? "vs" : "vs-dark");
        return module;
      });
  }
  return monacoState.loadPromise;
}

function disposeMonacoEditor() {
  if (typeof monacoState.resizeHandler === "function") {
    window.removeEventListener("resize", monacoState.resizeHandler);
    monacoState.resizeHandler = null;
  }
  if (monacoState.editor) {
    monacoState.editor.dispose();
    monacoState.editor = null;
  }
  if (monacoState.model) {
    monacoState.model.dispose();
    monacoState.model = null;
  }
}

function nextAnimationFrame() {
  return new Promise((resolve) => window.requestAnimationFrame(() => resolve()));
}

async function ensureMonacoEditor(path, content) {
  const editor = editorElements();
  const monaco = await loadMonacoModule();
  await nextAnimationFrame();
  await nextAnimationFrame();
  disposeMonacoEditor();
  editor.host.textContent = "";
  const model = monaco.editor.createModel(content, monacoLanguageForName(baseName(path)));
  const instance = monaco.editor.create(editor.host, {
    model,
    theme: currentColorMode() === "light" ? "vs" : "vs-dark",
    language: monacoLanguageForName(baseName(path)),
    automaticLayout: false,
    lineNumbers: "on",
    minimap: { enabled: false },
    scrollBeyondLastLine: false,
    renderLineHighlight: "line",
    wordWrap: "on",
    fontSize: 13,
    roundedSelection: false,
    readOnly: false,
  });
  const resizeHandler = () => {
    if (monacoState.editor) {
      monacoState.editor.layout();
    }
  };
  window.addEventListener("resize", resizeHandler);
  monacoState.model = model;
  monacoState.editor = instance;
  monacoState.resizeHandler = resizeHandler;
  instance.layout();
  instance.focus();
}

function currentEditorContent() {
  if (monacoState.editor) {
    return monacoState.editor.getValue();
  }
  return "";
}

function escapeRegExp(text) {
  return text.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
}

function globToRegExp(pattern) {
  const escaped = pattern
    .split("*")
    .map((part) => escapeRegExp(part))
    .join(".*");
  return new RegExp(`^${escaped}$`, "i");
}

function applyWildcardSelection(mode, pattern) {
  const pane = state.activePane;
  const model = paneState(pane);
  const matcher = globToRegExp(pattern);
  const candidates = model.visibleItems.filter((entry) => !entry.isParent);
  const matches = candidates.filter((entry) => matcher.test(entry.name));
  const matchPaths = new Set(matches.map((entry) => entry.path));

  let changed = 0;
  if (mode === "select") {
    const existing = new Set(model.selectedItems.map((item) => item.path));
    for (const entry of matches) {
      if (existing.has(entry.path)) {
        continue;
      }
      model.selectedItems.push({ path: entry.path, name: entry.name, kind: entry.kind });
      changed += 1;
    }
    if (matches.length > 0) {
      const last = matches[matches.length - 1];
      model.selectedItem = { path: last.path, name: last.name, kind: last.kind };
    }
  } else {
    const before = model.selectedItems.length;
    model.selectedItems = model.selectedItems.filter((item) => !matchPaths.has(item.path));
    changed = before - model.selectedItems.length;
    if (!model.selectedItem || matchPaths.has(model.selectedItem.path)) {
      model.selectedItem = model.selectedItems.length > 0 ? model.selectedItems[model.selectedItems.length - 1] : null;
    }
  }

  renderPaneItems(pane);
  setStatus(`Wildcard ${mode}: ${matches.length} matched, ${changed} changed`);
}

function closeWildcardPopup() {
  const elements = wildcardPopupElements();
  elements.overlay.classList.add("hidden");
  elements.error.textContent = "";
  elements.input.value = "";
}

function showDirectoryMoveNotSupported() {
  const message = "Directory move is not supported in v1";
  setError("actions-error", message);
  setStatus(message);
}

function showBatchDirectoryMoveNotSupported() {
  const message = "Batch directory move is not supported in v1";
  setError("actions-error", message);
  setStatus(message);
}

function resetMoveState() {
  moveState = {
    source: null,
    destination: "",
  };
}

function resetRenameState() {
  renameState = {
    source: null,
    name: "",
    submitAction: null,
  };
}

function settleRenamePopup(value = null, cancelled = false, notify = true) {
  const elements = renameElements();
  const submitAction = renameState.submitAction;
  elements.overlay.classList.add("hidden");
  elements.error.textContent = "";
  elements.input.value = "";
  elements.title.textContent = "Rename";
  elements.label.textContent = "Name";
  elements.applyButton.textContent = "Rename";
  resetRenameState();
  if (notify && typeof submitAction === "function") {
    return submitAction(value, null, cancelled);
  }
  return undefined;
}

function closeRenamePopup() {
  settleRenamePopup(null, true);
}

function openTextInputModal({ title, label, applyText, initialValue = "", onSubmit }) {
  const elements = renameElements();
  renameState.source = null;
  renameState.name = initialValue;
  renameState.submitAction = onSubmit;
  elements.title.textContent = title;
  elements.label.textContent = label;
  elements.applyButton.textContent = applyText;
  elements.input.value = initialValue;
  elements.error.textContent = "";
  elements.overlay.classList.remove("hidden");
  elements.input.focus();
  elements.input.select();
}

function openRenamePopup() {
  const selectedItems = activePaneState().selectedItems;
  if (selectedItems.length !== 1) {
    return false;
  }
  const source = selectedItems[0];
  return openTextInputModal({
    title: "Rename",
    label: "Name",
    applyText: "Rename",
    initialValue: source.name,
    onSubmit: async (rawValue, elements, cancelled) => {
      if (cancelled) {
        return true;
      }
      const newName = rawValue.trim();
      elements.error.textContent = "";
      if (!newName) {
        elements.error.textContent = "Name is required";
        return false;
      }
      if (newName === source.name) {
        elements.error.textContent = "Name must differ from current name";
        return false;
      }
      if (newName.includes("/")) {
        elements.error.textContent = "Name cannot contain /";
        return false;
      }
      if (newName === "." || newName === "..") {
        elements.error.textContent = "Invalid name";
        return false;
      }

      try {
        await apiRequest("POST", "/api/files/rename", {
          path: source.path,
          new_name: newName,
        });
        setSelectedItem(state.activePane, null);
        await loadBrowsePane(state.activePane);
        setStatus(`Renamed ${source.path}`);
        return true;
      } catch (err) {
        elements.error.textContent = err.message;
        return false;
      }
    },
  });
}

function resetBatchMoveState() {
  batchMoveState = {
    destinationBase: "",
    count: 0,
  };
}

function closeMovePopup() {
  const elements = moveElements();
  elements.overlay.classList.add("hidden");
  elements.error.textContent = "";
  elements.input.value = "";
  resetMoveState();
}

function closeBatchMovePopup() {
  const elements = batchMoveElements();
  elements.overlay.classList.add("hidden");
  elements.error.textContent = "";
  resetBatchMoveState();
}

function openMovePopup() {
  const selectedItems = activePaneState().selectedItems;
  if (selectedItems.length !== 1) {
    return false;
  }
  const source = selectedItems[0];
  const destination = defaultDestination(source.path, paneState(otherPane(state.activePane)).currentPath);
  const elements = moveElements();
  moveState.source = source;
  moveState.destination = destination;
  elements.source.textContent = `Source: ${source.path}`;
  elements.input.value = destination;
  elements.error.textContent = "";
  elements.overlay.classList.remove("hidden");
  elements.input.focus();
  elements.input.select();
  return true;
}

function openBatchMovePopup(selectedItems) {
  if (selectedItems.length === 0) {
    return false;
  }
  const destinationBase = paneState(otherPane(state.activePane)).currentPath;
  const elements = batchMoveElements();
  batchMoveState.destinationBase = destinationBase;
  batchMoveState.count = selectedItems.length;
  elements.count.textContent = `${selectedItems.length} selected item(s)`;
  elements.destination.textContent = `Destination: ${destinationBase}`;
  elements.error.textContent = "";
  elements.overlay.classList.remove("hidden");
  elements.applyButton.focus();
  return true;
}

function openF6Flow() {
  const selectedItems = activePaneState().selectedItems;
  if (selectedItems.length === 0) {
    return false;
  }
  if (selectedItems.length === 1) {
    return openMovePopup();
  }
  if (selectedItems.some((item) => item.kind !== "file")) {
    const roots = uniqueRootKeysForItems(selectedItems);
    if (roots.length > 1) {
      const message = "Batch move requires all selected items to be in the same root";
      setError("actions-error", message);
      setStatus(message);
      return true;
    }
    return openBatchMovePopup(selectedItems);
  }
  return openBatchMovePopup(selectedItems);
}

async function submitRenamePopup() {
  const elements = renameElements();
  if (typeof renameState.submitAction !== "function") {
    return;
  }
  const shouldClose = await renameState.submitAction(elements.input.value, elements, false);
  if (shouldClose !== false) {
    settleRenamePopup("", false, false);
  }
}

async function submitMovePopup() {
  const elements = moveElements();
  const source = moveState.source;
  if (!source) {
    return;
  }
  const destination = elements.input.value.trim();
  const sourceParent = currentParentPath(source.path);
  const destinationParent = currentParentPath(destination);

  elements.error.textContent = "";
  if (!destination) {
    elements.error.textContent = "Target path is required";
    return;
  }
  if (destination === source.path) {
    elements.error.textContent = "Destination must differ from source";
    return;
  }
  if (source.kind === "directory") {
    if (isNestedPath(source.path, destination)) {
      elements.error.textContent = "Destination cannot be inside source";
      return;
    }
    if (destinationParent !== sourceParent) {
      if (rootKeyFromPath(destination) !== rootKeyFromPath(source.path)) {
        elements.error.textContent = "Cross-root directory move is not supported in v1";
        return;
      }
    }
  }

  try {
    const result = await apiRequest("POST", "/api/files/move", {
      source: source.path,
      destination,
    });
    state.selectedTaskId = result.task_id;
    await refreshTasksSnapshot();
    closeMovePopup();
    setSelectedItem(state.activePane, null);
    await Promise.all([loadBrowsePane("left"), loadBrowsePane("right")]);
    setStatus(`Move: 1 success, 0 failed`);
  } catch (err) {
    elements.error.textContent = err.message;
  }
}

async function submitBatchMovePopup() {
  const elements = batchMoveElements();
  elements.error.textContent = "";
  try {
    await executeMoveSelection(batchMoveState.destinationBase);
    closeBatchMovePopup();
  } catch (err) {
    elements.error.textContent = err.message;
  }
}

function submitWildcardPopup() {
  const elements = wildcardPopupElements();
  const pattern = elements.input.value.trim();
  if (!pattern) {
    elements.error.textContent = "Pattern is required";
    return;
  }
  try {
    applyWildcardSelection(wildcardDialogMode, pattern);
    closeWildcardPopup();
  } catch (err) {
    elements.error.textContent = `Wildcard: ${err.message}`;
  }
}

function openWildcardPopup(mode) {
  wildcardDialogMode = mode;
  const pane = state.activePane;
  const elements = wildcardPopupElements();
  elements.title.textContent = mode === "select" ? "Wildcard Select" : "Wildcard Deselect";
  elements.meta.textContent = `Active pane: ${pane} (visible items only, case-insensitive)`;
  elements.applyButton.textContent = mode === "select" ? "Select" : "Deselect";
  elements.error.textContent = "";
  elements.input.value = "";
  elements.overlay.classList.remove("hidden");
  elements.input.focus();
}

function closeViewer() {
  const viewer = viewerElements();
  viewer.overlay.classList.add("hidden");
  viewer.error.textContent = "";
  viewer.content.textContent = "";
}

function closeVideoViewer() {
  const video = videoElements();
  video.overlay.classList.add("hidden");
  video.error.textContent = "";
  video.player.pause();
  video.player.removeAttribute("src");
  video.player.load();
}

function isPdfOpen() {
  return !pdfElements().overlay.classList.contains("hidden");
}

function closePdfViewer() {
  const pdf = pdfElements();
  pdf.overlay.classList.add("hidden");
  pdf.error.textContent = "";
  pdf.frame.removeAttribute("src");
}

function isImageOpen() {
  return !imageElements().overlay.classList.contains("hidden");
}

function fitImageToViewport() {
  const image = imageElements().image;
  const viewport = imageElements().viewport;
  if (!image.naturalWidth || !image.naturalHeight) {
    return false;
  }
  if (!viewport.clientWidth || !viewport.clientHeight) {
    return false;
  }
  const widthScale = viewport.clientWidth / image.naturalWidth;
  const heightScale = viewport.clientHeight / image.naturalHeight;
  const fitScale = Math.min(widthScale, heightScale, 1);
  if (!Number.isFinite(fitScale) || fitScale <= 0) {
    return false;
  }
  imageViewerState.fitScale = fitScale;
  imageViewerState.scale = imageViewerState.fitScale;
  applyImageScale();
  return true;
}

function adjustImageZoom(multiplier) {
  if (!isImageOpen()) {
    return;
  }
  const minScale = Math.max(imageViewerState.fitScale * 0.5, 0.1);
  const maxScale = Math.max(imageViewerState.fitScale * 6, 1.5);
  imageViewerState.scale = Math.min(maxScale, Math.max(minScale, currentImageScale() * multiplier));
  applyImageScale();
}

function resetImageZoom() {
  if (!isImageOpen()) {
    return;
  }
  imageViewerState.scale = imageViewerState.fitScale;
  applyImageScale();
}

function closeImageViewer() {
  const image = imageElements();
  image.overlay.classList.add("hidden");
  image.error.textContent = "";
  image.image.removeAttribute("src");
  image.image.removeAttribute("alt");
  image.image.onload = null;
  image.image.onerror = null;
  if (imageViewerState.resizeHandler) {
    window.removeEventListener("resize", imageViewerState.resizeHandler);
  }
  resetImageViewerState();
}

function isInfoOpen() {
  return !infoElements().overlay.classList.contains("hidden");
}

function closeInfo() {
  const elements = infoElements();
  elements.overlay.classList.add("hidden");
  elements.error.textContent = "";
  elements.grid.innerHTML = "";
}

function renderInfoField(label, value) {
  const grid = infoElements().grid;
  const labelNode = document.createElement("div");
  labelNode.className = "info-label";
  labelNode.textContent = label;
  const valueNode = document.createElement("div");
  valueNode.className = "info-value";
  valueNode.textContent = value == null || value === "" ? "-" : String(value);
  grid.append(labelNode, valueNode);
}

async function openInfo() {
  const selectedItems = activePaneState().selectedItems;
  if (selectedItems.length === 0) {
    return;
  }
  const elements = infoElements();
  elements.overlay.classList.remove("hidden");
  elements.title.textContent = "Properties";
  elements.error.textContent = "";
  elements.grid.innerHTML = "";
  if (selectedItems.length > 1) {
    const fileCount = selectedItems.filter((item) => item.kind === "file").length;
    const directoryCount = selectedItems.filter((item) => item.kind === "directory").length;
    renderInfoField("Selected items", selectedItems.length);
    renderInfoField("Files", fileCount);
    renderInfoField("Directories", directoryCount);
    return;
  }
  const selected = selectedItems[0];
  try {
    const data = await apiRequest("GET", `/api/files/info?${new URLSearchParams({ path: selected.path }).toString()}`);
    renderInfoField("Name", data.name);
    renderInfoField("Path", data.path);
    renderInfoField("Type", data.type);
    renderInfoField("Size", data.size);
    renderInfoField("Modified", formatModified(data.modified));
    renderInfoField("Root", data.root);
    renderInfoField("Extension", data.extension);
    renderInfoField("Content type", data.content_type);
    renderInfoField("Owner", data.owner);
    renderInfoField("Group", data.group);
    renderInfoField("Width", data.width);
    renderInfoField("Height", data.height);
  } catch (err) {
    elements.error.textContent = err.message;
  }
}

function isSearchOpen() {
  return !searchElements().overlay.classList.contains("hidden");
}

function closeSearch() {
  const elements = searchElements();
  elements.overlay.classList.add("hidden");
  elements.error.textContent = "";
}

function renderSearchResults(items) {
  const elements = searchElements();
  elements.results.innerHTML = "";
  if (!Array.isArray(items) || items.length === 0) {
    const empty = document.createElement("div");
    empty.className = "popup-meta";
    empty.textContent = "No matches found.";
    elements.results.append(empty);
    return;
  }

  for (const item of items) {
    const row = document.createElement("button");
    row.type = "button";
    row.className = "search-result";
    row.onclick = () => activateSearchResult(item);

    const name = document.createElement("div");
    name.className = "search-result-name";
    name.textContent = item.name;

    const path = document.createElement("div");
    path.className = "search-result-path";
    path.textContent = item.parent_path;

    const meta = document.createElement("div");
    meta.className = "search-result-meta";
    meta.textContent = `${item.type} · ${item.root}`;

    row.append(name, path, meta);
    elements.results.append(row);
  }
}

function activateSearchResult(item) {
  const pane = state.activePane;
  closeSearch();
  if (item.type === "directory") {
    navigateTo(pane, item.path);
    return;
  }
  paneState(pane).pendingSelectionPath = item.path;
  navigateTo(pane, item.parent_path);
}

function openSearch() {
  const pane = state.activePane;
  const elements = searchElements();
  searchState.pane = pane;
  searchState.path = paneState(pane).currentPath;
  searchState.query = "";
  elements.context.textContent = `Searching under: ${searchState.path}`;
  elements.input.value = "";
  elements.error.textContent = "";
  elements.results.innerHTML = "";
  elements.overlay.classList.remove("hidden");
  elements.input.focus();
  elements.input.select();
}

async function submitSearch() {
  const elements = searchElements();
  const query = elements.input.value.trim();
  searchState.query = query;
  elements.error.textContent = "";
  elements.results.innerHTML = "";
  try {
    const data = await apiRequest("GET", `/api/search?${new URLSearchParams({
      path: searchState.path,
      query,
    }).toString()}`);
    renderSearchResults(data.items);
    if (data.truncated) {
      elements.error.textContent = "Result limit reached. Showing first matches.";
    }
  } catch (err) {
    elements.error.textContent = err.message;
  }
}

function setSettingsTab(tab) {
  const elements = settingsElements();
  settingsState.activeTab = tab === "logs" ? "logs" : (tab === "downloads" ? "downloads" : (tab === "interface" ? "interface" : "general"));
  const isGeneral = settingsState.activeTab === "general";
  const isInterface = settingsState.activeTab === "interface";
  const isDownloads = settingsState.activeTab === "downloads";
  const isLogs = settingsState.activeTab === "logs";
  elements.generalTab.classList.toggle("is-active", isGeneral);
  elements.generalTab.setAttribute("aria-selected", isGeneral ? "true" : "false");
  elements.interfaceTab.classList.toggle("is-active", isInterface);
  elements.interfaceTab.setAttribute("aria-selected", isInterface ? "true" : "false");
  elements.downloadsTab.classList.toggle("is-active", isDownloads);
  elements.downloadsTab.setAttribute("aria-selected", isDownloads ? "true" : "false");
  elements.logsTab.classList.toggle("is-active", isLogs);
  elements.logsTab.setAttribute("aria-selected", isLogs ? "true" : "false");
  elements.generalPanel.classList.toggle("hidden", !isGeneral);
  elements.interfacePanel.classList.toggle("hidden", !isInterface);
  elements.downloadsPanel.classList.toggle("hidden", !isDownloads);
  elements.logsPanel.classList.toggle("hidden", !isLogs);
}

function formatHistoryLine(item) {
  const timestamp = item.finished_at || item.created_at || "";
  const when = formatModified(timestamp);
  const primaryPath = item.path || [item.source, item.destination].filter(Boolean).join(" -> ");
  return {
    title: `${item.operation} · ${item.status}`,
    path: primaryPath || "-",
    meta: when,
    error: item.status === "failed" ? (item.error_message || item.error_code || "") : "",
  };
}

function formatTaskStatusLabel(task) {
  if (task.operation === "download") {
    switch (task.status) {
      case "requested":
        return "Requested";
      case "preparing":
        return "Preparing";
      case "ready":
        return "Ready for download";
      case "failed":
        return "Failed";
      case "cancelled":
        return "Cancelled";
      default:
        return task.status;
    }
  }
  switch (task.status) {
    case "queued":
      return "Queued";
    case "running":
      return "Running";
    case "cancelling":
      return "Cancelling";
    case "cancelled":
      return "Cancelled";
    case "completed":
      return "Completed";
    case "failed":
      return "Failed";
    default:
      return task.status;
  }
}

function inferDownloadTaskContext(task) {
  if (task.operation !== "download") {
    return null;
  }
  if (typeof task.destination === "string" && /^kodidownload-\d{8}-\d{6}\.zip$/.test(task.destination)) {
    return "Multi-item ZIP";
  }
  return "Directory ZIP";
}

function formatTaskLine(task) {
  const when = formatModified(task.finished_at || task.created_at || "");
  const details = [];
  const downloadContext = inferDownloadTaskContext(task);
  if (downloadContext) {
    details.push(downloadContext);
  }
  if (typeof task.done_items === "number" && typeof task.total_items === "number") {
    details.push(`${task.done_items}/${task.total_items} items`);
  }
  if (task.current_item) {
    details.push(`Current: ${task.current_item}`);
  }
  return {
    title: `${task.operation} · ${formatTaskStatusLabel(task)}`,
    path: task.destination ? `${task.destination} · ${task.source}` : task.source || "-",
    meta: [when, ...details].filter(Boolean).join(" · "),
    error: task.status === "failed" ? (task.error_message || task.error_code || "") : "",
  };
}

function isActiveTask(task) {
  return Boolean(task) && ACTIVE_OPERATION_OPERATIONS.has(task.operation) && ACTIVE_TASK_STATUSES.has(task.status);
}

function activeTasksFromItems(items) {
  return Array.isArray(items) ? items.filter((task) => isActiveTask(task)) : [];
}

function isTerminalOperationTask(task) {
  return Boolean(task) && ACTIVE_OPERATION_OPERATIONS.has(task.operation) && TERMINAL_OPERATION_STATUSES.has(task.status);
}

function statusBadgeLabel(task) {
  switch (task?.status) {
    case "queued":
      return "Queued";
    case "running":
      return "Running";
    case "cancelling":
      return "Cancelling";
    case "completed":
      return "Completed";
    case "cancelled":
      return "Cancelled";
    case "failed":
      return "Failed";
    default:
      return formatTaskStatusLabel(task);
  }
}

function terminalOperationChipLabel(items) {
  const count = Array.isArray(items) ? items.length : 0;
  if (count <= 0) {
    return "";
  }
  return `${count} recent operation${count === 1 ? "" : "s"}`;
}

function visibleOperationSortKey(task) {
  const statusOrder = {
    running: 0,
    cancelling: 1,
    queued: 2,
    completed: 3,
    cancelled: 4,
    failed: 5,
  };
  return statusOrder[task?.status] ?? 9;
}

function sortVisibleOperations(items) {
  return [...items].sort((left, right) => {
    const statusDelta = visibleOperationSortKey(left) - visibleOperationSortKey(right);
    if (statusDelta !== 0) {
      return statusDelta;
    }
    const leftTime = left.finished_at || left.created_at || "";
    const rightTime = right.finished_at || right.created_at || "";
    return rightTime.localeCompare(leftTime);
  });
}

async function refreshOperationPanes() {
  if (headerTaskState.paneRefreshPromise) {
    return headerTaskState.paneRefreshPromise;
  }
  headerTaskState.paneRefreshPromise = Promise.all([loadBrowsePane("left"), loadBrowsePane("right")])
    .catch((err) => {
      setError("actions-error", `Refresh panes: ${err.message}`);
    })
    .finally(() => {
      headerTaskState.paneRefreshPromise = null;
    });
  return headerTaskState.paneRefreshPromise;
}

function taskIsCancellable(task) {
  return Boolean(task) && ACTIVE_OPERATION_OPERATIONS.has(task.operation) && ["queued", "running"].includes(task.status);
}

async function cancelTaskRequest(taskId) {
  return apiRequest("POST", `/api/tasks/${encodeURIComponent(taskId)}/cancel`);
}

function formatTaskOperationLabel(task) {
  const operation = String(task?.operation || "");
  if (!operation) {
    return "Task";
  }
  return operation.charAt(0).toUpperCase() + operation.slice(1);
}

function hasMeaningfulItemProgress(task) {
  return typeof task?.done_items === "number" && typeof task?.total_items === "number" && task.total_items > 0;
}

function canShowChipItemProgress(task) {
  if (!hasMeaningfulItemProgress(task)) {
    return false;
  }
  return task.operation === "copy" || task.operation === "duplicate" || task.operation === "delete";
}

function compactTaskCurrentItem(task) {
  if (!task?.current_item) {
    return "";
  }
  const value = String(task.current_item).replace(/\\/g, "/");
  if (value.length <= 44) {
    return value;
  }
  const parts = value.split("/").filter(Boolean);
  if (parts.length >= 2) {
    const shortened = `.../${parts.slice(-2).join("/")}`;
    if (shortened.length <= 44) {
      return shortened;
    }
  }
  return `...${value.slice(-41)}`;
}

function activeTaskChipLabel(items) {
  const count = Array.isArray(items) ? items.length : 0;
  if (count !== 1) {
    return `${count} active operation${count === 1 ? "" : "s"}`;
  }
  const task = items[0];
  const action = formatTaskOperationLabel(task);
  if (task.status === "cancelling") {
    return `${action} cancelling`;
  }
  if (canShowChipItemProgress(task)) {
    return `${action} ${task.done_items}/${task.total_items}`;
  }
  if (task.status === "queued") {
    return `${action} queued`;
  }
  return `${action} running`;
}

function taskProgressText(task) {
  if (!hasMeaningfulItemProgress(task)) {
    return "";
  }
  return `${task.done_items}/${task.total_items}`;
}

function taskProgressSubtext(task) {
  if (task?.status === "cancelling") {
    return "Stopping after current item...";
  }
  const progress = taskProgressText(task);
  if (progress) {
    return `${progress} items processed`;
  }
  return "";
}

function headerTaskRenderKey(items) {
  return JSON.stringify(
    Array.isArray(items)
      ? items.map((task) => ({
          id: task.id || "",
          operation: task.operation || "",
          status: task.status || "",
          source: task.source || "",
          destination: task.destination || "",
          done_items: task.done_items,
          total_items: task.total_items,
          current_item: task.current_item || "",
        }))
      : []
  );
}

function shouldPollHeaderTasks() {
  return headerTaskState.popoverOpen || headerTaskState.activeItems.length > 0 || headerTaskState.recentItems.length > 0;
}

function stopHeaderTaskPolling() {
  if (headerTaskState.pollTimer) {
    window.clearTimeout(headerTaskState.pollTimer);
    headerTaskState.pollTimer = null;
  }
}

function scheduleHeaderTaskPolling() {
  stopHeaderTaskPolling();
  if (!shouldPollHeaderTasks()) {
    return;
  }
  headerTaskState.pollTimer = window.setTimeout(async () => {
    await refreshTasksSnapshot();
    scheduleHeaderTaskPolling();
  }, 1500);
}

function setHeaderTaskPopoverOpen(nextOpen) {
  const elements = headerTaskElements();
  const open = Boolean(nextOpen) && headerTaskState.visibleItems.length > 0;
  headerTaskState.popoverOpen = open;
  if (elements.chipButton) {
    elements.chipButton.setAttribute("aria-expanded", open ? "true" : "false");
  }
  if (elements.popover) {
    elements.popover.classList.toggle("hidden", !open);
  }
  scheduleHeaderTaskPolling();
}

function renderHeaderTaskPopover(items) {
  const elements = headerTaskElements();
  if (!elements.popoverList) {
    return;
  }
  const renderKey = headerTaskRenderKey(items);
  if (headerTaskState.lastRenderKey === renderKey) {
    return;
  }
  const scrollTop = elements.popoverList.scrollTop;
  elements.popoverList.innerHTML = "";
  if (!Array.isArray(items) || items.length === 0) {
    const empty = document.createElement("div");
    empty.className = "header-task-item-empty";
    empty.textContent = "No active operations right now.";
    elements.popoverList.append(empty);
    headerTaskState.lastRenderKey = renderKey;
    return;
  }
  for (const task of items) {
    const line = formatTaskLine(task);
    const row = document.createElement("div");
    row.className = `header-task-item status-${task.status}`;
    const heading = document.createElement("div");
    heading.className = "header-task-item-heading";
    const title = document.createElement("div");
    title.className = "header-task-item-title";
    title.textContent = formatTaskOperationLabel(task);
    const badge = document.createElement("span");
    badge.className = `header-task-status-badge status-${task.status}`;
    badge.textContent = statusBadgeLabel(task);
    heading.append(title, badge);
    const path = document.createElement("div");
    path.className = "header-task-item-path";
    path.textContent = line.path;
    const meta = document.createElement("div");
    meta.className = "header-task-item-meta";
    meta.textContent = line.meta;
    row.append(heading, path, meta);
    const progressText = taskProgressText(task);
    if (progressText) {
      const progress = document.createElement("div");
      progress.className = "header-task-item-progress";
      progress.textContent = progressText;
      row.append(progress);
    }
    const currentItem = compactTaskCurrentItem(task);
    if (currentItem) {
      const current = document.createElement("div");
      current.className = "header-task-item-current";
      current.textContent = currentItem;
      current.title = String(task.current_item);
      row.append(current);
    }
    const subtext = taskProgressSubtext(task);
    if (subtext) {
      const note = document.createElement("div");
      note.className = "header-task-item-subtext";
      note.textContent = subtext;
      row.append(note);
    }
    if (taskIsCancellable(task) || task.status === "cancelling") {
      const actions = document.createElement("div");
      actions.className = "header-task-item-actions";
      const cancelButton = document.createElement("button");
      cancelButton.type = "button";
      cancelButton.className = "header-task-item-action";
      cancelButton.textContent = task.status === "cancelling" ? "Stopping..." : "Stop";
      cancelButton.disabled = task.status === "cancelling";
      if (!cancelButton.disabled) {
        cancelButton.onclick = async () => {
          cancelButton.disabled = true;
          try {
            await cancelTaskRequest(task.id);
            await refreshTasksSnapshot();
          } catch (err) {
            cancelButton.disabled = false;
            setError("actions-error", `Stop task: ${err.message}`);
          }
        };
      }
      actions.append(cancelButton);
      row.append(actions);
    }
    elements.popoverList.append(row);
  }
  headerTaskState.lastRenderKey = renderKey;
  elements.popoverList.scrollTop = scrollTop;
}

function renderHeaderTaskChip(items) {
  const elements = headerTaskElements();
  if (!elements.container || !elements.chipLabel) {
    return;
  }
  const activeItems = Array.isArray(items) ? items : [];
  const recentItems = Array.isArray(headerTaskState.recentItems) ? headerTaskState.recentItems : [];
  const visibleItems = activeItems.length > 0 ? activeItems : recentItems;
  const hasVisibleItems = visibleItems.length > 0;
  elements.container.classList.toggle("hidden", !hasVisibleItems);
  elements.chipLabel.textContent = activeItems.length > 0 ? activeTaskChipLabel(activeItems) : terminalOperationChipLabel(recentItems);
  if (!hasVisibleItems) {
    headerTaskState.lastRenderKey = "";
    setHeaderTaskPopoverOpen(false);
    return;
  }
  renderHeaderTaskPopover(visibleItems);
}

function updateHeaderTaskState(taskItems) {
  const items = Array.isArray(taskItems) ? taskItems : [];
  headerTaskState.activeItems = activeTasksFromItems(items);
  headerTaskState.visibleItems = sortVisibleOperations([...headerTaskState.activeItems, ...headerTaskState.recentItems]);
  renderHeaderTaskChip(headerTaskState.activeItems);
  scheduleHeaderTaskPolling();
}

function applyTaskSnapshot(taskItems) {
  const items = Array.isArray(taskItems) ? taskItems : [];
  const now = Date.now();
  const activeById = new Set();
  const nextKnownStatuses = {};
  const nextRecentItems = [];
  let shouldRefreshPanes = false;

  for (const task of items) {
    if (!task?.id) {
      continue;
    }
    nextKnownStatuses[task.id] = task.status || "";
    const previousStatus = headerTaskState.knownStatuses[task.id] || "";
    if (isActiveTask(task)) {
      activeById.add(task.id);
    }
    if (isTerminalOperationTask(task)) {
      if (previousStatus && !TERMINAL_OPERATION_STATUSES.has(previousStatus)) {
        shouldRefreshPanes = true;
        nextRecentItems.push({ ...task, _recent_until: now + headerTaskState.recentExpiryMs });
      }
    }
  }

  for (const recentTask of headerTaskState.recentItems) {
    if (!recentTask?.id || activeById.has(recentTask.id)) {
      continue;
    }
    if ((recentTask._recent_until || 0) > now) {
      nextRecentItems.push(recentTask);
    }
  }

  headerTaskState.recentItems = sortVisibleOperations(
    nextRecentItems.filter((task, index, collection) => collection.findIndex((entry) => entry.id === task.id) === index)
  );
  headerTaskState.knownStatuses = nextKnownStatuses;
  state.lastTaskCount = items.length;
  updateHeaderTaskState(items);
  if (shouldRefreshPanes) {
    void refreshOperationPanes();
  }
  return items;
}

function renderHistoryItems(items) {
  const elements = settingsElements();
  const renderKey = JSON.stringify(Array.isArray(items) ? items : []);
  if (settingsState.lastHistoryRenderKey === renderKey) {
    return;
  }
  const scrollTop = elements.logsList.scrollTop;
  elements.logsList.innerHTML = "";
  if (!Array.isArray(items) || items.length === 0) {
    const empty = document.createElement("div");
    empty.className = "popup-meta";
    empty.textContent = "No history entries yet.";
    elements.logsList.append(empty);
    settingsState.lastHistoryRenderKey = renderKey;
    return;
  }
  for (const item of items) {
    const line = formatHistoryLine(item);
    const row = document.createElement("div");
    row.className = `settings-log-item status-${item.status}`;
    const title = document.createElement("div");
    title.className = "settings-log-title";
    title.textContent = line.title;
    const path = document.createElement("div");
    path.className = "settings-log-path";
    path.textContent = line.path;
    const meta = document.createElement("div");
    meta.className = "settings-log-meta";
    meta.textContent = line.meta;
    row.append(title, path, meta);
    if (line.error) {
      const error = document.createElement("div");
      error.className = "settings-log-error";
      error.textContent = line.error;
      row.append(error);
    }
    elements.logsList.append(row);
  }
  settingsState.lastHistoryRenderKey = renderKey;
  elements.logsList.scrollTop = scrollTop;
}

function renderTaskItems(items) {
  const elements = settingsElements();
  const renderKey = JSON.stringify(Array.isArray(items) ? items : []);
  if (settingsState.lastTasksRenderKey === renderKey) {
    return;
  }
  const scrollTop = elements.tasksList.scrollTop;
  elements.tasksList.innerHTML = "";
  if (!Array.isArray(items) || items.length === 0) {
    const empty = document.createElement("div");
    empty.className = "popup-meta";
    empty.textContent = "No tasks yet.";
    elements.tasksList.append(empty);
    settingsState.lastTasksRenderKey = renderKey;
    return;
  }
  for (const task of items) {
    const line = formatTaskLine(task);
    const row = document.createElement("div");
    row.className = `settings-log-item status-${task.status}`;
    const title = document.createElement("div");
    title.className = "settings-log-title";
    title.textContent = line.title;
    const path = document.createElement("div");
    path.className = "settings-log-path";
    path.textContent = line.path;
    const meta = document.createElement("div");
    meta.className = "settings-log-meta";
    meta.textContent = line.meta;
    row.append(title, path, meta);
    if (line.error) {
      const error = document.createElement("div");
      error.className = "settings-log-error";
      error.textContent = line.error;
      row.append(error);
    }
    elements.tasksList.append(row);
  }
  settingsState.lastTasksRenderKey = renderKey;
  elements.tasksList.scrollTop = scrollTop;
}

async function loadHistoryForSettings() {
  const data = await apiRequest("GET", "/api/history");
  renderHistoryItems(data.items || []);
  settingsState.logsLoaded = true;
}

async function loadTasksForSettings() {
  const data = await apiRequest("GET", "/api/tasks");
  renderTaskItems(applyTaskSnapshot(data.items));
  settingsState.tasksLoaded = true;
}

async function loadLogsAndTasksForSettings() {
  const elements = settingsElements();
  elements.logsError.textContent = "";
  if (!settingsState.tasksLoaded) {
    elements.tasksList.innerHTML = '<div class="popup-meta">Loading...</div>';
  }
  if (!settingsState.logsLoaded) {
    elements.logsList.innerHTML = '<div class="popup-meta">Loading...</div>';
  }
  try {
    await Promise.all([loadTasksForSettings(), loadHistoryForSettings()]);
  } catch (err) {
    elements.tasksList.innerHTML = "";
    elements.logsList.innerHTML = "";
    elements.logsError.textContent = err.message;
  }
}

function stopSettingsLogsPolling() {
  if (settingsState.logsPollTimer) {
    window.clearTimeout(settingsState.logsPollTimer);
    settingsState.logsPollTimer = null;
  }
}

function scheduleSettingsLogsPolling() {
  stopSettingsLogsPolling();
  if (settingsState.activeTab !== "logs" || settingsElements().overlay.classList.contains("hidden")) {
    return;
  }
  settingsState.logsPollTimer = window.setTimeout(async () => {
    await loadLogsAndTasksForSettings();
    scheduleSettingsLogsPolling();
  }, 1500);
}

async function handleShowThumbnailsChange(event) {
  const input = event.target;
  try {
    await saveSettings({ show_thumbnails: !!input.checked });
  } catch (err) {
    input.checked = settingsState.showThumbnails;
    settingsElements().logsError.textContent = "";
    setError("actions-error", `Settings: ${err.message}`);
  }
}

async function handlePreferredStartupPathSave() {
  const settings = settingsElements();
  const leftValue = settings.startupPathLeftInput ? settings.startupPathLeftInput.value : "";
  const rightValue = settings.startupPathRightInput ? settings.startupPathRightInput.value : "";
  settings.generalError.textContent = "";
  try {
    await saveSettings({
      preferred_startup_path_left: leftValue,
      preferred_startup_path_right: rightValue,
    });
    setStatus("Preferred startup paths saved");
  } catch (err) {
    settings.generalError.textContent = err.message;
  }
}

async function handleInterfaceSave() {
  const settings = settingsElements();
  const themeValue = settings.selectedThemeInput ? settings.selectedThemeInput.value : "default";
  settings.interfaceError.textContent = "";
  try {
    await saveSettings({ selected_theme: themeValue });
    setStatus("Theme saved");
  } catch (err) {
    settings.interfaceError.textContent = err.message;
  }
}

function closeSettings() {
  stopSettingsLogsPolling();
  settingsElements().overlay.classList.add("hidden");
}

async function openSettings(tab = "general") {
  const elements = settingsElements();
  elements.overlay.classList.remove("hidden");
  elements.generalError.textContent = "";
  setSettingsTab(tab);
  if (settingsState.activeTab === "logs") {
    await loadLogsAndTasksForSettings();
    scheduleSettingsLogsPolling();
  } else {
    stopSettingsLogsPolling();
  }
  (settingsState.activeTab === "logs"
    ? elements.logsTab
    : settingsState.activeTab === "downloads"
      ? elements.downloadsTab
    : settingsState.activeTab === "interface"
      ? elements.interfaceTab
      : elements.generalTab).focus();
}

function editorIsDirty() {
  return currentEditorContent() !== editorState.originalContent;
}

function resetEditorState() {
  editorState = {
    path: null,
    originalContent: "",
    modified: null,
  };
}

async function attemptCloseEditor() {
  if (editorIsDirty()) {
    const discard = await openConfirmModal({
      title: "Discard unsaved changes?",
      message: "Your unsaved editor changes will be lost.",
      path: editorState.path || "",
      applyText: "Discard",
    });
    if (!discard) {
      return;
    }
  }
  closeEditor();
}

function closeEditor() {
  const editor = editorElements();
  disposeMonacoEditor();
  editor.overlay.classList.add("hidden");
  editor.error.textContent = "";
  editor.host.textContent = "";
  resetEditorState();
}

async function openTextViewer() {
  const selectedItems = activePaneState().selectedItems;
  if (selectedItems.length !== 1 || selectedItems[0].kind !== "file") {
    return;
  }
  const selected = selectedItems[0];
  const viewer = viewerElements();
  viewer.overlay.classList.remove("hidden");
  viewer.title.textContent = "View";
  viewer.fileName.textContent = selected.name;
  viewer.filePath.textContent = selected.path;
  viewer.error.textContent = "";
  viewer.content.textContent = "Loading...";
  try {
    const data = await apiRequest("GET", `/api/files/view?${new URLSearchParams({ path: selected.path }).toString()}`);
    viewer.fileName.textContent = data.name;
    viewer.filePath.textContent = data.path;
    viewer.content.textContent = data.content;
    if (data.truncated) {
      viewer.error.textContent = "Preview truncated for safety";
    }
  } catch (err) {
    viewer.content.textContent = "";
    viewer.error.textContent = err.message;
  }
}

async function openPdfViewer() {
  const selectedItems = activePaneState().selectedItems;
  if (selectedItems.length !== 1 || !isPdfSelection(selectedItems[0])) {
    return;
  }
  const selected = selectedItems[0];
  const pdf = pdfElements();
  const pdfUrl = `/api/files/pdf?${new URLSearchParams({ path: selected.path }).toString()}`;
  pdf.overlay.classList.remove("hidden");
  pdf.title.textContent = "PDF";
  pdf.fileName.textContent = selected.name;
  pdf.filePath.textContent = selected.path;
  pdf.error.textContent = "";
  pdf.frame.src = pdfUrl;
}

async function openImageViewer() {
  const selectedItems = activePaneState().selectedItems;
  if (selectedItems.length !== 1 || !isImageSelection(selectedItems[0])) {
    return;
  }
  const selected = selectedItems[0];
  const image = imageElements();
  const imageUrl = `/api/files/image?${new URLSearchParams({ path: selected.path }).toString()}`;

  closeImageViewer();
  image.overlay.classList.remove("hidden");
  image.title.textContent = "Image";
  image.fileName.textContent = selected.name;
  image.filePath.textContent = selected.path;
  image.error.textContent = "";
  image.image.alt = selected.name;
  image.image.onload = () => {
    requestAnimationFrame(() => {
      if (!fitImageToViewport()) {
        requestAnimationFrame(() => {
          fitImageToViewport();
        });
      }
    });
    image.image.onload = null;
  };
  image.image.onerror = () => {
    image.error.textContent = "Image could not be displayed in this browser.";
    image.image.onerror = null;
  };
  imageViewerState.path = selected.path;
  imageViewerState.resizeHandler = () => fitImageToViewport();
  window.addEventListener("resize", imageViewerState.resizeHandler);
  image.image.src = imageUrl;
}

function videoPlaybackMessage(item) {
  const lower = (item.name || "").toLowerCase();
  if (lower.endsWith(".mkv")) {
    return "MKV playback is best-effort and depends on browser codec support.";
  }
  return "";
}

async function openVideoViewer() {
  const selectedItems = activePaneState().selectedItems;
  if (selectedItems.length !== 1 || !isVideoSelection(selectedItems[0])) {
    return;
  }
  const selected = selectedItems[0];
  const video = videoElements();
  const streamUrl = `/api/files/video?${new URLSearchParams({ path: selected.path }).toString()}`;

  video.overlay.classList.remove("hidden");
  video.title.textContent = "Video";
  video.fileName.textContent = selected.name;
  video.filePath.textContent = selected.path;
  video.error.textContent = videoPlaybackMessage(selected);
  video.player.pause();
  video.player.src = streamUrl;
  video.player.load();
}

async function openEditor() {
  const selectedItems = activePaneState().selectedItems;
  if (selectedItems.length !== 1 || !isEditableSelection(selectedItems[0])) {
    return;
  }
  const selected = selectedItems[0];
  const editor = editorElements();
  editor.overlay.classList.remove("hidden");
  editor.title.textContent = "Edit";
  editor.fileName.textContent = selected.name;
  editor.filePath.textContent = selected.path;
  editor.error.textContent = "";
  editor.host.textContent = "Loading editor...";
  editor.saveButton.disabled = true;
  try {
    const data = await apiRequest("GET", `/api/files/view?${new URLSearchParams({ path: selected.path, for_edit: "true" }).toString()}`);
    editor.fileName.textContent = data.name;
    editor.filePath.textContent = data.path;
    await ensureMonacoEditor(data.path, data.content);
    editor.saveButton.disabled = false;
    editorState.path = data.path;
    editorState.originalContent = data.content;
    editorState.modified = data.modified;
  } catch (err) {
    disposeMonacoEditor();
    editor.host.textContent = "";
    editor.error.textContent = err.message;
  }
}

function openViewer() {
  const selectedItems = activePaneState().selectedItems;
  if (selectedItems.length !== 1 || selectedItems[0].kind !== "file") {
    return;
  }
  const selected = selectedItems[0];
  if (isRemoteBrowsePath(selected.path)) {
    if (isImageSelection(selected)) {
      openImageViewer();
      return;
    }
    openTextViewer();
    return;
  }
  if (isImageSelection(selected)) {
    openImageViewer();
    return;
  }
  if (isVideoSelection(selected)) {
    openVideoViewer();
    return;
  }
  if (isPdfSelection(selected)) {
    openPdfViewer();
    return;
  }
  openTextViewer();
}

async function saveEditor() {
  if (!editorState.path) {
    return;
  }
  const editor = editorElements();
  editor.error.textContent = "";
  try {
    const response = await apiRequest("POST", "/api/files/save", {
      path: editorState.path,
      content: currentEditorContent(),
      expected_modified: editorState.modified,
    });
    editorState.originalContent = currentEditorContent();
    editorState.modified = response.modified;
    setStatus(`Saved ${response.path}`);
    closeEditor();
    await loadBrowsePane(state.activePane);
  } catch (err) {
    editor.error.textContent = err.message;
  }
}

function moveCurrentRow(delta) {
  const pane = state.activePane;
  const model = paneState(pane);
  if (!Array.isArray(model.visibleItems) || model.visibleItems.length === 0) {
    model.currentRowIndex = -1;
    return;
  }
  if (model.currentRowIndex < 0) {
    model.currentRowIndex = 0;
  } else {
    const maxIndex = model.visibleItems.length - 1;
    model.currentRowIndex = Math.max(0, Math.min(maxIndex, model.currentRowIndex + delta));
  }
  renderPaneItems(pane);
  scrollCurrentRowIntoView(pane);
}

function extendSelectionByRow(delta) {
  const pane = state.activePane;
  const model = paneState(pane);
  if (!Array.isArray(model.visibleItems) || model.visibleItems.length === 0) {
    model.currentRowIndex = -1;
    return;
  }
  const originalIndex = model.currentRowIndex < 0 ? 0 : model.currentRowIndex;
  if (model.currentRowIndex < 0) {
    model.currentRowIndex = 0;
  }
  if (!Number.isInteger(model.selectionAnchorIndex)) {
    model.selectionAnchorIndex = originalIndex;
  }
  const maxIndex = model.visibleItems.length - 1;
  model.currentRowIndex = Math.max(0, Math.min(maxIndex, model.currentRowIndex + delta));
  setRangeSelection(pane, model.selectionAnchorIndex, model.currentRowIndex);
  renderPaneItems(pane);
  scrollCurrentRowIntoView(pane);
}

function jumpCurrentRow(edge) {
  const pane = state.activePane;
  const model = paneState(pane);
  if (!Array.isArray(model.visibleItems) || model.visibleItems.length === 0) {
    model.currentRowIndex = -1;
    return;
  }
  model.currentRowIndex = edge === "start" ? 0 : model.visibleItems.length - 1;
  renderPaneItems(pane);
  scrollCurrentRowIntoView(pane);
}

function openCurrentDirectory() {
  const pane = state.activePane;
  const item = currentRowItem(pane);
  if (!item) {
    return;
  }
  if (item.kind === "directory") {
    navigateTo(pane, item.path);
    return;
  }
  if (isImageSelection(item)) {
    openImageViewer();
    return;
  }
  if (!isRemoteBrowsePath(item.path) && isVideoSelection(item)) {
    openVideoViewer();
  }
}

function toggleCurrentSelection() {
  const pane = state.activePane;
  const item = currentRowItem(pane);
  if (!item || item.isParent) {
    return;
  }
  setSelectionAnchor(pane, paneState(pane).currentRowIndex);
  toggleSelection(pane, { path: item.path, name: item.name, kind: item.kind });
  renderPaneItems(pane);
}

function clearSelectionForActivePane() {
  const pane = state.activePane;
  clearSelectionAnchor(pane);
  setSelectedItem(pane, null);
  renderPaneItems(pane);
}

function handleKeyboardShortcuts(event) {
  if (isContextMenuOpen()) {
    if (event.key === "Escape") {
      event.preventDefault();
      closeContextMenu();
    }
    return;
  }
  if (event.key === "Escape" && !uploadElements().menuPopup.classList.contains("hidden")) {
    event.preventDefault();
    closeUploadMenu();
    return;
  }
  if (isFeedbackModalOpen()) {
    if (event.key === "Escape" || event.key === "Enter") {
      event.preventDefault();
      closeFeedbackModal();
    }
    return;
  }
  if (isDownloadModalOpen()) {
    if (event.key === "Escape" && !downloadProgressState.active) {
      event.preventDefault();
      closeDownloadModal();
    }
    return;
  }
  if (isInfoOpen()) {
    if (event.key === "Escape") {
      event.preventDefault();
      closeInfo();
    }
    return;
  }
  if (isSearchOpen()) {
    if (event.key === "Escape") {
      event.preventDefault();
      closeSearch();
      return;
    }
    if (event.key === "Enter") {
      event.preventDefault();
      submitSearch();
      return;
    }
    return;
  }
  if (isRenamePopupOpen()) {
    if (event.key === "Escape") {
      event.preventDefault();
      closeRenamePopup();
      return;
    }
    if (event.key === "Enter") {
      event.preventDefault();
      submitRenamePopup();
      return;
    }
    return;
  }
  if (isSettingsOpen()) {
    if (event.key === "Escape") {
      event.preventDefault();
      closeSettings();
      return;
    }
    return;
  }
  if (isBatchMovePopupOpen()) {
    if (event.key === "Escape") {
      event.preventDefault();
      closeBatchMovePopup();
      return;
    }
    if (event.key === "Enter") {
      event.preventDefault();
      submitBatchMovePopup();
      return;
    }
    return;
  }
  if (isDeleteConfirmModalOpen()) {
    if (event.key === "Escape") {
      event.preventDefault();
      closeDeleteConfirmModal();
      return;
    }
    if (event.key === "Enter") {
      event.preventDefault();
      submitDeleteConfirmModal();
      return;
    }
    return;
  }
  if (isUploadConflictModalOpen()) {
    if (event.key === "Escape") {
      event.preventDefault();
      resolveUploadConflict("cancel");
    }
    return;
  }
  if (isMovePopupOpen()) {
    if (event.key === "Escape") {
      event.preventDefault();
      closeMovePopup();
      return;
    }
    if (event.key === "Enter") {
      event.preventDefault();
      submitMovePopup();
    }
    return;
  }
  if (isEditorOpen()) {
    if (event.key === "Escape") {
      event.preventDefault();
      attemptCloseEditor();
    }
    return;
  }
  if (isImageOpen()) {
    if (event.key === "Escape") {
      event.preventDefault();
      closeImageViewer();
    }
    return;
  }
  if (isVideoOpen()) {
    if (event.key === "Escape") {
      event.preventDefault();
      closeVideoViewer();
    }
    return;
  }
  if (isPdfOpen()) {
    if (event.key === "Escape") {
      event.preventDefault();
      closePdfViewer();
    }
    return;
  }
  if (isViewerOpen()) {
    if (event.key === "Escape") {
      event.preventDefault();
      closeViewer();
    }
    return;
  }
  if (isWildcardPopupOpen()) {
    return;
  }
  if (!shouldHandleShortcut(event.target)) {
    return;
  }
  if (event.key === "Escape" && headerTaskState.popoverOpen) {
    event.preventDefault();
    setHeaderTaskPopoverOpen(false);
    return;
  }

  const isInfoShortcut = event.key === "Enter" && !event.shiftKey && !event.altKey && (event.metaKey || event.ctrlKey);
  if (isInfoShortcut) {
    event.preventDefault();
    openInfo();
    return;
  }

  const isSearchShortcut = event.key.toLowerCase() === "f" && event.shiftKey && !event.altKey && (event.metaKey || event.ctrlKey);
  if (isSearchShortcut) {
    event.preventDefault();
    openSearch();
    return;
  }

  if (actionShortcutHandled(event)) {
    event.preventDefault();
    return;
  }

  if (event.shiftKey && event.key === "+") {
    event.preventDefault();
    openWildcardPopup("select");
    return;
  }
  if (event.shiftKey && event.key === "_") {
    event.preventDefault();
    openWildcardPopup("deselect");
    return;
  }
  if (event.metaKey && event.key === "ArrowUp") {
    event.preventDefault();
    jumpCurrentRow("start");
    return;
  }
  if (event.metaKey && event.key === "ArrowDown") {
    event.preventDefault();
    jumpCurrentRow("end");
    return;
  }
  if (event.altKey && event.key === "ArrowUp") {
    event.preventDefault();
    moveCurrentRow(-ROW_JUMP_STEP);
    return;
  }
  if (event.altKey && event.key === "ArrowDown") {
    event.preventDefault();
    moveCurrentRow(ROW_JUMP_STEP);
    return;
  }
  if (event.key === "Tab") {
    event.preventDefault();
    setActivePane(otherPane(state.activePane));
    return;
  }
  if (event.key === "Backspace") {
    event.preventDefault();
    navigateToParent(state.activePane);
    return;
  }
  if (event.key === "ArrowUp") {
    if (event.shiftKey) {
      event.preventDefault();
      extendSelectionByRow(-1);
      return;
    }
    event.preventDefault();
    moveCurrentRow(-1);
    return;
  }
  if (event.key === "ArrowDown") {
    if (event.shiftKey) {
      event.preventDefault();
      extendSelectionByRow(1);
      return;
    }
    event.preventDefault();
    moveCurrentRow(1);
    return;
  }
  if (event.key === "Enter") {
    event.preventDefault();
    openCurrentDirectory();
    return;
  }
  if (event.key === " " || event.code === "Space") {
    event.preventDefault();
    toggleCurrentSelection();
    return;
  }
  if (event.key === "Escape") {
    event.preventDefault();
    clearSelectionForActivePane();
  }
}

function setupPaneEvents(pane) {
  document.getElementById(`${pane}-pane`).onclick = () => setActivePane(pane);
  document.getElementById(`${pane}-hidden-toggle`).onchange = (ev) => {
    setActivePane(pane);
    paneState(pane).showHidden = ev.target.checked;
    loadBrowsePane(pane);
  };
}

function setupEvents() {
  setupPaneEvents("left");
  setupPaneEvents("right");
  document.addEventListener("keydown", handleKeyboardShortcuts);
  document.getElementById("theme-toggle").onclick = toggleTheme;
  const headerTasks = headerTaskElements();
  if (headerTasks.chipButton) {
    headerTasks.chipButton.onclick = (event) => {
      event.stopPropagation();
      setHeaderTaskPopoverOpen(!headerTaskState.popoverOpen);
    };
  }
  if (headerTasks.logsButton) {
    headerTasks.logsButton.onclick = () => {
      setHeaderTaskPopoverOpen(false);
      openSettings("logs");
    };
  }
  document.getElementById("upload-btn").onclick = openUploadPicker;
  document.getElementById("upload-menu-toggle").onclick = (event) => {
    event.stopPropagation();
    toggleUploadMenu();
  };
  document.getElementById("upload-folder-btn").onclick = openFolderPicker;
  document.getElementById("settings-btn").onclick = () => openSettings("general");
  document.getElementById("view-btn").onclick = openViewer;
  document.getElementById("edit-btn").onclick = openEditor;
  document.getElementById("rename-btn").onclick = openRenamePopup;
  document.getElementById("delete-btn").onclick = deleteSelected;
  document.getElementById("copy-btn").onclick = startCopySelected;
  document.getElementById("move-btn").onclick = openF6Flow;
  document.getElementById("mkdir-btn").onclick = createFolderForActivePane;
  uploadElements().input.onchange = handleUploadSelection;
  const modalCancel = uploadModalElements().cancelButton;
  if (modalCancel) {
    modalCancel.onclick = requestUploadCancel;
  }
  const feedback = feedbackElements();
  if (feedback.closeButton) {
    feedback.closeButton.onclick = closeFeedbackModal;
  }
  if (feedback.overlay) {
    feedback.overlay.onclick = (event) => {
      if (event.target === feedback.overlay) {
        closeFeedbackModal();
      }
    };
  }
  const downloadModal = downloadModalElements();
  if (downloadModal.logsButton) {
    downloadModal.logsButton.onclick = () => {
      openSettings("logs");
    };
  }
  if (downloadModal.cancelButton) {
    downloadModal.cancelButton.onclick = () => {
      requestArchiveDownloadCancel().catch((err) => {
        markZipDownloadFailed(err);
        setStatus("Download failed");
      });
    };
  }
  if (downloadModal.closeButton) {
    downloadModal.closeButton.onclick = closeDownloadModal;
  }
  if (downloadModal.overlay) {
    downloadModal.overlay.onclick = (event) => {
      if (event.target === downloadModal.overlay) {
        closeDownloadModal();
      }
    };
  }
  const contextMenu = contextMenuElements();
  if (contextMenu.renameButton) {
    contextMenu.renameButton.onclick = startContextMenuRename;
  }
  if (contextMenu.openButton) {
    contextMenu.openButton.onclick = startContextMenuOpen;
  }
  if (contextMenu.editButton) {
    contextMenu.editButton.onclick = startContextMenuEdit;
  }
  if (contextMenu.downloadButton) {
    contextMenu.downloadButton.onclick = startContextMenuDownload;
  }
  if (contextMenu.duplicateButton) {
    contextMenu.duplicateButton.onclick = startContextMenuDuplicate;
  }
  if (contextMenu.copyButton) {
    contextMenu.copyButton.onclick = startContextMenuCopy;
  }
  if (contextMenu.moveButton) {
    contextMenu.moveButton.onclick = startContextMenuMove;
  }
  if (contextMenu.deleteButton) {
    contextMenu.deleteButton.onclick = startContextMenuDelete;
  }
  if (contextMenu.propertiesButton) {
    contextMenu.propertiesButton.onclick = startContextMenuProperties;
  }
  document.addEventListener("click", (event) => {
    const elements = uploadElements();
    if (!elements.menu || elements.menu.contains(event.target)) {
    } else {
      closeUploadMenu();
    }
    const headerTaskContainer = headerTaskElements().container;
    if (headerTaskContainer && !headerTaskContainer.contains(event.target)) {
      setHeaderTaskPopoverOpen(false);
    }
    const contextMenu = contextMenuElements().menu;
    if (contextMenu && !contextMenu.contains(event.target)) {
      closeContextMenu();
    }
  });
  document.addEventListener("contextmenu", (event) => {
    const contextMenu = contextMenuElements().menu;
    if (contextMenu && contextMenu.contains(event.target)) {
      event.preventDefault();
      return;
    }
    const row = event.target instanceof Element ? event.target.closest("li[data-row-index]") : null;
    if (!row) {
      closeContextMenu();
    }
  });

  const rename = renameElements();
  rename.closeButton.onclick = closeRenamePopup;
  rename.cancelButton.onclick = closeRenamePopup;
  rename.applyButton.onclick = submitRenamePopup;
  rename.input.onkeydown = (event) => {
    if (event.key === "Enter") {
      event.preventDefault();
      submitRenamePopup();
      return;
    }
    if (event.key === "Escape") {
      event.preventDefault();
      closeRenamePopup();
    }
  };
  rename.overlay.onclick = (event) => {
    if (event.target === rename.overlay) {
      closeRenamePopup();
    }
  };

  const settings = settingsElements();
  settings.closeButton.onclick = closeSettings;
  settings.generalTab.onclick = () => {
    stopSettingsLogsPolling();
    setSettingsTab("general");
  };
  settings.interfaceTab.onclick = () => {
    stopSettingsLogsPolling();
    setSettingsTab("interface");
  };
  settings.downloadsTab.onclick = () => {
    stopSettingsLogsPolling();
    setSettingsTab("downloads");
  };
  settings.logsTab.onclick = async () => {
    await openSettings("logs");
  };
  settings.showThumbnailsInput.onchange = handleShowThumbnailsChange;
  settings.generalSaveButton.onclick = handlePreferredStartupPathSave;
  settings.interfaceSaveButton.onclick = handleInterfaceSave;
  settings.overlay.onclick = (event) => {
    if (event.target === settings.overlay) {
      closeSettings();
    }
  };

  const image = imageElements();
  image.closeButton.onclick = closeImageViewer;
  image.zoomInButton.onclick = () => adjustImageZoom(1.2);
  image.zoomOutButton.onclick = () => adjustImageZoom(1 / 1.2);
  image.resetButton.onclick = resetImageZoom;

  const search = searchElements();
  search.closeButton.onclick = closeSearch;
  search.overlay.onclick = (event) => {
    if (event.target === search.overlay) {
      closeSearch();
    }
  };

  const info = infoElements();
  info.closeButton.onclick = closeInfo;
  info.overlay.onclick = (event) => {
    if (event.target === info.overlay) {
      closeInfo();
    }
  };

  const pdf = pdfElements();
  pdf.closeButton.onclick = closePdfViewer;

  const wildcard = wildcardPopupElements();
  wildcard.cancelButton.onclick = closeWildcardPopup;
  wildcard.applyButton.onclick = submitWildcardPopup;
  wildcard.input.onkeydown = (event) => {
    if (event.key === "Enter") {
      event.preventDefault();
      submitWildcardPopup();
      return;
    }
    if (event.key === "Escape") {
      event.preventDefault();
      closeWildcardPopup();
    }
  };
  wildcard.overlay.onclick = (event) => {
    if (event.target === wildcard.overlay) {
      closeWildcardPopup();
    }
  };

  const move = moveElements();
  move.closeButton.onclick = closeMovePopup;
  move.cancelButton.onclick = closeMovePopup;
  move.applyButton.onclick = submitMovePopup;
  move.input.onkeydown = (event) => {
    if (event.key === "Enter") {
      event.preventDefault();
      submitMovePopup();
      return;
    }
    if (event.key === "Escape") {
      event.preventDefault();
      closeMovePopup();
    }
  };
  move.overlay.onclick = (event) => {
    if (event.target === move.overlay) {
      closeMovePopup();
    }
  };

  const batchMove = batchMoveElements();
  batchMove.cancelButton.onclick = closeBatchMovePopup;
  batchMove.applyButton.onclick = submitBatchMovePopup;
  batchMove.overlay.onclick = (event) => {
    if (event.target === batchMove.overlay) {
      closeBatchMovePopup();
    }
  };

  const deleteConfirm = deleteConfirmElements();
  deleteConfirm.cancelButton.onclick = closeDeleteConfirmModal;
  deleteConfirm.applyButton.onclick = submitDeleteConfirmModal;
  deleteConfirm.overlay.onclick = (event) => {
    if (event.target === deleteConfirm.overlay) {
      closeDeleteConfirmModal();
    }
  };

  const viewer = viewerElements();
  viewer.closeButton.onclick = closeViewer;
  viewer.overlay.onclick = (event) => {
    if (event.target === viewer.overlay) {
      closeViewer();
    }
  };

  const video = videoElements();
  video.closeButton.onclick = closeVideoViewer;
  video.player.onerror = () => {
    video.error.textContent = "Playback failed in this browser for this file.";
  };
  video.overlay.onclick = (event) => {
    if (event.target === video.overlay) {
      closeVideoViewer();
    }
  };

  const editor = editorElements();
  editor.closeButton.onclick = attemptCloseEditor;
  editor.cancelButton.onclick = attemptCloseEditor;
  editor.saveButton.onclick = saveEditor;
  editor.overlay.onclick = (event) => {
    if (event.target === editor.overlay) {
      attemptCloseEditor();
    }
  };
}

async function init() {
  setError("actions-error", "");
  applyTheme("default", "dark");
  setActivePane("left");
  ensureSourceSwitchers();
  setupEvents();
  await loadSettings();
  applyTheme(settingsState.selectedTheme, settingsState.selectedColorMode);
  paneState("left").currentPath = settingsState.preferredStartupPathLeft || "/Volumes";
  paneState("right").currentPath = settingsState.preferredStartupPathRight || "/Volumes";
  await loadBrowsePane("left");
  if (paneState("left").currentPath !== "/Volumes" && document.getElementById("left-browse-error").textContent) {
    setError("left-browse-error", "");
    paneState("left").currentPath = "/Volumes";
    await loadBrowsePane("left");
  }
  await loadBrowsePane("right");
  if (paneState("right").currentPath !== "/Volumes" && document.getElementById("right-browse-error").textContent) {
    setError("right-browse-error", "");
    paneState("right").currentPath = "/Volumes";
    await loadBrowsePane("right");
  }
  await refreshTasksSnapshot();
}

init();
