// MNote debug /tree shell 浏览器运行时外置模块。
// 该文件由 tree.rs 原 inline runtime 同构迁出，语义仍由 Rust tree_shell reducer 主导。

import {
  reconcilePageRuntimeResult,
  reducePageActionWithRuntime,
} from "./tree-shell-page-runtime.js";
import {
  buildFileTreeRuntimeEnvironment,
  compareTreeShellItems,
  getFileTreeRowAssetId,
  getFileTreeRowDocumentId,
  getFileTreeRowIconKind,
  getFileTreeRowMetaLabel,
  getFileTreeRowOwnerDocumentId,
  normalizeTreeShellTreeItems,
} from "./tree-shell-filetree-runtime.js";
import {
  createTreeShellFileTreeMenuRuntime,
} from "./tree-shell-filetree-menu-runtime.js";
import {
  createTreeShellFileTreeDndRuntime,
  TREE_SHELL_FILETREE_DRAG_MIME as FILETREE_DRAG_MIME,
} from "./tree-shell-filetree-dnd-runtime.js";
import {
  createTreeShellRenderer,
} from "./tree-shell-render-runtime.js";
import {
  focusTreeShellPickerRowElement,
  hydrateTreeShellInitialPageTree,
  hydrateTreeShellInitialPickerTree,
  patchTreeShellPageActiveDom,
  patchTreeShellPageExpansionDom,
  patchTreeShellPickerActiveDom,
} from "./tree-shell-dom-runtime.js";
import {
  computeTreeShellPickerStateActionResult,
  getTreeShellVisiblePickerEntries,
  isTreeShellPickerEntryPickable,
  normalizeTreeShellPickerItemKey,
  resolveTreeShellCurrentPickerItemKey,
} from "./tree-shell-picker-runtime.js";
import {
  normalizeTreeShellNumber as normalizeNumber,
  normalizeTreeShellStringArray as normalizeStringArray,
  normalizeTreeShellText as normalizeText,
  parseTreeShellState,
  parseTreeShellStateFromHtml,
  resolveTreeShellMode,
  resolveTreeShellTargetOrigin,
} from "./tree-shell-state-runtime.js";

function startTreeShellRuntime() {
  const stateElement = document.getElementById("tree-shell-state");
  const appElement = document.getElementById("tree-shell-app");
  const statusElement = document.getElementById("tree-shell-status");
  const lastActionElement = document.getElementById("tree-shell-last-action");
  const createRootButton = document.getElementById("tree-create-root");
  if (!stateElement || !appElement || !statusElement || !lastActionElement || !createRootButton) {
    return;
  }

  const state = parseTreeShellState(stateElement);
  const rendererInput =
    state.rendererInput && typeof state.rendererInput === "object"
      ? state.rendererInput
      : {};
  const rendererFiletreeSelection =
    rendererInput.filetreeSelection && typeof rendererInput.filetreeSelection === "object"
      ? rendererInput.filetreeSelection
      : {};
  const filetreeSelectionReducer =
    rendererInput.filetreeSelectionReducer &&
    typeof rendererInput.filetreeSelectionReducer === "object"
      ? rendererInput.filetreeSelectionReducer
      : {};
  const pickerStateReducer =
    rendererInput.pickerStateReducer &&
    typeof rendererInput.pickerStateReducer === "object"
      ? rendererInput.pickerStateReducer
      : {};
  const pageFocusKeyboardReducer =
    rendererInput.pageFocusKeyboardReducer &&
    typeof rendererInput.pageFocusKeyboardReducer === "object"
      ? rendererInput.pageFocusKeyboardReducer
      : {};
  const runtimeArtifact =
    rendererInput.runtimeArtifact && typeof rendererInput.runtimeArtifact === "object"
      ? rendererInput.runtimeArtifact
      : {};
  const runtimeApi =
    runtimeArtifact.runtimeApi && typeof runtimeArtifact.runtimeApi === "object"
      ? runtimeArtifact.runtimeApi
      : {};
  const hostOverride =
    window.__MNOTE_TREE_SHELL_OVERRIDE__ &&
    typeof window.__MNOTE_TREE_SHELL_OVERRIDE__ === "object"
      ? window.__MNOTE_TREE_SHELL_OVERRIDE__
      : {};
  const channel =
    typeof state.channel === "string" && state.channel.trim()
      ? state.channel.trim()
      : "mnote-tree-shell-v1";
  const workspaceId =
    typeof state.workspaceId === "string" && state.workspaceId.trim()
      ? state.workspaceId.trim()
      : "";
  const sourceKind =
    typeof state.sourceKind === "string" && state.sourceKind.trim()
      ? state.sourceKind.trim()
      : "local_folder";
  const rootUri =
    typeof state.rootUri === "string" && state.rootUri.trim()
      ? state.rootUri.trim()
      : "";
  const initialLocalWatchRevision =
    state.localWatchRevision &&
    typeof state.localWatchRevision === "object" &&
    typeof state.localWatchRevision.revision === "string"
      ? state.localWatchRevision.revision
      : "";
  const actorId =
    typeof state.actorId === "string" && state.actorId.trim()
      ? state.actorId.trim()
      : "";
  const activeDocumentId =
    typeof state.activeDocumentId === "string" && state.activeDocumentId.trim()
      ? state.activeDocumentId.trim()
      : "";
  const focusedDocumentId =
    typeof state.focusedDocumentId === "string" && state.focusedDocumentId.trim()
      ? state.focusedDocumentId.trim()
      : "";
  const activePickerItemKey =
    typeof rendererInput.activePickerItem === "string" && rendererInput.activePickerItem.trim()
      ? rendererInput.activePickerItem.trim()
      : typeof state.activePickerItemKey === "string" && state.activePickerItemKey.trim()
        ? state.activePickerItemKey.trim()
        : "";
  const mode = resolveTreeShellMode(state);
  const allowRootPick = state.allowRootPick === true;
  const excludedIds = new Set(
    normalizeStringArray(rendererInput.excludedPickerIds).length > 0
      ? normalizeStringArray(rendererInput.excludedPickerIds)
      : normalizeStringArray(state.excludeIds),
  );
  const pickerStateReducerContractName =
    typeof pickerStateReducer.contractName === "string" &&
    pickerStateReducer.contractName.trim()
      ? pickerStateReducer.contractName.trim()
      : "";
  const pickerStateReducerActions = new Set(
    normalizeStringArray(pickerStateReducer.actions),
  );
  const pageFocusKeyboardReducerContractName =
    typeof pageFocusKeyboardReducer.contractName === "string" &&
    pageFocusKeyboardReducer.contractName.trim()
      ? pageFocusKeyboardReducer.contractName.trim()
      : "";
  const pageFocusKeyboardReducerActions = new Set(
    normalizeStringArray(pageFocusKeyboardReducer.actions),
  );
  const commandPath =
    typeof state.commandPath === "string" && state.commandPath.trim()
      ? state.commandPath.trim()
      : "/api/tree/commands";
  let mediaAssets = Array.isArray(state.mediaAssets) ? state.mediaAssets : [];
  let mindmapAssets = Array.isArray(state.mindmapAssets) ? state.mindmapAssets : [];
  let tableAssets = Array.isArray(state.tableAssets) ? state.tableAssets : [];
  const mindmapAssetChildren =
    state.mindmapAssetChildren && typeof state.mindmapAssetChildren === "object"
      ? state.mindmapAssetChildren
      : {};
  const titleElement = document.getElementById("tree-shell-title");
  const summaryElement = document.getElementById("tree-shell-summary");
  const toolbarElement = document.getElementById("tree-shell-toolbar");
  const targetOrigin = resolveTreeShellTargetOrigin(document);
  const initialRenameRowId = (() => {
    try {
      return normalizeText(new URL(window.location.href).searchParams.get("renameRowId"));
    } catch {
      return "";
    }
  })();
  const runtimeReduceEndpoint = normalizeText(
    runtimeApi.reduceEndpoint,
    "/api/tree/runtime/reduce",
  );

  let rawItems = Array.isArray(hostOverride.items) ? hostOverride.items : state.items;
  let normalizedItems = normalizeTreeShellTreeItems(rawItems, { excludedIds });
  let itemById = new Map();
  let fileTreeRowById = new Map();
  let childrenByParentId = new Map();
  let roots = [];
  let assetsByDocId = new Map();

  const rebuildTreeIndexes = () => {
    itemById = new Map(normalizedItems.map((item) => [item.nodeId, item]));
    fileTreeRowById = new Map(normalizedItems.map((item) => [item.rowId, item]));
    childrenByParentId = new Map();
    roots = [];
    assetsByDocId = new Map();
    [...mediaAssets, ...mindmapAssets, ...tableAssets].forEach((asset) => {
      const documentId = normalizeText(asset?.document_id);
      const assetId = normalizeText(asset?.id);
      if (!documentId || !assetId) return;
      const bucket = assetsByDocId.get(documentId) || [];
      bucket.push({
        id: assetId,
        documentId,
        assetType: normalizeText(asset?.asset_type, "file"),
        fileName: normalizeText(asset?.file_name, "附件"),
        storagePath: normalizeText(asset?.storage_path),
      });
      assetsByDocId.set(documentId, bucket);
    });
    normalizedItems.forEach((item) => {
      const parentId = item.parentNodeId && itemById.has(item.parentNodeId) ? item.parentNodeId : null;
      if (!parentId) {
        roots.push(item);
        return;
      }
      const bucket = childrenByParentId.get(parentId) || [];
      bucket.push(item);
      childrenByParentId.set(parentId, bucket);
    });
    roots.sort(compareTreeShellItems);
    childrenByParentId.forEach((bucket) => bucket.sort(compareTreeShellItems));
  };
  rebuildTreeIndexes();

  const rendererExpandedIds = normalizeStringArray(rendererInput.expandedIds);
  const expanded = new Set(
    rendererExpandedIds.length > 0
      ? rendererExpandedIds
      : normalizedItems
          .filter((item) => item.childCount > 0 && item.expandedByDefault)
          .map((item) => item.nodeId),
  );
  let currentActiveDocumentId = activeDocumentId;
  let currentFocusedDocumentId = focusedDocumentId;
  let currentActivePickerItemKey = activePickerItemKey;
  const resolvePickerRootFocused = () =>
    mode === "picker" && currentActivePickerItemKey === "__root__";
  const resolveFocusedNodeIdFromHostState = () => {
    const pickerRootFocused = resolvePickerRootFocused();
    return mode === "picker"
      ? currentActivePickerItemKey &&
        currentActivePickerItemKey !== "__root__" &&
        itemById.has(currentActivePickerItemKey)
        ? currentActivePickerItemKey
        : currentActiveDocumentId && itemById.has(currentActiveDocumentId)
          ? currentActiveDocumentId
          : pickerRootFocused
            ? ""
            : roots[0]?.nodeId || ""
      : currentFocusedDocumentId && itemById.has(currentFocusedDocumentId)
        ? currentFocusedDocumentId
        : currentActiveDocumentId && itemById.has(currentActiveDocumentId)
          ? currentActiveDocumentId
          : roots[0]?.nodeId || "";
  };
  let focusedNodeId = resolveFocusedNodeIdFromHostState();
  const rendererSelectedFileTreeRowIds = normalizeStringArray(
    rendererFiletreeSelection.selectedRowIds,
  );
  const rendererAnchorRowId =
    typeof rendererFiletreeSelection.anchorRowId === "string" &&
    rendererFiletreeSelection.anchorRowId.trim()
      ? rendererFiletreeSelection.anchorRowId.trim()
      : null;
  const rendererFocusedRowId =
    typeof rendererFiletreeSelection.focusedRowId === "string" &&
    rendererFiletreeSelection.focusedRowId.trim()
      ? rendererFiletreeSelection.focusedRowId.trim()
      : null;
  const filetreeSelectionReducerContractName =
    typeof filetreeSelectionReducer.contractName === "string" &&
    filetreeSelectionReducer.contractName.trim()
      ? filetreeSelectionReducer.contractName.trim()
      : "";
  const filetreeSelectionReducerActions = new Set(
    normalizeStringArray(filetreeSelectionReducer.actions),
  );
  let selectedFileTreeRowIds = new Set(
    rendererSelectedFileTreeRowIds.length > 0
      ? rendererSelectedFileTreeRowIds
      : currentActiveDocumentId ? [`doc:${currentActiveDocumentId}`] : []
  );
  let fileTreeAnchorRowId = rendererAnchorRowId || (currentActiveDocumentId ? `doc:${currentActiveDocumentId}` : null);
  let fileTreeFocusedRowId = rendererFocusedRowId || (currentActiveDocumentId ? `doc:${currentActiveDocumentId}` : null);
  let visibleFileTreeRowIds = [];
  let draggingPageNodeId = "";
  let draggingFileTreeRowIds = [];
  let activePageDropNodeId = null;
  let fileTreeClipboard = { action: null, rowIds: [] };
  let inlineRenameState = { mode: null, id: null, committing: false };
  let renderNode = () => {
    throw new Error("tree shell renderer is not ready");
  };
  let renderTree = () => {
    throw new Error("tree shell renderer is not ready");
  };
  let syncFileTreeSelectionDom = () => {};
  let openHydratedFileTreeItem = () => {};
  let hydrateInitialFileTree = () => false;

  let activeCursor = itemById.get(currentActiveDocumentId) || null;
  while (activeCursor && activeCursor.parentNodeId && itemById.has(activeCursor.parentNodeId)) {
    expanded.add(activeCursor.parentNodeId);
    activeCursor = itemById.get(activeCursor.parentNodeId) || null;
  }
  assetsByDocId.forEach((_, documentId) => {
    if (itemById.has(documentId)) {
      expanded.add(documentId);
    }
  });

  if (titleElement) {
    titleElement.textContent =
      mode === "picker"
        ? "页面选择"
        : mode === "filetree"
          ? "资源管理器"
          : "页面树";
  }
  if (summaryElement) {
    summaryElement.textContent =
      mode === "picker"
        ? "这个页面复用统一 projection 协议，以轻量选择器模式承载 move / embed picker。当前阶段只负责树浏览与目标选择，不承接命令写链。"
        : mode === "filetree"
          ? "这个页面以文件树模式消费 Rust 侧 projection，并承载页面、index 与附件浏览。当前阶段仍是过渡验证壳，但 filetree 协议与渲染分支必须保持闭环。"
          : "这个页面直接消费 Rust 侧 projection，并通过统一 command route 回写树操作。当前阶段先交付最小可交互壳，用于主 Sidebar 页面树切流与真实网页验证。";
  }
  if (toolbarElement && mode === "picker") {
    toolbarElement.style.display = "none";
  }

  let busy = false;

  const setBusy = (nextBusy) => {
    busy = nextBusy;
    createRootButton.disabled = nextBusy;
    appElement.querySelectorAll("button").forEach((button) => {
      button.disabled = nextBusy;
    });
  };

  const setStatus = (message, tone = "normal") => {
    statusElement.textContent = message;
    statusElement.dataset.tone = tone;
  };

  const setLastAction = (message, tone = "normal") => {
    lastActionElement.textContent = message;
    lastActionElement.dataset.tone = tone;
  };

  const focusRowElement = (nodeId) => {
    if (!nodeId) return;
    window.requestAnimationFrame(() => {
      const row = appElement.querySelector(`.tree-row[data-node-id="${nodeId}"]`);
      if (!(row instanceof HTMLElement)) return;
      row.focus({ preventScroll: true });
      row.scrollIntoView({ block: "nearest" });
    });
  };

  const postToHost = (type, extra = {}) => {
    if (window.parent === window) return;
    const payload = Object.assign({ channel, type }, extra);
    window.parent.postMessage(payload, targetOrigin);
  };

  const canExpandFileTreeRow = (item) => {
    if (!item) return false;
    return item.childCount > 0 || item.capabilities.includes("expand");
  };

  const fileTreeDndRuntime = createTreeShellFileTreeDndRuntime({
    appElement,
    expanded,
    sourceKind,
    workspaceId,
    canExpandFileTreeRow,
    getFileTreeAnchorRowId: () => fileTreeAnchorRowId,
    getFileTreeFocusedRowId: () => fileTreeFocusedRowId,
    getFileTreeRowById: () => fileTreeRowById,
    getItemById: () => itemById,
    getSelectedFileTreeRowIds: () => selectedFileTreeRowIds,
    getVisibleFileTreeRowIds: () => visibleFileTreeRowIds,
    renderTree: () => renderTree(),
    scheduleRefresh: (...args) => scheduleRefresh(...args),
    sendCommand: (...args) => sendCommand(...args),
    setLastAction,
    setStatus,
  });
  const clearFileTreeDropFeedback = fileTreeDndRuntime.clearFileTreeDropFeedback;
  const executeLocalFileTreeExternalDrop = fileTreeDndRuntime.executeLocalFileTreeExternalDrop;
  const executeLocalFileTreeInternalDrop = fileTreeDndRuntime.executeLocalFileTreeInternalDrop;
  const filterRedundantFileTreeRowIds = fileTreeDndRuntime.filterRedundantFileTreeRowIds;
  const getActiveFileTreeDropPosition = fileTreeDndRuntime.getActiveFileTreeDropPosition;
  const getActiveFileTreeDropRowId = fileTreeDndRuntime.getActiveFileTreeDropRowId;
  const getActiveFileTreeRootDrop = fileTreeDndRuntime.getActiveFileTreeRootDrop;
  const getFileTreeDropTargetFromEvent = fileTreeDndRuntime.getFileTreeDropTargetFromEvent;
  const inferDefaultFileTreeDropDocumentId = fileTreeDndRuntime.inferDefaultFileTreeDropDocumentId;
  const runFileTreePreflight = fileTreeDndRuntime.runFileTreePreflight;
  const setFileTreeDropFeedback = fileTreeDndRuntime.setFileTreeDropFeedback;
  const updateFileTreeDropFeedback = fileTreeDndRuntime.updateFileTreeDropFeedback;
  const validateFileTreeInternalDrop = fileTreeDndRuntime.validateFileTreeInternalDrop;

  const isExternalFileDrag = (event) => {
    const types = event.dataTransfer?.types;
    return Array.isArray(types)
      ? types.includes("Files")
      : types instanceof DOMStringList
        ? types.contains("Files")
        : false;
  };

  const isInternalFileTreeDrag = (event) => {
    const types = event.dataTransfer?.types;
    return Array.isArray(types)
      ? types.includes(FILETREE_DRAG_MIME)
      : types instanceof DOMStringList
        ? types.contains(FILETREE_DRAG_MIME)
        : false;
  };

  const emitFileTreeSelectionChange = () => {
    if (mode !== "filetree") return;
    postToHost("tree.filetree.selection.changed", {
      selectedRowIds: Array.from(selectedFileTreeRowIds),
      anchorRowId: fileTreeAnchorRowId,
      focusedRowId: fileTreeFocusedRowId,
      payload: {
        selectedRowIds: Array.from(selectedFileTreeRowIds),
        anchorRowId: fileTreeAnchorRowId,
        focusedRowId: fileTreeFocusedRowId,
      },
    });
  };

  const getFileTreeRangeRowIds = (fromId, toId) => {
    const fromIndex = visibleFileTreeRowIds.indexOf(fromId);
    const toIndex = visibleFileTreeRowIds.indexOf(toId);
    if (fromIndex < 0 || toIndex < 0) {
      return [toId];
    }
    const lo = Math.min(fromIndex, toIndex);
    const hi = Math.max(fromIndex, toIndex);
    return visibleFileTreeRowIds.slice(lo, hi + 1);
  };

  const normalizeFileTreeSelectionState = (selection) => {
    const selectedRowIds =
      selection?.selectedRowIds instanceof Set
        ? new Set(
            Array.from(selection.selectedRowIds)
              .map((rowId) => normalizeText(rowId))
              .filter(Boolean),
          )
        : new Set(normalizeStringArray(selection?.selectedRowIds));
    const anchorRowId = normalizeText(selection?.anchorRowId) || null;
    const focusedRowId = normalizeText(selection?.focusedRowId) || null;
    return {
      selectedRowIds,
      anchorRowId,
      focusedRowId,
    };
  };

  const readFileTreeSelectionState = () =>
    normalizeFileTreeSelectionState({
      selectedRowIds: Array.from(selectedFileTreeRowIds),
      anchorRowId: fileTreeAnchorRowId,
      focusedRowId: fileTreeFocusedRowId,
    });

  const commitFileTreeSelection = (nextSelection) => {
    const normalizedSelection = normalizeFileTreeSelectionState(nextSelection);
    selectedFileTreeRowIds = normalizedSelection.selectedRowIds;
    fileTreeAnchorRowId = normalizedSelection.anchorRowId;
    fileTreeFocusedRowId = normalizedSelection.focusedRowId;
    emitFileTreeSelectionChange();
    return normalizedSelection;
  };

  const computeFileTreeSelectionActionResult = (action) => {
    const currentSelection = readFileTreeSelectionState();
    if (
      mode !== "filetree" ||
      filetreeSelectionReducerContractName !== "rust_filetree_selection_reducer_v1" ||
      !filetreeSelectionReducerActions.has(action?.kind || "")
    ) {
      return {
        nextSelection: currentSelection,
        dragRowIds: action?.kind === "resolve_drag_rows"
          ? [normalizeText(action?.rowId)].filter(Boolean)
          : null,
      };
    }

    if (action.kind === "select_row") {
      const rowId = normalizeText(action.rowId);
      if (!rowId) {
        return { nextSelection: currentSelection, dragRowIds: null };
      }
      const shiftKey = action.modifiers?.shiftKey === true;
      const metaKey = action.modifiers?.metaKey === true;
      const ctrlKey = action.modifiers?.ctrlKey === true;
      const toggleSelection = metaKey || ctrlKey;

      if (shiftKey) {
        const anchor =
          currentSelection.anchorRowId || currentSelection.focusedRowId || rowId;
        const nextSelection = toggleSelection
          ? new Set(currentSelection.selectedRowIds)
          : new Set();
        getFileTreeRangeRowIds(anchor, rowId).forEach((id) => {
          nextSelection.add(id);
        });
        return {
          nextSelection: {
            selectedRowIds: nextSelection,
            anchorRowId: currentSelection.anchorRowId || anchor,
            focusedRowId: rowId,
          },
          dragRowIds: null,
        };
      }

      if (toggleSelection) {
        const nextSelection = new Set(currentSelection.selectedRowIds);
        if (nextSelection.has(rowId)) {
          nextSelection.delete(rowId);
        } else {
          nextSelection.add(rowId);
        }
        return {
          nextSelection: {
            selectedRowIds: nextSelection,
            anchorRowId: rowId,
            focusedRowId: rowId,
          },
          dragRowIds: null,
        };
      }

      return {
        nextSelection: {
          selectedRowIds: new Set([rowId]),
          anchorRowId: rowId,
          focusedRowId: rowId,
        },
        dragRowIds: null,
      };
    }

    if (action.kind === "select_context_row") {
      const rowId = normalizeText(action.rowId);
      if (!rowId) {
        return { nextSelection: currentSelection, dragRowIds: null };
      }
      if (currentSelection.selectedRowIds.has(rowId)) {
        return {
          nextSelection: {
            selectedRowIds: new Set(currentSelection.selectedRowIds),
            anchorRowId: currentSelection.anchorRowId,
            focusedRowId: rowId,
          },
          dragRowIds: null,
        };
      }
      return {
        nextSelection: {
          selectedRowIds: new Set([rowId]),
          anchorRowId: rowId,
          focusedRowId: rowId,
        },
        dragRowIds: null,
      };
    }

    if (action.kind === "normalize_visible_rows") {
      const visibleSet = new Set(normalizeStringArray(action.visibleRowIds));
      const nextSelectedRowIds = Array.from(currentSelection.selectedRowIds).filter((rowId) =>
        visibleSet.has(rowId),
      );
      return {
        nextSelection: {
          selectedRowIds: new Set(nextSelectedRowIds),
          anchorRowId:
            currentSelection.anchorRowId && visibleSet.has(currentSelection.anchorRowId)
              ? currentSelection.anchorRowId
              : null,
          focusedRowId:
            currentSelection.focusedRowId && visibleSet.has(currentSelection.focusedRowId)
              ? currentSelection.focusedRowId
              : null,
        },
        dragRowIds: null,
      };
    }

    if (action.kind === "clear") {
      return {
        nextSelection: {
          selectedRowIds: new Set(),
          anchorRowId: null,
          focusedRowId: null,
        },
        dragRowIds: null,
      };
    }

    if (action.kind === "resolve_drag_rows") {
      const rowId = normalizeText(action.rowId);
      if (!rowId) {
        return { nextSelection: currentSelection, dragRowIds: [] };
      }
      return {
        nextSelection: currentSelection,
        dragRowIds: currentSelection.selectedRowIds.has(rowId)
          ? Array.from(currentSelection.selectedRowIds)
          : [rowId],
      };
    }

    return { nextSelection: currentSelection, dragRowIds: null };
  };

  const applyFileTreeSelectionAction = (action) => {
    const result = computeFileTreeSelectionActionResult(action);
    if (action?.kind !== "resolve_drag_rows") {
      commitFileTreeSelection(result.nextSelection);
    }
    return result;
  };

  const selectFileTreeRow = (rowId, modifiers = {}) => {
    applyFileTreeSelectionAction({
      kind: "select_row",
      rowId,
      modifiers: {
        shiftKey: modifiers.shiftKey === true,
        ctrlKey: modifiers.ctrlKey === true,
        metaKey: modifiers.metaKey === true,
      },
    });
  };

  const selectFileTreeContextRow = (rowId) => {
    applyFileTreeSelectionAction({
      kind: "select_context_row",
      rowId,
    });
  };

  const clearFileTreeSelection = () => {
    if (mode !== "filetree") return;
    if (
      selectedFileTreeRowIds.size === 0 &&
      !fileTreeAnchorRowId &&
      !fileTreeFocusedRowId
    ) {
      return;
    }
    applyFileTreeSelectionAction({ kind: "clear" });
    renderTree();
  };

  const normalizeFileTreeSelectionForVisibleRows = () => {
    if (mode !== "filetree") return;
    const currentSelection = readFileTreeSelectionState();
    const nextSelection = computeFileTreeSelectionActionResult({
      kind: "normalize_visible_rows",
      visibleRowIds: visibleFileTreeRowIds,
    }).nextSelection;

    if (
      nextSelection.selectedRowIds.size === currentSelection.selectedRowIds.size &&
      Array.from(nextSelection.selectedRowIds).every((rowId) =>
        currentSelection.selectedRowIds.has(rowId),
      ) &&
      nextSelection.anchorRowId === currentSelection.anchorRowId &&
      nextSelection.focusedRowId === currentSelection.focusedRowId
    ) {
      return;
    }

    commitFileTreeSelection(nextSelection);
  };

  const resolveFileTreeDraggedRowIds = (rowId) =>
    computeFileTreeSelectionActionResult({
      kind: "resolve_drag_rows",
      rowId,
    }).dragRowIds || [];

  const patchFileTreeCutDecoration = () => {
    appElement.querySelectorAll('[data-rust-rendered-row="filetree"], .tree-row[data-shell-mode="filetree"]').forEach((row) => {
      if (!(row instanceof HTMLElement)) return;
      row.dataset.cut = String(
        fileTreeClipboard.action === "cut" &&
          fileTreeClipboard.rowIds.includes(normalizeText(row.dataset.rowId)),
      );
    });
  };

  const focusFileTreeRowByOffset = (offset) => {
    if (visibleFileTreeRowIds.length === 0) return;
    const currentIndex = Math.max(0, visibleFileTreeRowIds.indexOf(fileTreeFocusedRowId));
    const nextIndex = Math.max(0, Math.min(visibleFileTreeRowIds.length - 1, currentIndex + offset));
    const rowId = visibleFileTreeRowIds[nextIndex];
    if (!rowId) return;
    commitFileTreeSelection({ selectedRowIds: [rowId], anchorRowId: rowId, focusedRowId: rowId });
    const row = appElement.querySelector(`.tree-row[data-shell-mode="filetree"][data-row-id="${CSS.escape(rowId)}"]`);
    if (row instanceof HTMLElement) row.focus({ preventScroll: true });
  };

  const setFileTreeClipboard = (action) => {
    const rowIds = selectedFileTreeRowIds.size > 0
      ? Array.from(selectedFileTreeRowIds)
      : fileTreeFocusedRowId
        ? [fileTreeFocusedRowId]
        : [];
    fileTreeClipboard = { action, rowIds };
    patchFileTreeCutDecoration();
    setLastAction(action === "cut" ? "已剪切选中文件树节点" : "已复制选中文件树节点");
  };

  const resolveFileTreeActionRowIds = (rowId) => {
    const normalizedRowId = normalizeText(rowId);
    if (normalizedRowId && selectedFileTreeRowIds.has(normalizedRowId)) {
      return Array.from(selectedFileTreeRowIds);
    }
    if (normalizedRowId) return [normalizedRowId];
    if (selectedFileTreeRowIds.size > 0) return Array.from(selectedFileTreeRowIds);
    return fileTreeFocusedRowId ? [fileTreeFocusedRowId] : [];
  };

  const runFileTreeDelete = async (rowId) => {
    const rowIds = resolveFileTreeActionRowIds(rowId);
    const deletableItems = rowIds
      .map((id) => fileTreeRowById.get(id))
      .filter((item) => Boolean(getFileTreeRowDocumentId(item)) || (sourceKind === "local_folder" && item?.rowKind === "asset"));
    if (deletableItems.length === 0) {
      setLastAction("当前选择没有可删除的页面或 Markdown 文件", "error");
      return false;
    }
    const accepted = await runFileTreePreflight("delete", {
      rowIds: deletableItems.map((item) => item.rowId),
    });
    if (!accepted) return false;
    if (sourceKind === "local_folder") {
      for (const item of deletableItems) {
        await sendCommand({
          action: "delete",
          workspaceId,
          documentId: getFileTreeRowDocumentId(item) || item.rowId,
        });
      }
      scheduleRefresh();
      return true;
    }
    if (sourceKind === "convex_workspace") {
      for (const item of deletableItems) {
        const documentId = getFileTreeRowDocumentId(item);
        await sendCommand({
          action: "delete",
          workspaceId,
          documentId,
        });
        applyRemovedDocumentLocally(documentId);
      }
      if (!deletableItems.every((item) => !fileTreeRowById.has(item.rowId))) {
        scheduleRefresh();
      }
      return true;
    }
    postToHost("tree.filetree.delete", {
      workspaceId,
      rowIds: deletableItems.map((item) => item.rowId),
      payload: {
        workspaceId,
        rowIds: deletableItems.map((item) => item.rowId),
        documentIds: deletableItems.map(getFileTreeRowDocumentId),
      },
    });
    return true;
  };

  const pasteFileTreeClipboardInto = async (targetRowId) => {
    if (!fileTreeClipboard.action || fileTreeClipboard.rowIds.length === 0) return;
    const targetItem = targetRowId ? fileTreeRowById.get(targetRowId) : null;
    const target = targetItem
      ? {
      rowId: targetItem.rowId,
      rowKind: targetItem.rowKind,
      nodeId: targetItem.nodeId,
      documentId: getFileTreeRowDocumentId(targetItem) || null,
      ownerDocumentId: getFileTreeRowOwnerDocumentId(targetItem) || null,
      assetId: getFileTreeRowAssetId(targetItem) || null,
    }
      : { rowId: null, rowKind: "root", nodeId: null, documentId: null, ownerDocumentId: null, assetId: null };
    if (sourceKind === "local_folder") {
      const pasted = await executeLocalFileTreeInternalDrop(
        target,
        fileTreeClipboard.rowIds,
        fileTreeClipboard.action === "copy",
        "paste",
      );
      if (pasted && fileTreeClipboard.action === "cut") {
        fileTreeClipboard = { action: null, rowIds: [] };
      }
      patchFileTreeCutDecoration();
    } else {
      const accepted = await runFileTreePreflight("paste", {
        target,
        rowIds: fileTreeClipboard.rowIds,
        copy: fileTreeClipboard.action === "copy",
      });
      if (!accepted) return;
      if (sourceKind === "convex_workspace") {
        const sourceRowIds = filterRedundantFileTreeRowIds(fileTreeClipboard.rowIds);
        for (const rowId of sourceRowIds) {
          const sourceItem = fileTreeRowById.get(rowId);
          const documentId = getFileTreeRowDocumentId(sourceItem);
          if (!documentId) continue;
          await sendCommand({
            action: fileTreeClipboard.action === "copy" ? "copy" : "move",
            workspaceId,
            documentId,
            parentId: target.documentId || null,
            targetParentId: target.documentId || null,
            sortOrder: 0,
          });
        }
        if (fileTreeClipboard.action === "cut") {
          fileTreeClipboard = { action: null, rowIds: [] };
        }
        patchFileTreeCutDecoration();
        scheduleRefresh();
      } else {
        postToHost("tree.filetree.paste", {
          workspaceId,
          rowIds: fileTreeClipboard.rowIds,
          action: fileTreeClipboard.action,
          targetRowId: target.rowId,
          targetRowKind: target.rowKind,
          documentId: target.documentId,
          payload: {
            workspaceId,
            rowIds: fileTreeClipboard.rowIds,
            action: fileTreeClipboard.action,
            targetRowId: target.rowId,
            targetRowKind: target.rowKind,
            documentId: target.documentId,
          },
        });
      }
    }
  };

  const handleFileTreeKeyDown = (event, item) => {
    if (mode !== "filetree") return;
    if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "c") {
      event.preventDefault();
      setFileTreeClipboard("copy");
      return;
    }
    if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "x") {
      event.preventDefault();
      setFileTreeClipboard("cut");
      return;
    }
    if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "v") {
      event.preventDefault();
      void pasteFileTreeClipboardInto(item?.rowId || fileTreeFocusedRowId);
      return;
    }
    if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "a") {
      event.preventDefault();
      commitFileTreeSelection({
        selectedRowIds: visibleFileTreeRowIds,
        anchorRowId: visibleFileTreeRowIds[0] || null,
        focusedRowId: visibleFileTreeRowIds[visibleFileTreeRowIds.length - 1] || null,
      });
      renderTree();
      return;
    }
    if (event.key === "F2") {
      event.preventDefault();
      const rowId = item?.rowId || fileTreeFocusedRowId;
      const renameItem = rowId ? fileTreeRowById.get(rowId) : null;
      if (rowId && getFileTreeRowDocumentId(renameItem)) {
        beginInlineRename("filetree", rowId);
      } else {
        setLastAction("当前资源暂不支持重命名", "error");
      }
      return;
    }
    if (event.key === "ArrowDown") {
      event.preventDefault();
      focusFileTreeRowByOffset(1);
      return;
    }
    if (event.key === "ArrowUp") {
      event.preventDefault();
      focusFileTreeRowByOffset(-1);
      return;
    }
    if (event.key === "Home") {
      event.preventDefault();
      focusFileTreeRowByOffset(-visibleFileTreeRowIds.length);
      return;
    }
    if (event.key === "End") {
      event.preventDefault();
      focusFileTreeRowByOffset(visibleFileTreeRowIds.length);
      return;
    }
    if (event.key === "Enter") {
      event.preventDefault();
      if (item) {
        const documentId = getFileTreeRowDocumentId(item);
        if (documentId) {
          handleNavigate(documentId);
        } else {
          openHydratedFileTreeItem(item);
        }
      }
      return;
    }
    if (event.key === "Delete" || event.key === "Backspace") {
      event.preventDefault();
      void runFileTreeDelete(item?.rowId || fileTreeFocusedRowId);
    }
  };

  const applyTreeShellStateSnapshot = (nextState, options = {}) => {
    if (!nextState || typeof nextState !== "object") return false;
    mediaAssets = Array.isArray(nextState.mediaAssets) ? nextState.mediaAssets : [];
    mindmapAssets = Array.isArray(nextState.mindmapAssets) ? nextState.mindmapAssets : [];
    tableAssets = Array.isArray(nextState.tableAssets) ? nextState.tableAssets : [];
    rawItems = Array.isArray(nextState.items) ? nextState.items : [];
    normalizedItems = normalizeTreeShellTreeItems(rawItems, { excludedIds });
    rebuildTreeIndexes();
    if (currentActiveDocumentId && !itemById.has(currentActiveDocumentId)) {
      currentActiveDocumentId = roots[0]?.nodeId || "";
    }
    if (currentFocusedDocumentId && !itemById.has(currentFocusedDocumentId)) {
      currentFocusedDocumentId = currentActiveDocumentId;
    }
    normalizedItems
      .filter((item) => item.childCount > 0 && item.expandedByDefault)
      .forEach((item) => expanded.add(item.nodeId));
    focusedNodeId = resolveFocusedNodeIdFromHostState();
    renderTree();
    if (mode === "filetree") {
      emitFileTreeSelectionChange();
    }
    const renameRowId = normalizeText(options.renameRowId);
    if (renameRowId) {
      window.setTimeout(() => {
        if (fileTreeRowById.has(renameRowId)) {
          beginInlineRename("filetree", renameRowId);
        }
      }, 80);
    }
    return true;
  };

  const refreshLocalFolderSnapshot = async (options = {}) => {
    const response = await fetch(window.location.href, {
      headers: { "accept": "text/html" },
    });
    if (!response.ok) return false;
    const nextState = parseTreeShellStateFromHtml(await response.text());
    document.documentElement.setAttribute(
      "data-mnote-local-folder-watch-applied",
      options.appliedValue || "projection",
    );
    document.documentElement.removeAttribute("data-mnote-local-folder-watch-disabled");
    return applyTreeShellStateSnapshot(nextState, options);
  };

  const scheduleRefresh = (options = {}) => {
    window.setTimeout(() => {
      void refreshLocalFolderSnapshot(options);
    }, 80);
  };

  let localFolderEventRefreshPending = false;
  const scheduleLocalFolderEventRefresh = (options = {}) => {
    if (localFolderEventRefreshPending) return;
    localFolderEventRefreshPending = true;
    window.setTimeout(() => {
      localFolderEventRefreshPending = false;
      void refreshLocalFolderSnapshot(options);
    }, 120);
  };

  const addTreeItemLocally = (item) => {
    if (!item?.nodeId || itemById.has(item.nodeId)) return false;
    normalizedItems.push(item);
    itemById.set(item.nodeId, item);
    fileTreeRowById.set(item.rowId, item);
    const parentId = item.parentNodeId && itemById.has(item.parentNodeId) ? item.parentNodeId : null;
    if (parentId) {
      const bucket = childrenByParentId.get(parentId) || [];
      bucket.push(item);
      bucket.sort(compareTreeShellItems);
      childrenByParentId.set(parentId, bucket);
      const parentItem = itemById.get(parentId);
      if (parentItem) parentItem.childCount = Math.max(parentItem.childCount || 0, bucket.length);
      expanded.add(parentId);
    } else {
      roots.push(item);
      roots.sort(compareTreeShellItems);
    }
    return true;
  };

  const applyCreatedDocumentLocally = (result, parentId, title) => {
    const documentId =
      typeof result?.documentId === "string" && result.documentId.trim()
        ? result.documentId.trim()
        : typeof result?.id === "string" && result.id.trim()
          ? result.id.trim()
          : "";
    if (!documentId) return false;
    const createdAt = normalizeText(result?.updatedAt || result?.execution?.updated_at);
    const parentNodeId = parentId && itemById.has(parentId) ? parentId : null;
    const item = {
      rowId: `doc:${documentId}`,
      rowKind: "document",
      nodeId: documentId,
      parentNodeId,
      title: normalizeText(result?.title, title || "无标题"),
      depth: parentNodeId ? normalizeNumber(itemById.get(parentNodeId)?.depth, 0) + 1 : 0,
      childCount: 0,
      position: normalizeNumber(result?.sortOrder ?? result?.execution?.sort_order ?? Date.now()),
      expandedByDefault: true,
      iconHint: "page",
      capabilities: ["open", "rename", "delete", "move", "create"],
      resourceMeta: {
        resourceKind: "document",
        documentId,
        assetId: "",
        assetKind: "",
        objectIdentity: { objectKind: "page", documentId, blockId: null, assetId: null },
        blockAssetRelation: null,
      },
      updatedAt: createdAt,
    };
    if (!addTreeItemLocally(item)) return false;
    currentFocusedDocumentId = documentId;
    focusedNodeId = documentId;
    currentActiveDocumentId = documentId;
    renderTree();
    return true;
  };

  const removeTreeItemEverywhere = (item) => {
    if (!item) return;
    const itemIndex = normalizedItems.indexOf(item);
    if (itemIndex >= 0) normalizedItems.splice(itemIndex, 1);
    itemById.delete(item.nodeId);
    fileTreeRowById.delete(item.rowId);
    const rootIndex = roots.indexOf(item);
    if (rootIndex >= 0) roots.splice(rootIndex, 1);
    const bucket = item.parentNodeId ? childrenByParentId.get(item.parentNodeId) : null;
    if (bucket) {
      const bucketIndex = bucket.indexOf(item);
      if (bucketIndex >= 0) bucket.splice(bucketIndex, 1);
      if (bucket.length === 0) childrenByParentId.delete(item.parentNodeId);
    }
  };

  const applyRemovedDocumentLocally = (documentId) => {
    const normalizedDocumentId = normalizeText(documentId);
    if (!normalizedDocumentId) return false;
    const removedNodeIds = new Set([normalizedDocumentId]);
    let changed = false;
    let expandedDuringScan = true;
    while (expandedDuringScan) {
      expandedDuringScan = false;
      normalizedItems.forEach((item) => {
        if (item.parentNodeId && removedNodeIds.has(item.parentNodeId) && !removedNodeIds.has(item.nodeId)) {
          removedNodeIds.add(item.nodeId);
          expandedDuringScan = true;
        }
      });
    }
    normalizedItems.slice().forEach((item) => {
      const itemDocumentId = getFileTreeRowOwnerDocumentId(item) || item.nodeId;
      if (removedNodeIds.has(item.nodeId) || itemDocumentId === normalizedDocumentId) {
        removeTreeItemEverywhere(item);
        changed = true;
      }
    });
    if (!changed) return false;
    if (currentActiveDocumentId === normalizedDocumentId) currentActiveDocumentId = roots[0]?.nodeId || "";
    if (currentFocusedDocumentId === normalizedDocumentId) currentFocusedDocumentId = currentActiveDocumentId;
    focusedNodeId = resolveFocusedNodeIdFromHostState();
    renderTree();
    return true;
  };

  if (sourceKind === "local_folder" && rootUri) {
    let localFolderWatchRevision = initialLocalWatchRevision;
    const applyLocalFolderRevision = (revision) => {
      const nextRevision = normalizeText(revision);
      if (!nextRevision || nextRevision === localFolderWatchRevision) return false;
      localFolderWatchRevision = nextRevision;
      document.documentElement.setAttribute("data-mnote-tree-live-revision", nextRevision);
      return true;
    };
    window.addEventListener("tree:snapshot", function(event) {
      const detail = event && event.detail ? event.detail : {};
      const payload = detail.payload || detail;
      if (!payload || payload.sourceKind !== "local_folder") return;
      if (!applyLocalFolderRevision(payload.revision || detail.revision)) return;
      scheduleLocalFolderEventRefresh({ appliedValue: "snapshot" });
    });
    window.addEventListener("tree:resync", function(event) {
      const detail = event && event.detail ? event.detail : {};
      const payload = detail.payload || detail;
      if (!payload || payload.sourceKind !== "local_folder") return;
      if (!applyLocalFolderRevision(payload.revision || detail.revision)) return;
      scheduleLocalFolderEventRefresh({ appliedValue: "resync" });
    });
    window.addEventListener("tree:local-folder-watch-batch", function(event) {
      const detail = event && event.detail ? event.detail : {};
      const payload = detail.payload || detail;
      if (!payload || payload.sourceKind !== "local_folder") return;
      if (!applyLocalFolderRevision(payload.revision || detail.revision)) return;
      scheduleLocalFolderEventRefresh({ appliedValue: "watch_batch" });
    });
    window.addEventListener("tree:error", function(event) {
      const detail = event && event.detail ? event.detail : {};
      const payload = detail.payload || detail;
      if (!payload || payload.sourceKind !== "local_folder") return;
      document.documentElement.setAttribute(
        "data-mnote-tree-live-error",
        normalizeText(payload.code || payload.error || payload.message, "tree_live_error"),
      );
    });
    const transport = document.documentElement.getAttribute("data-mnote-tree-live-transport") || "";
    if (transport !== "local-folder-events") {
      document.documentElement.setAttribute("data-mnote-local-folder-watch-applied", "static");
      document.documentElement.setAttribute("data-mnote-local-folder-watch-disabled", "events-required");
    }
  }

  const readErrorMessage = async (response) => {
    const text = await response.text();
    try {
      const payload = text ? JSON.parse(text) : null;
      const fromMessage =
        payload && typeof payload.message === "string" ? payload.message :
        payload && typeof payload.error === "string" ? payload.error :
        payload && payload.result && typeof payload.result.message === "string" ? payload.result.message :
        "";
      if (fromMessage) return fromMessage;
    } catch {
      // 忽略 JSON 解析失败，继续返回文本片段
    }
    return text ? text.slice(0, 180) : "树命令执行失败";
  };

  const sendCommand = async (payload) => {
    setBusy(true);
    setStatus("正在提交树命令…");
    try {
      const commandPayload = Object.assign(
        {},
        payload,
        sourceKind ? { sourceKind } : {},
        rootUri ? { rootUri } : {},
      );
      const response = await fetch(commandPath, {
        method: "POST",
        headers: {
          "content-type": "application/json",
          "x-mnote-workspace-id": workspaceId,
          "x-mnote-source-channel": "mnote_web_tree_shell",
          "x-mnote-source-client": "mnote-web",
          ...(actorId
            ? {
                "x-mnote-actor-id": actorId,
                "x-mnote-actor-type": "user",
              }
            : {}),
        },
        body: JSON.stringify(commandPayload),
      });
      if (!response.ok) {
        throw new Error(await readErrorMessage(response));
      }
      const data = await response.json().catch(() => null);
      if (!data || typeof data !== "object") {
        throw new Error("tree command 返回了无效响应");
      }
      if (data.ok === true && data.result) {
        return data.result;
      }
      if (data.result && typeof data.result === "object") {
        return data.result;
      }
      return data;
    } finally {
      setBusy(false);
    }
  };

  const getSiblings = (parentId) => {
    if (!parentId) return roots.slice();
    return (childrenByParentId.get(parentId) || []).slice();
  };

  const PAGE_DRAG_MIME = "application/x-mnote-page-tree-node";

  const clearPageDropFeedback = () => {
    if (!activePageDropNodeId) {
      return;
    }
    const previousRow = appElement.querySelector(
      `.tree-row[data-shell-mode="page"][data-node-id="${activePageDropNodeId}"]`,
    );
    if (previousRow instanceof HTMLElement) {
      previousRow.dataset.dropFeedback = "false";
    }
    activePageDropNodeId = null;
  };

  const setPageDropFeedback = (nodeId) => {
    const nextNodeId = normalizeText(nodeId);
    if (activePageDropNodeId && activePageDropNodeId !== nextNodeId) {
      const previousRow = appElement.querySelector(
        `.tree-row[data-shell-mode="page"][data-node-id="${activePageDropNodeId}"]`,
      );
      if (previousRow instanceof HTMLElement) {
        previousRow.dataset.dropFeedback = "false";
      }
    }

    if (!nextNodeId) {
      activePageDropNodeId = null;
      return;
    }

    const nextRow = appElement.querySelector(
      `.tree-row[data-shell-mode="page"][data-node-id="${nextNodeId}"]`,
    );
    if (nextRow instanceof HTMLElement) {
      nextRow.dataset.dropFeedback = "true";
    }
    activePageDropNodeId = nextNodeId;
  };

  const resolvePageDropTargetNodeId = (element) => {
    const row = element instanceof Element
      ? element.closest('.tree-row[data-shell-mode="page"]')
      : null;
    if (!(row instanceof HTMLElement)) {
      return "";
    }
    return normalizeText(row.dataset.nodeId);
  };

  const readPageDragNodeId = (event) => {
    const raw =
      event.dataTransfer?.getData(PAGE_DRAG_MIME) ||
      event.dataTransfer?.getData("text/plain") ||
      draggingPageNodeId ||
      "";
    return normalizeText(raw);
  };

  const canAcceptPageDrop = (sourceNodeId, targetNodeId) => {
    if (!sourceNodeId || !targetNodeId || sourceNodeId === targetNodeId) {
      return false;
    }
    const sourceItem = itemById.get(sourceNodeId);
    const targetItem = itemById.get(targetNodeId);
    if (!sourceItem || !targetItem) {
      return false;
    }
    return sourceItem.parentNodeId === targetItem.parentNodeId;
  };

  const postPageExpandChange = (nodeId, nextExpanded) => {
    if (mode !== "page" || !nodeId) return;
    postToHost("tree.page.expand.changed", {
      documentId: nodeId,
      expanded: nextExpanded === true,
      target: { documentId: nodeId },
      payload: { documentId: nodeId, expanded: nextExpanded === true },
    });
  };

  const postPageFocusChange = (nodeId) => {
    if (mode !== "page" || !nodeId) return;
    postToHost("tree.page.focus.changed", {
      documentId: nodeId,
      target: { documentId: nodeId },
      payload: { documentId: nodeId },
    });
  };

  const commitPageExpandedIds = (expandedIds) => {
    const nextExpanded = new Set(normalizeStringArray(expandedIds));
    let changed = nextExpanded.size !== expanded.size;
    if (!changed) {
      changed = Array.from(nextExpanded).some((nodeId) => !expanded.has(nodeId));
    }
    if (!changed) return false;
    expanded.clear();
    nextExpanded.forEach((nodeId) => expanded.add(nodeId));
    return true;
  };

  const patchPageTreeAfterRuntimeState = (changedNodeIds, focusedId) => {
    if (mode !== "page") return;
    if (usedRustInitialRenderer) {
      const ids = normalizeStringArray(changedNodeIds);
      ids.forEach((nodeId) => {
        patchPageTreeExpansionDom(nodeId);
      });
      patchPageTreeActiveDom();
      if (focusedId) focusRowElement(focusedId);
      return;
    }
    renderTree();
    if (focusedId) focusRowElement(focusedId);
  };

  const applyLocalPageExpansionFallback = (nodeId, nextExpanded) => {
    if (nextExpanded) expanded.add(nodeId);
    else expanded.delete(nodeId);
    postPageExpandChange(nodeId, nextExpanded);
    if (mode === "page" && usedRustInitialRenderer && patchPageTreeExpansionDom(nodeId)) {
      focusRowElement(nodeId);
      return;
    }
    renderTree();
  };

  const toggleExpand = (nodeId) => {
    applyLocalPageExpansionFallback(nodeId, !expanded.has(nodeId));
  };

  const getVisiblePageItems = () => {
    const visible = [];
    const walk = (entries) => {
      entries.forEach((item) => {
        visible.push(item);
        if (item.childCount > 0 && expanded.has(item.nodeId)) {
          walk(getSiblings(item.nodeId));
        }
      });
    };
    walk(roots);
    return visible;
  };

  const getVisiblePickerEntries = () =>
    getTreeShellVisiblePickerEntries({
      mode,
      allowRootPick,
      roots,
      expanded,
      getSiblings,
    });

  const isPickerEntryPickable = (entry) =>
    isTreeShellPickerEntryPickable(entry, {
      allowRootPick,
      excludedIds,
    });

  const getPickablePickerEntries = () =>
    getVisiblePickerEntries().filter((entry) => isPickerEntryPickable(entry));

  const normalizePickerItemKey = (pickerItemKey) =>
    normalizeTreeShellPickerItemKey(pickerItemKey, {
      allowRootPick,
      itemById,
      excludedIds,
    });

  const resolveCurrentPickerItemKey = () =>
    resolveTreeShellCurrentPickerItemKey({
      currentActivePickerItemKey,
      currentActiveDocumentId,
      normalizePickerItemKey,
      getPickablePickerEntries,
    });

  const computePickerStateActionResult = (action) =>
    computeTreeShellPickerStateActionResult({
      mode,
      pickerStateReducerContractName,
      pickerStateReducerActions,
      resolveCurrentPickerItemKey,
      getPickablePickerEntries,
      normalizePickerItemKey,
    }, action);

  const focusNode = (nodeId) => {
    if (!nodeId || !itemById.has(nodeId)) return;
    if (focusedNodeId === nodeId) {
      focusRowElement(nodeId);
      return;
    }
    focusedNodeId = nodeId;
    postPageFocusChange(nodeId);
    if (usedRustInitialRenderer) {
      patchPageTreeActiveDom();
      focusRowElement(nodeId);
      return;
    }
    renderTree();
    focusRowElement(nodeId);
  };

  const resolvePageActionItem = (action, item) => {
    const actionNodeId = normalizeText(action?.nodeId);
    if (actionNodeId && itemById.has(actionNodeId)) {
      return itemById.get(actionNodeId);
    }
    if (item?.nodeId && itemById.has(item.nodeId)) {
      return item;
    }
    return focusedNodeId && itemById.has(focusedNodeId)
      ? itemById.get(focusedNodeId)
      : null;
  };

  const buildPageRuntimeAction = (action, item) => {
    const actionKind = normalizeText(action?.kind).toLowerCase();
    if (actionKind === "focus") {
      const nodeId = normalizeText(action?.nodeId || item?.nodeId);
      return nodeId ? { kind: "focus", nodeId } : null;
    }
    if (actionKind === "move_next") return { kind: "moveNext" };
    if (actionKind === "move_previous") return { kind: "movePrevious" };
    if (actionKind === "move_home") return { kind: "moveHome" };
    if (actionKind === "move_end") return { kind: "moveEnd" };
    if (actionKind === "open") return { kind: "openFocused" };
    if (actionKind === "context_menu") return { kind: "contextMenuFocused" };
    if (actionKind === "expand" || actionKind === "collapse" || actionKind === "toggle") {
      const nodeId = normalizeText(action?.nodeId || item?.nodeId);
      return nodeId ? { kind: actionKind, nodeId } : null;
    }
    return null;
  };

  const buildPageRuntimeEnvironment = () => ({
    visibleNodeIds: getVisiblePageItems().map((entry) => entry.nodeId),
    expandableNodeIds: normalizedItems
      .filter((entry) => entry.childCount > 0 && getSiblings(entry.nodeId).length > 0)
      .map((entry) => entry.nodeId),
  });

  const readPageRuntimeState = (action, item) => {
    const actionKind = normalizeText(action?.kind).toLowerCase();
    const nodeId = normalizeText(action?.nodeId || item?.nodeId);
    const focusedId =
      (actionKind === "open" || actionKind === "context_menu") && itemById.has(nodeId)
        ? nodeId
        : focusedNodeId || null;
    return {
      focusedId,
      expandedIds: Array.from(expanded),
      dropFeedback: null,
    };
  };

  const normalizePageRuntimeResult = (runtimeResult) => {
    if (!runtimeResult || runtimeResult.mode !== "page") {
      return null;
    }
    const stateSnapshot =
      runtimeResult.state && runtimeResult.state.mode === "page"
        ? runtimeResult.state.state
        : null;
    const pagePatch = Array.isArray(runtimeResult.domPatches)
      ? runtimeResult.domPatches.find((patch) => patch?.kind === "pageState")
      : null;
    const focusedId =
      typeof pagePatch?.focusedId === "string"
        ? pagePatch.focusedId
        : typeof stateSnapshot?.focusedId === "string"
          ? stateSnapshot.focusedId
          : "";
    const expandedIds = Array.isArray(pagePatch?.expandedIds)
      ? pagePatch.expandedIds
      : Array.isArray(stateSnapshot?.expandedIds)
        ? stateSnapshot.expandedIds
        : null;
    return {
      focusedId: normalizeText(focusedId),
      expandedIds: expandedIds ? normalizeStringArray(expandedIds) : null,
      hostEvents: Array.isArray(runtimeResult.hostEvents) ? runtimeResult.hostEvents : [],
    };
  };

  const replayPageRuntimeHostEvents = (runtimeResult, item, sourceElement) => {
    const result = normalizePageRuntimeResult(runtimeResult);
    if (!result) return false;
    let replayed = false;
    result.hostEvents.forEach((event) => {
      if (!event || typeof event !== "object") return;
      if (event.kind === "pageOpen") {
        const nodeId = normalizeText(event.nodeId);
        if (nodeId) {
          handleNavigate(nodeId);
          replayed = true;
        }
        return;
      }
      if (event.kind === "pageContextMenu") {
        const nodeId = normalizeText(event.nodeId || item?.nodeId);
        if (!nodeId) return;
        const rect = sourceElement?.getBoundingClientRect?.();
        openContextMenu(
          nodeId,
          rect ? rect.left + Math.min(rect.width - 12, 28) : 0,
          rect ? rect.top + Math.min(rect.height - 12, 18) : 0,
        );
        replayed = true;
      }
    });
    return replayed;
  };

  const pageRuntimeBridge = {
    mode,
    runtimeReduceEndpoint,
    buildPageRuntimeAction,
    buildPageRuntimeEnvironment,
    readPageRuntimeState,
    readErrorMessage,
    normalizePageRuntimeResult,
    commitPageExpandedIds,
    normalizeText,
    postPageFocusChange,
    postPageExpandChange,
    patchPageTreeAfterRuntimeState,
    get expanded() {
      return expanded;
    },
    getFocusedNodeId: () => focusedNodeId,
    setFocusedNodeId: (nextFocusedNodeId) => {
      focusedNodeId = nextFocusedNodeId;
    },
  };

  const applyLocalPageActionFallback = (action, item, sourceElement) => {
    if (mode !== "page") return;
    const actionKind = normalizeText(action?.kind).toLowerCase();
    if (!pageFocusKeyboardReducerActions.has(actionKind)) {
      return;
    }

    if (actionKind === "focus") {
      const nextFocusId = normalizeText(action?.nodeId);
      if (nextFocusId && itemById.has(nextFocusId)) {
        focusNode(nextFocusId);
      }
      return;
    }

    const visible = getVisiblePageItems();
    const visibleIds = visible.map((entry) => entry.nodeId);
    if (visibleIds.length === 0) {
      return;
    }
    const currentFocusId =
      visibleIds.includes(focusedNodeId) ? focusedNodeId : visibleIds[0];
    const currentIndex = visibleIds.indexOf(currentFocusId);

    if (actionKind === "move_next") {
      const next = visible[currentIndex + 1];
      if (next) focusNode(next.nodeId);
      return;
    }
    if (actionKind === "move_previous") {
      const previous = visible[currentIndex - 1];
      if (previous) focusNode(previous.nodeId);
      return;
    }
    if (actionKind === "move_home") {
      focusNode(visibleIds[0]);
      return;
    }
    if (actionKind === "move_end") {
      focusNode(visibleIds[visibleIds.length - 1]);
      return;
    }
    if (actionKind === "expand") {
      if (item?.childCount > 0 && !expanded.has(item.nodeId)) {
        applyLocalPageExpansionFallback(item.nodeId, true);
        focusRowElement(item.nodeId);
        return;
      }
      const firstChild = item ? getSiblings(item.nodeId)[0] : null;
      if (firstChild) {
        focusNode(firstChild.nodeId);
      }
      return;
    }
    if (actionKind === "collapse") {
      if (item?.childCount > 0 && expanded.has(item.nodeId)) {
        applyLocalPageExpansionFallback(item.nodeId, false);
        focusRowElement(item.nodeId);
        return;
      }
      if (item?.parentNodeId && itemById.has(item.parentNodeId)) {
        focusNode(item.parentNodeId);
      }
      return;
    }
    if (actionKind === "open") {
      if (item?.nodeId) {
        handleNavigate(item.nodeId);
      }
      return;
    }
    if (actionKind === "context_menu") {
      if (!item?.nodeId) {
        return;
      }
      const rect = sourceElement?.getBoundingClientRect?.();
      if (!rect) {
        return;
      }
      openContextMenu(
        item.nodeId,
        rect.left + Math.min(rect.width - 12, 28),
        rect.top + Math.min(rect.height - 12, 18),
      );
    }
  };

  const applyPageKeyboardAction = (action, item, sourceElement) => {
    if (mode !== "page") return;
    const actionKind = normalizeText(action?.kind).toLowerCase();
    if (!pageFocusKeyboardReducerActions.has(actionKind)) {
      return;
    }
    const runtimeItem = resolvePageActionItem(action, item);
    const runtimeAction = buildPageRuntimeAction(action, runtimeItem);
    if (!runtimeAction) {
      applyLocalPageActionFallback(action, runtimeItem || item, sourceElement);
      return;
    }
    void reducePageActionWithRuntime(pageRuntimeBridge, action, runtimeItem)
      .then((runtimeResult) => {
        const replayedHostEvent = replayPageRuntimeHostEvents(
          runtimeResult,
          runtimeItem,
          sourceElement,
        );
        const reconciledState = reconcilePageRuntimeResult(
          pageRuntimeBridge,
          runtimeResult,
          runtimeItem,
        );
        if (!replayedHostEvent && !reconciledState) {
          applyLocalPageActionFallback(action, runtimeItem || item, sourceElement);
        }
      })
      .catch(() => {
        applyLocalPageActionFallback(action, runtimeItem || item, sourceElement);
      });
  };

  const postPickerFocusChange = (pickerItemKey) => {
    if (mode !== "picker") return;
    const normalizedItemKey = normalizeText(pickerItemKey);
    const documentId =
      normalizedItemKey && normalizedItemKey !== "__root__"
        ? normalizedItemKey
        : null;
    postToHost("tree.picker.focus.changed", {
      documentId,
      itemKey: normalizedItemKey || null,
      pickerItemKey: normalizedItemKey || null,
      target: { documentId },
      payload: {
        documentId,
        itemKey: normalizedItemKey || null,
      },
    });
  };

  const applyPickerFocusByItemKey = (pickerItemKey, options = {}) => {
    if (mode !== "picker") return;
    const result = computePickerStateActionResult({
      kind: "focus",
      itemKey: pickerItemKey,
    });
    const shouldFocusDom = options.focusDom === true;
    const nextPickerItemKey = result.nextItemKey || "";
    const nextDocumentId =
      nextPickerItemKey && nextPickerItemKey !== "__root__"
        ? nextPickerItemKey
        : null;

    currentActivePickerItemKey = nextPickerItemKey;
    currentActiveDocumentId = nextDocumentId;
    focusedNodeId = nextDocumentId || "";
    if (usedRustInitialRenderer) {
      patchPickerActiveDom();
      if (shouldFocusDom) focusPickerRowElement(nextPickerItemKey);
    } else {
      renderTree();
    }
    if (shouldFocusDom && nextDocumentId) {
      focusRowElement(nextDocumentId);
    }
    postPickerFocusChange(nextPickerItemKey || null);
  };

  const applyPickerStateAction = (action) => {
    if (mode !== "picker") {
      return {
        nextItemKey: "",
        pickedDocumentId: null,
        pickedRoot: false,
      };
    }

    const result = computePickerStateActionResult(action);
    if (action?.kind !== "pick") {
      applyPickerFocusByItemKey(result.nextItemKey);
    }
    return result;
  };

  const postPickerPickResultToHost = (result) => {
    if (mode !== "picker" || !result) return;
    if (result.pickedRoot) {
      setLastAction("已选择根目录");
      postToHost("tree.pick.root", {
        documentId: null,
        target: { documentId: null },
        payload: { documentId: null },
      });
      return;
    }
    if (result.pickedDocumentId) {
      postToHost("tree.pick", {
        documentId: result.pickedDocumentId,
        itemKey: result.nextItemKey || result.pickedDocumentId,
        target: { documentId: result.pickedDocumentId },
        payload: { documentId: result.pickedDocumentId },
      });
    }
  };

  const handlePickerCommand = (command) => {
    if (mode !== "picker") return;

    const normalizedCommand = normalizeText(command);
    if (!pickerStateReducerActions.has(normalizedCommand)) {
      return;
    }

    if (normalizedCommand === "pick") {
      postPickerPickResultToHost(applyPickerStateAction({ kind: "pick" }));
      return;
    }

    applyPickerStateAction({ kind: normalizedCommand });
  };

  const localCreatedRowIdFromCommandResult = (result, rowKind) => {
    const relativePath =
      typeof result?.execution?.relativePath === "string" && result.execution.relativePath.trim()
        ? result.execution.relativePath.trim()
        : typeof result?.relativePath === "string" && result.relativePath.trim()
          ? result.relativePath.trim()
          : "";
    if (!relativePath) return "";
    return `local:${rowKind}:${relativePath}`;
  };

  const fileTreeMenuRuntime = createTreeShellFileTreeMenuRuntime({
    sourceKind,
    workspaceId,
    getFileTreeRowById: () => fileTreeRowById,
    getSelectedFileTreeRowIds: () => selectedFileTreeRowIds,
    getFileTreeClipboard: () => fileTreeClipboard,
    setFileTreeClipboard,
    commitFileTreeSelection,
    syncFileTreeSelectionDom,
    pasteFileTreeClipboardInto,
    runFileTreeDelete,
    handleCreate: (...args) => handleCreate(...args),
    sendCommand,
    setStatus,
    setLastAction,
    scheduleRefresh,
    refreshLocalFolderSnapshot,
    expanded,
    renderTree: () => renderTree(),
    postToHost,
    openHydratedFileTreeItem: (...args) => openHydratedFileTreeItem(...args),
    beginInlineRename: (...args) => beginInlineRename(...args),
    localCreatedRowIdFromCommandResult,
  });
  const openFileTreeContextMenu = fileTreeMenuRuntime.openFileTreeContextMenu;

  const openContextMenu = (nodeId, clientX, clientY) => {
    if (!nodeId || mode !== "page") return;
    setLastAction(`已打开页面 ${nodeId} 的上下文菜单`);
    postToHost("tree.page.context-menu", {
      documentId: nodeId,
      target: { documentId: nodeId },
      payload: { documentId: nodeId, x: clientX, y: clientY },
      x: clientX,
      y: clientY,
    });
  };

  const getElementCenter = (element) => {
    const rect = element.getBoundingClientRect();
    return {
      x: rect.left + rect.width / 2,
      y: rect.top + rect.height / 2,
    };
  };

  const readFileTreeInternalDropPayload = (event) => {
    const raw =
      event.dataTransfer?.getData("application/x-mnote-file-tree") ||
      event.dataTransfer?.getData("text/plain") ||
      "";
    if (!raw) return null;
    try {
      const payload = JSON.parse(raw);
      if (
        payload?.type !== "mnote-file-tree-dnd" ||
        payload.version !== 1 ||
        !Array.isArray(payload.rowIds)
      ) {
        return null;
      }
      const rowIds = payload.rowIds
        .map((value) => normalizeText(value))
        .filter(Boolean);
      return rowIds.length > 0 ? rowIds : null;
    } catch {
      return null;
    }
  };

  const handleRowKeyDown = (event, item) => {
    if (mode !== "page") return;
    if (event.key === "ArrowDown") {
      event.preventDefault();
      applyPageKeyboardAction({ kind: "move_next" }, item, event.currentTarget);
      return;
    }
    if (event.key === "ArrowUp") {
      event.preventDefault();
      applyPageKeyboardAction({ kind: "move_previous" }, item, event.currentTarget);
      return;
    }
    if (event.key === "ArrowRight") {
      event.preventDefault();
      applyPageKeyboardAction({ kind: "expand" }, item, event.currentTarget);
      return;
    }
    if (event.key === "ArrowLeft") {
      event.preventDefault();
      applyPageKeyboardAction({ kind: "collapse" }, item, event.currentTarget);
      return;
    }
    if (event.key === "Enter") {
      event.preventDefault();
      applyPageKeyboardAction({ kind: "open" }, item, event.currentTarget);
      return;
    }
    if (event.key === "F2") {
      event.preventDefault();
      beginInlineRename("page", item.nodeId);
      return;
    }
    if (
      event.key === "ContextMenu" ||
      (event.shiftKey && event.key === "F10")
    ) {
      event.preventDefault();
      applyPageKeyboardAction({ kind: "context_menu" }, item, event.currentTarget);
    }
  };

  const handleNavigate = (nodeId) => {
    if (!nodeId) return;
    if (mode === "picker") {
      setLastAction(`已选择页面 ${nodeId}`);
      postToHost("tree.pick", {
        documentId: nodeId,
        target: { documentId: nodeId },
        payload: { documentId: nodeId },
      });
      return;
    }
    setLastAction(`准备打开页面 ${nodeId}`);
    postToHost("tree.navigate", {
      documentId: nodeId,
      target: { documentId: nodeId },
      payload: { documentId: nodeId },
    });
  };

  const handleCreate = async (parentId) => {
    const title = "无标题";
    try {
      const result = await sendCommand({
        action: "create",
        workspaceId,
        parentId,
        title,
        accessScope: "private",
        content: [],
      });
      const documentId =
        typeof result.documentId === "string" && result.documentId.trim()
          ? result.documentId.trim()
          : "";
      setStatus("创建页面成功");
      setLastAction(parentId ? "已创建无标题子页面" : "已创建无标题页面");
      postToHost("tree.node.created", {
        documentId,
        target: { documentId },
        payload: { documentId },
      });
      if (sourceKind === "convex_workspace" && applyCreatedDocumentLocally(result, parentId, title)) {
        if (mode === "filetree") {
          beginInlineRename("filetree", `doc:${documentId}`);
        } else {
          beginInlineRename("page", documentId);
        }
        return;
      }
      if (documentId) {
        postToHost("tree.navigate", {
          documentId,
          target: { documentId },
          payload: { documentId },
        });
      }
      scheduleRefresh({
        renameRowId: localCreatedRowIdFromCommandResult(result, "markdown"),
      });
    } catch (error) {
      const message = error instanceof Error ? error.message : "创建页面失败";
      setStatus(message, "error");
      setLastAction("创建页面失败", "error");
      window.alert(message);
    }
  };

  const focusInlineRenameInput = (id) => {
    window.requestAnimationFrame(() => {
      const input = appElement.querySelector(
        `.tree-rename-input[data-rename-id="${CSS.escape(id)}"]`,
      );
      if (!(input instanceof HTMLInputElement)) return;
      input.focus();
      const dotIndex = input.value.lastIndexOf(".");
      const end = dotIndex > 0 ? dotIndex : input.value.length;
      input.setSelectionRange(0, end);
    });
  };

  const beginInlineRename = (renameMode, id) => {
    inlineRenameState = { mode: renameMode, id, committing: false };
    renderTree();
    focusInlineRenameInput(id);
  };

  const cancelInlineRename = () => {
    inlineRenameState = { mode: null, id: null, committing: false };
    renderTree();
  };

  const commitInlineRename = async (title) => {
    if (!inlineRenameState.mode || !inlineRenameState.id || inlineRenameState.committing) return;
    const trimmed = normalizeText(title);
    if (!trimmed) {
      cancelInlineRename();
      return;
    }
    inlineRenameState.committing = true;
    const renameMode = inlineRenameState.mode;
    const id = inlineRenameState.id;
    const item = renameMode === "filetree" ? fileTreeRowById.get(id) : itemById.get(id);
    const documentId = renameMode === "filetree"
      ? getFileTreeRowDocumentId(item)
      : id;
    if (!documentId) {
      inlineRenameState = { mode: null, id: null, committing: false };
      setLastAction("当前资源暂不支持重命名", "error");
      renderTree();
      return;
    }
    try {
      const result = await sendCommand({
        action: "rename",
        workspaceId,
        documentId,
        title: trimmed,
      });
      const resultDocumentId =
        typeof result.documentId === "string" && result.documentId.trim()
          ? result.documentId.trim()
          : id;
      setStatus("重命名成功");
      setLastAction(`已重命名为 ${trimmed}`);
      postToHost("tree.node.renamed", {
        documentId: resultDocumentId,
        target: { documentId: resultDocumentId },
        payload: { documentId: resultDocumentId },
      });
      inlineRenameState = { mode: null, id: null, committing: false };
      scheduleRefresh();
    } catch (error) {
      const message = error instanceof Error ? error.message : "重命名失败";
      setStatus(message, "error");
      setLastAction("重命名失败", "error");
      inlineRenameState = { mode: null, id: null, committing: false };
      renderTree();
      window.alert(message);
    }
  };

  const attachInlineRenameInput = (input, originalTitle) => {
    input.addEventListener("click", (event) => event.stopPropagation());
    input.addEventListener("dblclick", (event) => event.stopPropagation());
    input.addEventListener("keydown", (event) => {
      event.stopPropagation();
      if (event.key === "Enter") {
        event.preventDefault();
        void commitInlineRename(input.value);
      } else if (event.key === "Escape") {
        event.preventDefault();
        cancelInlineRename();
      }
    });
    input.addEventListener("blur", () => {
      if (!inlineRenameState.mode || inlineRenameState.committing) return;
      if (input.value === originalTitle) {
        cancelInlineRename();
        return;
      }
      void commitInlineRename(input.value);
    });
  };

  const handleMove = async (nodeId, delta) => {
    const item = itemById.get(nodeId);
    if (!item) return;
    const siblings = getSiblings(item.parentNodeId);
    const currentIndex = siblings.findIndex((entry) => entry.nodeId === nodeId);
    if (currentIndex === -1) return;
    const nextIndex = currentIndex + delta;
    if (nextIndex < 0 || nextIndex >= siblings.length) return;
    try {
      const result = await sendCommand({
        action: "move",
        workspaceId,
        documentId: nodeId,
        parentId: item.parentNodeId,
        sortOrder: nextIndex,
      });
      const documentId =
        typeof result.documentId === "string" && result.documentId.trim()
          ? result.documentId.trim()
          : nodeId;
      setStatus("移动页面成功");
      setLastAction(delta < 0 ? "页面已上移" : "页面已下移");
      postToHost("tree.subtree.moved", {
        documentId,
        target: { documentId },
        payload: { documentId },
      });
      scheduleRefresh();
    } catch (error) {
      const message = error instanceof Error ? error.message : "移动页面失败";
      setStatus(message, "error");
      setLastAction("移动页面失败", "error");
      window.alert(message);
    }
  };

  const handlePageDropMove = async (sourceNodeId, targetNodeId) => {
    const sourceItem = itemById.get(sourceNodeId);
    const targetItem = itemById.get(targetNodeId);
    if (!sourceItem || !targetItem) return;
    const siblings = getSiblings(targetItem.parentNodeId);
    const targetIndex = siblings.findIndex((entry) => entry.nodeId === targetNodeId);
    if (targetIndex < 0) return;
    try {
      const result = await sendCommand({
        action: "move",
        workspaceId,
        documentId: sourceNodeId,
        parentId: targetItem.parentNodeId,
        sortOrder: targetIndex,
      });
      const documentId =
        typeof result.documentId === "string" && result.documentId.trim()
          ? result.documentId.trim()
          : sourceNodeId;
      setStatus("移动页面成功");
      setLastAction(`页面已拖放到 ${targetItem.title}`);
      postToHost("tree.subtree.moved", {
        documentId,
        target: { documentId },
        payload: { documentId },
      });
      scheduleRefresh();
    } catch (error) {
      const message = error instanceof Error ? error.message : "拖拽移动失败";
      setStatus(message, "error");
      setLastAction("拖拽移动失败", "error");
      window.alert(message);
    }
  };

  const bindPageRowEvents = (row, item) => {
    if (!(row instanceof HTMLElement) || !item) return;
    row.dataset.active = String(item.nodeId === currentActiveDocumentId);
    row.dataset.focused = String(item.nodeId === focusedNodeId);
    row.dataset.nodeId = item.nodeId;
    row.dataset.shellMode = "page";
    row.dataset.dropFeedback = String(activePageDropNodeId === item.nodeId);
    row.tabIndex = item.nodeId === focusedNodeId ? 0 : -1;
    row.setAttribute("role", "treeitem");
    row.setAttribute("aria-level", String(item.depth + 1));
    row.setAttribute("aria-expanded", item.childCount > 0 ? String(expanded.has(item.nodeId)) : "false");
    row.draggable = true;
    row.dataset.draggable = "true";
    row.addEventListener("focus", () => {
      if (focusedNodeId !== item.nodeId) {
        applyPageKeyboardAction({ kind: "focus", nodeId: item.nodeId }, item, row);
      }
    });
    row.addEventListener("keydown", (event) => handleRowKeyDown(event, item));
    row.addEventListener("contextmenu", (event) => {
      event.preventDefault();
      openContextMenu(item.nodeId, event.clientX, event.clientY);
    });
    row.addEventListener("dragstart", (event) => {
      draggingPageNodeId = item.nodeId;
      if (event.dataTransfer) {
        event.dataTransfer.effectAllowed = "move";
        event.dataTransfer.setData(PAGE_DRAG_MIME, item.nodeId);
        event.dataTransfer.setData("text/plain", item.nodeId);
      }
      setLastAction(`开始拖拽页面 ${item.title}`);
    });
    row.addEventListener("dragover", (event) => {
      const sourceNodeId = readPageDragNodeId(event);
      const targetNodeId = resolvePageDropTargetNodeId(event.target);
      if (!canAcceptPageDrop(sourceNodeId, targetNodeId)) {
        clearPageDropFeedback();
        return;
      }
      event.preventDefault();
      if (event.dataTransfer) {
        event.dataTransfer.dropEffect = "move";
      }
      setPageDropFeedback(targetNodeId);
    });
    row.addEventListener("dragleave", (event) => {
      const relatedTarget =
        event.relatedTarget instanceof Node ? event.relatedTarget : null;
      if (relatedTarget && row.contains(relatedTarget)) {
        return;
      }
      if (activePageDropNodeId === item.nodeId) {
        clearPageDropFeedback();
      }
    });
    row.addEventListener("drop", (event) => {
      const sourceNodeId = readPageDragNodeId(event);
      const targetNodeId = resolvePageDropTargetNodeId(event.target);
      clearPageDropFeedback();
      draggingPageNodeId = "";
      if (!canAcceptPageDrop(sourceNodeId, targetNodeId)) {
        return;
      }
      event.preventDefault();
      void handlePageDropMove(sourceNodeId, targetNodeId);
    });
    row.addEventListener("dragend", () => {
      draggingPageNodeId = "";
      clearPageDropFeedback();
    });

    row.querySelectorAll("[data-rust-action]").forEach((element) => {
      if (!(element instanceof HTMLElement)) return;
      element.addEventListener("click", (event) => {
        event.stopPropagation();
        const action = normalizeText(element.dataset.rustAction);
        if (action === "toggle") {
          event.preventDefault();
          applyPageKeyboardAction({ kind: "toggle", nodeId: item.nodeId }, item, element);
        } else if (action === "open") {
          applyPageKeyboardAction({ kind: "open", nodeId: item.nodeId }, item, element);
        } else if (action === "create") {
          void handleCreate(item.nodeId);
        } else if (action === "rename") {
          beginInlineRename("page", item.nodeId);
        } else if (action === "menu") {
          applyPageKeyboardAction({ kind: "context_menu", nodeId: item.nodeId }, item, element);
        }
      });
    });
  };

  const patchPageTreeActiveDom = () =>
    patchTreeShellPageActiveDom({
      mode,
      appElement,
      normalizeText,
      focusedNodeId,
      currentActiveDocumentId,
      activePageDropNodeId,
    });

  const patchPageTreeExpansionDom = (nodeId) =>
    patchTreeShellPageExpansionDom({
      mode,
      appElement,
      normalizeText,
      itemById,
      expanded,
      getSiblings,
      renderNode,
      patchPageTreeActiveDom,
    }, nodeId);

  const hydrateInitialPageTree = () =>
    hydrateTreeShellInitialPageTree({
      mode,
      appElement,
      normalizeText,
      itemById,
      bindPageRowEvents,
      focusedNodeId,
      focusRowElement,
    });

  const bindPickerRootEvents = (row) => {
    if (!(row instanceof HTMLElement)) return;
    row.dataset.focused = String(resolvePickerRootFocused());
    row.addEventListener("click", () => {
      applyPickerFocusByItemKey("__root__", { focusDom: true });
      postPickerPickResultToHost(applyPickerStateAction({ kind: "pick" }));
    });
  };

  const bindPickerRowEvents = (row, item) => {
    if (!(row instanceof HTMLElement) || !item) return;
    row.dataset.nodeId = item.nodeId;
    row.dataset.shellMode = "picker";
    row.dataset.focused = String(
      currentActivePickerItemKey === item.nodeId ||
      (!currentActivePickerItemKey && currentActiveDocumentId === item.nodeId),
    );
    row.addEventListener("click", () => {
      applyPickerFocusByItemKey(item.nodeId, { focusDom: true });
      postPickerPickResultToHost(applyPickerStateAction({ kind: "pick" }));
    });
  };

  const focusPickerRowElement = (pickerItemKey) =>
    focusTreeShellPickerRowElement({
      appElement,
      normalizeText,
    }, pickerItemKey);

  const patchPickerActiveDom = () =>
    patchTreeShellPickerActiveDom({
      mode,
      appElement,
      normalizeText,
      currentActivePickerItemKey,
      currentActiveDocumentId,
    });

  const hydrateInitialPickerTree = () =>
    hydrateTreeShellInitialPickerTree({
      mode,
      appElement,
      normalizeText,
      itemById,
      bindPickerRootEvents,
      bindPickerRowEvents,
    });

  const hydrateInitialRenderer = () => {
    if (mode === "page") {
      const usedRustInitialPageRenderer = hydrateInitialPageTree();
      return usedRustInitialPageRenderer;
    }
    if (mode === "filetree") {
      return hydrateInitialFileTree();
    }
    if (mode === "picker") {
      return hydrateInitialPickerTree();
    }
    return false;
  };

  const treeShellRenderer = createTreeShellRenderer({
    FILETREE_DRAG_MIME,
    PAGE_DRAG_MIME,
    allowRootPick,
    appElement,
    applyPageKeyboardAction,
    applyPickerFocusByItemKey,
    applyPickerStateAction,
    attachInlineRenameInput,
    beginInlineRename,
    canAcceptPageDrop,
    canExpandFileTreeRow,
    clearFileTreeDropFeedback,
    clearFileTreeSelection,
    clearPageDropFeedback,
    executeLocalFileTreeExternalDrop,
    executeLocalFileTreeInternalDrop,
    expanded,
    getActiveFileTreeDropPosition,
    getActiveFileTreeDropRowId,
    getActiveFileTreeRootDrop,
    getActivePageDropNodeId: () => activePageDropNodeId,
    getCurrentActiveDocumentId: () => currentActiveDocumentId,
    getElementCenter,
    getFileTreeDropTargetFromEvent,
    getFileTreeRowById: () => fileTreeRowById,
    focusRowElement,
    getFocusedNodeId: () => focusedNodeId,
    getInlineRenameState: () => inlineRenameState,
    getItemById: () => itemById,
    getRoots: () => roots,
    getSelectedFileTreeRowIds: () => selectedFileTreeRowIds,
    getSiblings,
    handleCreate,
    handleFileTreeKeyDown,
    handleMove,
    handleNavigate,
    handlePageDropMove,
    handleRowKeyDown,
    isBusy: () => busy,
    mode,
    normalizeText,
    normalizeFileTreeSelectionForVisibleRows,
    openContextMenu,
    openFileTreeContextMenu,
    patchFileTreeCutDecoration,
    postPickerPickResultToHost,
    postToHost,
    pushVisibleFileTreeRowId: (rowId) => {
      visibleFileTreeRowIds.push(rowId);
    },
    readFileTreeInternalDropPayload,
    readPageDragNodeId,
    resolveFileTreeDraggedRowIds,
    resolvePageDropTargetNodeId,
    resolvePickerRootFocused,
    runFileTreePreflight,
    selectFileTreeContextRow,
    selectFileTreeRow,
    setFileTreeDropFeedback,
    setDraggingFileTreeRowIds: (rowIds) => {
      draggingFileTreeRowIds = rowIds;
    },
    setDraggingPageNodeId: (nodeId) => {
      draggingPageNodeId = nodeId;
    },
    setLastAction,
    setPageDropFeedback,
    setVisibleFileTreeRowIds: (rowIds) => {
      visibleFileTreeRowIds = rowIds;
    },
    sourceKind,
    toggleExpand,
    validateFileTreeInternalDrop,
    workspaceId,
  });
  renderNode = treeShellRenderer.renderNode;
  renderTree = treeShellRenderer.renderTree;
  syncFileTreeSelectionDom = treeShellRenderer.syncFileTreeSelectionDom;
  openHydratedFileTreeItem = treeShellRenderer.openHydratedFileTreeItem;
  hydrateInitialFileTree = treeShellRenderer.hydrateInitialFileTree;

  window.addEventListener("message", (event) => {
    const payload = event.data;
    if (!payload || typeof payload !== "object") {
      return;
    }
    if (normalizeText(payload.channel) !== channel) {
      return;
    }
    const messageType = normalizeText(payload.type);
    if (messageType === "tree.picker.command") {
      handlePickerCommand(payload.command);
      return;
    }
    if (messageType !== "tree.shell.state.patch") {
      return;
    }

    let changed = false;
    const nextActiveDocumentId = normalizeText(payload.activeDocumentId);
    const nextFocusedDocumentId = normalizeText(payload.focusedDocumentId);
    const nextActivePickerItemKey = normalizeText(payload.activePickerItemKey);

    if (nextActiveDocumentId !== currentActiveDocumentId) {
      currentActiveDocumentId = nextActiveDocumentId;
      changed = true;
    }
    if (nextFocusedDocumentId !== currentFocusedDocumentId) {
      currentFocusedDocumentId = nextFocusedDocumentId;
      changed = true;
    }
    if (nextActivePickerItemKey !== currentActivePickerItemKey) {
      currentActivePickerItemKey = nextActivePickerItemKey;
      changed = true;
    }

    if (!changed) {
      return;
    }

    focusedNodeId = resolveFocusedNodeIdFromHostState();
    if (mode === "picker" && usedRustInitialRenderer) {
      patchPickerActiveDom();
      return;
    }
    renderTree();
    if (mode === "page" && focusedNodeId) {
      focusRowElement(focusedNodeId);
    }
  });

  createRootButton.addEventListener("click", () => {
    if (mode === "picker") return;
    void handleCreate(null);
  });

  const emitReady = () => {
    postToHost("tree.ready", {
      workspaceId,
      payload: { workspaceId },
    });
  };

  const usedRustInitialRenderer = hydrateInitialRenderer();
  if (!usedRustInitialRenderer) {
    renderTree();
  }
  if (mode === "page" && focusedNodeId) {
    postPageFocusChange(focusedNodeId);
  }
  if (mode === "filetree") {
    emitFileTreeSelectionChange();
  }
  if (mode === "filetree" && initialRenameRowId) {
    const url = new URL(window.location.href);
    url.searchParams.delete("renameRowId");
    window.history.replaceState(null, "", url.toString());
    window.setTimeout(() => {
      if (fileTreeRowById.has(initialRenameRowId)) {
        beginInlineRename("filetree", initialRenameRowId);
      }
    }, 120);
  }
  setStatus(
    mode === "picker"
      ? "Tree picker 已就绪，可以展开目录并选择目标页面。"
      : mode === "filetree"
        ? "File tree shell 已就绪，可以打开页面与附件。"
      : "Tree shell 已就绪，可以进行展开、创建、重命名和排序操作。",
  );
  setLastAction("已向宿主发送 ready 消息。");
  emitReady();
  window.setTimeout(emitReady, 300);
  window.setTimeout(emitReady, 1200);
}

startTreeShellRuntime();
