"use client";

import type { DragEvent, KeyboardEvent as ReactKeyboardEvent, MouseEvent, ReactNode } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
  buildTreeShellDomChildrenByParent,
  buildTreeShellDomFiletreeSelection,
  buildTreeShellDomKernelFileTreeItems,
  buildTreeShellDomPageItems,
  buildTreeShellDomPickerItems,
  type TreeShellDomProjectionItem,
  type TreeShellPickerItem,
} from "@/components/sidebar/tree-shell-dom-model";
import type {
  FileTreeShellDeleteSelectionPayload,
  FileTreeShellExternalDropPayload,
  FileTreeShellInternalDropPayload,
  TreeShellMutationPayload,
  TreeShellHostMode,
  TreeShellPickerCommand,
} from "@/components/sidebar/tree-shell-host";
import {
  normalizeFileTreeSelectionForVisibleRows as normalizeVisibleFileTreeSelection,
  reduceFileTreeSelection,
} from "@/lib/file-tree/selection";
import type { KernelFileTreeProjectionItem } from "@/lib/kernel-file-tree";
import type { PageTreeProjectionItem } from "@/lib/tree-projection";
import { cn } from "@/lib/utils";

type PageRuntimeState = {
  focusedId: string | null;
  expandedIds: string[];
  dropFeedback?: {
    sourceNodeId: string;
    targetNodeId?: string | null;
    targetParentId: string | null;
    position: "before" | "inside" | "after";
  } | null;
};

type FileTreeRuntimeState = {
  activeRowId: string | null;
  selection: {
    selectedRowIds: string[];
    anchorRowId: string | null;
    focusedRowId: string | null;
  };
  dragRowIds: string[];
  dragEffect: "copy" | "move" | "none" | null;
  dropTargetRowId: string | null;
};

type PickerRuntimeState = {
  activeItemKey: string | null;
};

type TreeShellRuntimeResult = {
  requestId: string;
  mode: "page" | "fileTree" | "picker";
  state?: { mode: "page"; state: PageRuntimeState } | { mode: "fileTree"; state: FileTreeRuntimeState } | { mode: "picker"; state: PickerRuntimeState };
  hostEvents?: Array<Record<string, unknown>>;
  commandEvents?: Array<Record<string, unknown>>;
};

type TreeShellRustDomShellHostProps = {
  mode: TreeShellHostMode;
  surfaceTestId: string;
  workspaceId: string;
  rootNodeId?: string | null;
  activeDocumentId?: string | null;
  focusedDocumentId?: string | null;
  activePickerItemKey?: string | null;
  allowRootPick?: boolean;
  excludeIds?: string[];
  pickerCommand?: TreeShellPickerCommand | null;
  pickerItems?: TreeShellPickerItem[];
  pageTreeItems?: PageTreeProjectionItem[];
  inlineFileTreeItems?: KernelFileTreeProjectionItem[];
  channel?: string;
  host?: string;
  onNavigate?: (documentId: string) => void;
  onPick?: (targetId: string | null) => void;
  onPickerFocusChange?: (payload: { itemKey: string | null; documentId: string | null }) => void;
  onPageContextMenu?: (payload: { documentId: string; x: number; y: number }) => void;
  onPageExpandChange?: (payload: { documentId: string | null; expanded: boolean }) => void;
  onPageFocusChange?: (payload: { documentId: string | null }) => void;
  onFileTreeContextMenu?: (payload: {
    documentId: string | null;
    assetId: string | null;
    rowId: string | null;
    rowKind: string | null;
    x: number;
    y: number;
  }) => void;
  onFileTreeSelectionChange?: (payload: {
    selectedRowIds: string[];
    anchorRowId: string | null;
    focusedRowId: string | null;
  }) => void;
  onFileTreeDeleteSelection?: (payload: FileTreeShellDeleteSelectionPayload) => void;
  onInternalDrop?: (payload: FileTreeShellInternalDropPayload) => void;
  onDropFiles?: (payload: FileTreeShellExternalDropPayload) => void;
  onAssetOpen?: (payload: { assetId: string; documentId: string | null }) => void;
  onTreeMutation?: (payload: TreeShellMutationPayload) => void;
  children?: ReactNode;
};

const PAGE_DROP_MIME = "application/x-mnote-page-tree-node";
const FILETREE_DROP_MIME = "application/x-mnote-file-tree";
const LEGACY_FILETREE_DROP_MIME = "application/x-mnote-filetree-row-ids";
const RUNTIME_REDUCE_ENDPOINT = "/api/tree/runtime/reduce";
const RUNTIME_JS_GLUE_URL = "/api/tree-shell-runtime/mnote-tree-shell-runtime.js";
const RUNTIME_WASM_MODULE_URL = "/api/tree-shell-runtime/mnote-tree-shell-runtime_bg.wasm";

type TreeShellRuntimeWasmModule = {
  default?: (moduleOrPath?: string | URL | Request) => Promise<unknown> | unknown;
  init?: (moduleOrPath?: string | URL | Request) => Promise<unknown> | unknown;
  reduceTreeShellRuntime?: (request: unknown) => Promise<unknown> | unknown;
  reduce_tree_shell_runtime?: (request: unknown) => Promise<unknown> | unknown;
};

type TreeShellRuntimeReducer = (request: unknown) => Promise<unknown> | unknown;

type TreeCommandResponsePayload = {
  id?: string;
  result?: {
    documentId?: string;
    parentId?: string | null;
    title?: string | null;
    sortOrder?: number | null;
    workspaceId?: string | null;
    updatedAt?: string | null;
    execution?: TreeShellMutationPayload["execution"];
  } | null;
};

type TreeShellRuntimeGlobal = typeof globalThis & {
  __MNOTE_TREE_SHELL_RUNTIME__?: {
    reduceTreeShellRuntime?: TreeShellRuntimeReducer;
    reduce_tree_shell_runtime?: TreeShellRuntimeReducer;
  };
};

let treeShellRuntimeWasmPromise:
  | Promise<{ reduceTreeShellRuntime: (request: unknown) => Promise<TreeShellRuntimeResult> }>
  | null = null;

const normalizeString = (value: unknown, defaultValue = "") => {
  if (typeof value !== "string") {
    return defaultValue;
  }
  const trimmed = value.trim();
  return trimmed || defaultValue;
};

const isRecord = (value: unknown): value is Record<string, unknown> =>
  Boolean(value && typeof value === "object" && !Array.isArray(value));

const normalizeStringArray = (value: unknown) =>
  Array.isArray(value) ? value.map((item) => normalizeString(item)).filter(Boolean) : [];

function toRowId(item: TreeShellDomProjectionItem) {
  return item.rowId || item.nodeId;
}

function toDocumentId(item: TreeShellDomProjectionItem) {
  return normalizeString(item.resourceMeta?.documentId, item.nodeId);
}

function toAssetId(item: TreeShellDomProjectionItem) {
  return normalizeString(item.resourceMeta?.assetId) || null;
}

function toFiletreeTestId(item: TreeShellDomProjectionItem) {
  const rowKind = normalizeString(item.rowKind, "doc");
  if (rowKind === "doc" || rowKind === "document") return "filetree-doc-row";
  if (rowKind === "index") return "filetree-index-row";
  if (rowKind === "asset-folder" || rowKind === "asset_folder") return "filetree-asset-folder-row";
  return "filetree-asset-row";
}

function toShellRowKind(item: TreeShellDomProjectionItem) {
  const rowKind = normalizeString(item.rowKind, "doc");
  if (rowKind === "document") return "doc";
  if (rowKind === "assetFolder" || rowKind === "asset_folder") return "asset-folder";
  return rowKind;
}

function normalizeShellRowKind(value: unknown, defaultValue = "") {
  const rowKind = normalizeString(value, defaultValue);
  if (rowKind === "document") return "doc";
  if (rowKind === "assetFolder" || rowKind === "asset_folder") return "asset-folder";
  return rowKind;
}

function readTreeCommandResult(payload: unknown): NonNullable<TreeCommandResponsePayload["result"]> {
  if (!isRecord(payload) || !isRecord(payload.result)) {
    return {};
  }
  return payload.result;
}

function readTreeCommandDocumentId(payload: unknown, result: NonNullable<TreeCommandResponsePayload["result"]>) {
  if (result.documentId) {
    return normalizeString(result.documentId);
  }
  return isRecord(payload) ? normalizeString(payload.id) : "";
}

function itemHasVisibleChildren(item: TreeShellDomProjectionItem, childrenByParent: Map<string, TreeShellDomProjectionItem[]>) {
  return item.childCount > 0 && (childrenByParent.get(item.nodeId)?.length ?? 0) > 0;
}

function readFiletreeDragRowIds(dataTransfer: DataTransfer) {
  for (const type of [FILETREE_DROP_MIME, LEGACY_FILETREE_DROP_MIME, "text/plain"]) {
    const raw = dataTransfer.getData(type);
    if (!raw) continue;
    try {
      const parsed = JSON.parse(raw) as { rowIds?: unknown };
      if (Array.isArray(parsed.rowIds)) {
        return parsed.rowIds.map((rowId) => normalizeString(rowId)).filter(Boolean);
      }
    } catch {
      const normalized = normalizeString(raw);
      if (normalized) return [normalized];
    }
  }
  return [];
}

function normalizeRuntimeState<T>(result: TreeShellRuntimeResult | null, mode: "page" | "fileTree" | "picker"): T | null {
  if (!result?.state || result.state.mode !== mode) {
    return null;
  }
  return result.state.state as T;
}

function areFileTreeSelectionsEqual(
  left: FileTreeRuntimeState["selection"],
  right: FileTreeRuntimeState["selection"],
) {
  if (left.anchorRowId !== right.anchorRowId || left.focusedRowId !== right.focusedRowId) {
    return false;
  }
  if (left.selectedRowIds.length !== right.selectedRowIds.length) {
    return false;
  }
  return left.selectedRowIds.every((rowId, index) => rowId === right.selectedRowIds[index]);
}

function toLocalSelectionState(selection: FileTreeRuntimeState["selection"]) {
  return {
    selectedRowIds: new Set(selection.selectedRowIds),
    anchorRowId: selection.anchorRowId,
    focusedRowId: selection.focusedRowId,
  };
}

function fromLocalSelectionState(selection: ReturnType<typeof toLocalSelectionState>) {
  return {
    selectedRowIds: Array.from(selection.selectedRowIds),
    anchorRowId: selection.anchorRowId,
    focusedRowId: selection.focusedRowId,
  };
}

async function loadTreeShellRuntimeWasm() {
  if (!treeShellRuntimeWasmPromise) {
    treeShellRuntimeWasmPromise = (async () => {
      const preloadedRuntime = (globalThis as TreeShellRuntimeGlobal).__MNOTE_TREE_SHELL_RUNTIME__;
      const preloadedReducer =
        typeof preloadedRuntime?.reduceTreeShellRuntime === "function"
          ? preloadedRuntime.reduceTreeShellRuntime
          : typeof preloadedRuntime?.reduce_tree_shell_runtime === "function"
            ? preloadedRuntime.reduce_tree_shell_runtime
            : null;
      if (preloadedReducer) {
        return {
          reduceTreeShellRuntime: async (request: unknown) =>
            (await preloadedReducer(request)) as TreeShellRuntimeResult,
        };
      }
      const runtimeModule = (await import(
        /* webpackIgnore: true */ RUNTIME_JS_GLUE_URL
      )) as TreeShellRuntimeWasmModule;
      const initRuntime =
        typeof runtimeModule.default === "function"
          ? runtimeModule.default
          : typeof runtimeModule.init === "function"
            ? runtimeModule.init
            : null;
      if (initRuntime) {
        await initRuntime(RUNTIME_WASM_MODULE_URL);
      }
      const reduceTreeShellRuntime =
        typeof runtimeModule.reduceTreeShellRuntime === "function"
          ? runtimeModule.reduceTreeShellRuntime
          : typeof runtimeModule.reduce_tree_shell_runtime === "function"
            ? runtimeModule.reduce_tree_shell_runtime
            : null;
      if (!reduceTreeShellRuntime) {
        throw new Error("tree shell wasm reducer missing");
      }
      return {
        reduceTreeShellRuntime: async (request: unknown) =>
          (await reduceTreeShellRuntime(request)) as TreeShellRuntimeResult,
      };
    })().catch((error) => {
      treeShellRuntimeWasmPromise = null;
      throw error;
    });
  }
  return treeShellRuntimeWasmPromise;
}

export function TreeShellRustDomShellHost({
  mode,
  surfaceTestId,
  workspaceId,
  activeDocumentId = null,
  focusedDocumentId = null,
  activePickerItemKey = null,
  allowRootPick = false,
  excludeIds = [],
  pickerCommand = null,
  pickerItems,
  pageTreeItems,
  inlineFileTreeItems,
  onNavigate,
  onPick,
  onPickerFocusChange,
  onPageContextMenu,
  onPageExpandChange,
  onPageFocusChange,
  onFileTreeContextMenu,
  onFileTreeSelectionChange,
  onFileTreeDeleteSelection,
  onInternalDrop,
  onDropFiles,
  onAssetOpen,
  onTreeMutation,
}: TreeShellRustDomShellHostProps) {
  const [pageState, setPageState] = useState<PageRuntimeState>(() => ({
    focusedId: focusedDocumentId ?? activeDocumentId,
    expandedIds: [],
    dropFeedback: null,
  }));
  const [fileTreeState, setFileTreeState] = useState<FileTreeRuntimeState>(() => {
    const selection = buildTreeShellDomFiletreeSelection(activeDocumentId);
    return {
      activeRowId: activeDocumentId ? `index:${activeDocumentId}` : null,
      selection,
      dragRowIds: [],
      dragEffect: null,
      dropTargetRowId: null,
    };
  });
  const [pickerState, setPickerState] = useState<PickerRuntimeState>(() => ({
    activeItemKey: activePickerItemKey,
  }));
  const pickerCommandSeqRef = useRef<number | null>(null);
  const fileTreeStateRef = useRef(fileTreeState);
  const fileTreeSelectionVersionRef = useRef(0);
  const fileTreeSelectionInitializedRef = useRef(false);

  const pageItems = useMemo(() => {
    if (mode !== "page") return [];
    const expanded = new Set(
      pageTreeItems
        ?.filter((item) => item.childCount > 0 && item.expandedByDefault)
        .map((item) => item.nodeId) ?? [],
    );
    return buildTreeShellDomPageItems(pageTreeItems ?? [], expanded);
  }, [mode, pageTreeItems]);
  const fileTreeItems = useMemo(
    () => (mode === "filetree" ? buildTreeShellDomKernelFileTreeItems(inlineFileTreeItems ?? []) : []),
    [inlineFileTreeItems, mode],
  );
  const pickerProjectionItems = useMemo(
    () => (mode === "picker" ? buildTreeShellDomPickerItems(pickerItems ?? []) : []),
    [mode, pickerItems],
  );
  const items = mode === "page" ? pageItems : mode === "filetree" ? fileTreeItems : pickerProjectionItems;
  const childrenByParent = useMemo(() => buildTreeShellDomChildrenByParent(items), [items]);
  const itemByNodeId = useMemo(
    () => new Map(items.map((item) => [item.nodeId, item])),
    [items],
  );
  const visibleFileTreeRowIds = useMemo(() => fileTreeItems.map(toRowId), [fileTreeItems]);
  const visiblePageItems = useMemo(() => {
    const expanded = new Set(pageState.expandedIds);
    const visible: TreeShellDomProjectionItem[] = [];
    const visit = (parentId: string) => {
      for (const item of childrenByParent.get(parentId) ?? []) {
        visible.push(item);
        if (expanded.has(item.nodeId)) {
          visit(item.nodeId);
        }
      }
    };
    visit("");
    return visible;
  }, [childrenByParent, pageState.expandedIds]);
  const pickerEntries = useMemo(() => {
    const excluded = new Set(excludeIds);
    const docRows = (pickerItems ?? [])
      .filter((item): item is Extract<TreeShellPickerItem, { kind: "doc"; id: string }> => {
        return item.kind === "doc" && Boolean(normalizeString(item.id)) && !excluded.has(item.id);
      })
      .map((item, index) => ({
        itemKey: item.id,
        documentId: item.id,
        title: normalizeString(item.title, "无标题"),
        subtitle: item.subtitle,
        depth: typeof item.depth === "number" && Number.isFinite(item.depth) ? Math.max(0, item.depth) : 0,
        position: index,
      }));
    return allowRootPick
      ? [
          {
            itemKey: "__root__",
            documentId: null,
            title: pickerItems?.find((item) => item.kind === "root")?.title ?? "根目录",
            subtitle: pickerItems?.find((item) => item.kind === "root")?.subtitle,
            depth: 0,
            position: -1,
          },
          ...docRows,
        ]
      : docRows;
  }, [allowRootPick, excludeIds, pickerItems]);

  const reduceRuntime = useCallback(
    async (request: Record<string, unknown>) => {
      try {
        const runtime = await loadTreeShellRuntimeWasm();
        const result = await runtime.reduceTreeShellRuntime(request);
        if (result?.requestId === request.requestId) {
          return result;
        }
      } catch {
        // 说明：WASM artifact 在测试或降级环境不可用时，退到同源 HTTP seam。
      }
      try {
        const response = await fetch(`${RUNTIME_REDUCE_ENDPOINT}?workspaceId=${encodeURIComponent(workspaceId)}`, {
          method: "POST",
          credentials: "include",
          headers: {
            accept: "application/json",
            "content-type": "application/json",
          },
          body: JSON.stringify(request),
        });
        if (!response.ok) {
          return null;
        }
        return (await response.json().catch(() => null)) as TreeShellRuntimeResult | null;
      } catch {
        return null;
      }
    },
    [workspaceId],
  );

  const reducePageAction = useCallback(
    async (action: Record<string, unknown>, stateSnapshot: PageRuntimeState = pageState) => {
      const requestId = `page-${Date.now()}-${Math.random().toString(36).slice(2)}`;
      const result = await reduceRuntime({
        mode: "page",
        requestId,
        environment: {
          visibleNodeIds: visiblePageItems.map((item) => item.nodeId),
          expandableNodeIds: pageItems.filter((item) => itemHasVisibleChildren(item, childrenByParent)).map((item) => item.nodeId),
          rows: pageItems.map((item) => ({
            nodeId: item.nodeId,
            parentNodeId: item.parentNodeId,
            position: item.position,
          })),
        },
        state: stateSnapshot,
        action,
      });
      const nextState = normalizeRuntimeState<PageRuntimeState>(result, "page");
      if (nextState) {
        setPageState(nextState);
        return { result, state: nextState };
      }
      return { result: null, state: stateSnapshot };
    },
    [childrenByParent, pageItems, pageState, reduceRuntime, visiblePageItems],
  );

  useEffect(() => {
    fileTreeStateRef.current = fileTreeState;
  }, [fileTreeState]);

  const updateFileTreeState = useCallback(
    (updater: (prev: FileTreeRuntimeState) => FileTreeRuntimeState) => {
      let nextStateSnapshot = fileTreeStateRef.current;
      setFileTreeState((prev) => {
        const nextState = updater(prev);
        nextStateSnapshot = nextState;
        fileTreeStateRef.current = nextState;
        return nextState;
      });
      return nextStateSnapshot;
    },
    [],
  );

  const commitFileTreeSelection = useCallback(
    (nextSelection: FileTreeRuntimeState["selection"]) => {
      let changed = false;
      const nextState = updateFileTreeState((prev) => {
        if (areFileTreeSelectionsEqual(prev.selection, nextSelection)) {
          return prev;
        }
        changed = true;
        return {
          ...prev,
          selection: nextSelection,
        };
      });
      fileTreeSelectionInitializedRef.current = true;
      if (changed) {
        fileTreeSelectionVersionRef.current += 1;
        onFileTreeSelectionChange?.(nextSelection);
      }
      return nextState;
    },
    [onFileTreeSelectionChange, updateFileTreeState],
  );

  const readFileTreeState = useCallback(() => fileTreeStateRef.current, []);

  const reduceFileTreeAction = useCallback(
    async (
      action: Record<string, unknown>,
      stateSnapshot: FileTreeRuntimeState = readFileTreeState(),
      options?: { selectionVersion?: number },
    ) => {
      const requestId = `filetree-${Date.now()}-${Math.random().toString(36).slice(2)}`;
      const result = await reduceRuntime({
        mode: "fileTree",
        requestId,
        environment: {
          visibleRowIds: fileTreeItems.map(toRowId),
          rows: fileTreeItems.map((item) => ({
            rowId: toRowId(item),
            rowKind: toShellRowKind(item),
            documentId: toDocumentId(item) || null,
            assetId: toAssetId(item),
          })),
        },
        state: stateSnapshot,
        action,
      });
      const nextState = normalizeRuntimeState<FileTreeRuntimeState>(result, "fileTree");
      if (nextState) {
        const shouldApplySelection =
          options?.selectionVersion === undefined || options.selectionVersion === fileTreeSelectionVersionRef.current;
        const mergedState = updateFileTreeState((prev) => ({
          ...nextState,
          selection: shouldApplySelection ? nextState.selection : prev.selection,
        }));
        if (shouldApplySelection && !areFileTreeSelectionsEqual(stateSnapshot.selection, mergedState.selection)) {
          onFileTreeSelectionChange?.(mergedState.selection);
        }
        return { result, state: mergedState };
      }
      return { result: null, state: stateSnapshot };
    },
    [fileTreeItems, onFileTreeSelectionChange, readFileTreeState, reduceRuntime, updateFileTreeState],
  );

  const reducePickerAction = useCallback(
    async (action: Record<string, unknown>, stateSnapshot: PickerRuntimeState = pickerState) => {
      const requestId = `picker-${Date.now()}-${Math.random().toString(36).slice(2)}`;
      const result = await reduceRuntime({
        mode: "picker",
        requestId,
        environment: {
          items: [
            ...(allowRootPick
              ? [{ itemKey: "__root__", documentId: null, pickable: true }]
              : []),
            ...pickerEntries
              .filter((item) => item.documentId)
              .map((item) => ({
                itemKey: item.itemKey,
                documentId: item.documentId,
                pickable: true,
              })),
          ],
          excludedIds: excludeIds,
          allowRootPick,
        },
        state: stateSnapshot,
        action,
      });
      const nextState = normalizeRuntimeState<PickerRuntimeState>(result, "picker");
      if (nextState) {
        setPickerState(nextState);
        const documentId = nextState.activeItemKey === "__root__" ? null : nextState.activeItemKey;
        onPickerFocusChange?.({ itemKey: nextState.activeItemKey, documentId });
        return { result, state: nextState };
      }
      return { result: null, state: stateSnapshot };
    },
    [allowRootPick, excludeIds, onPickerFocusChange, pickerEntries, pickerState, reduceRuntime],
  );

  const applyPageHostEvents = useCallback(
    (hostEvents?: Array<Record<string, unknown>>, sourceElement?: HTMLElement | null) => {
      for (const event of hostEvents ?? []) {
        if (event.kind === "pageOpen") {
          const nodeId = normalizeString(event.nodeId);
          if (!nodeId) continue;
          onPageFocusChange?.({ documentId: nodeId });
          onNavigate?.(nodeId);
          continue;
        }
        if (event.kind === "pageContextMenu") {
          const nodeId = normalizeString(event.nodeId);
          if (!nodeId) continue;
          const rect = sourceElement?.getBoundingClientRect();
          onPageContextMenu?.({
            documentId: nodeId,
            x: rect ? rect.left + rect.width / 2 : 0,
            y: rect ? rect.top + rect.height / 2 : 0,
          });
        }
      }
    },
    [onNavigate, onPageContextMenu, onPageFocusChange],
  );

  const buildFileTreeDropPayload = useCallback(
    (event: Record<string, unknown>, context: { rowId: string | null; rowKind: string | null; files?: FileList | File[] }) => {
      const target = isRecord(event.target) ? event.target : {};
      const targetRowId = normalizeString(event.targetRowId, context.rowId ?? "") || null;
      const targetRowKind = normalizeShellRowKind(target.kind, context.rowKind ?? "") || null;
      const documentId = normalizeString(target.documentId) || null;
      const assetId = normalizeString(target.assetId) || null;
      return {
        targetDocumentId: documentId,
        targetRowId,
        targetRowKind,
        targetAssetId: assetId,
        files: context.files,
      };
    },
    [],
  );

  const applyFileTreeHostEvents = useCallback(
    (
      hostEvents: Array<Record<string, unknown>> | undefined,
      context?: {
        rowId?: string | null;
        rowKind?: string | null;
        x?: number;
        y?: number;
        rowIds?: string[];
        copy?: boolean;
        files?: FileList | File[];
      },
    ) => {
      for (const event of hostEvents ?? []) {
        const target = isRecord(event.target) ? event.target : {};
        const documentId = normalizeString(target.documentId) || null;
        const assetId = normalizeString(target.assetId) || null;
        const rowId = normalizeString(event.rowId, context?.rowId ?? "") || normalizeString(event.targetRowId, context?.rowId ?? "") || null;
        const rowKind = normalizeShellRowKind(target.kind, context?.rowKind ?? "") || null;
        if (event.kind === "fileTreeOpen") {
          if (assetId) {
            onAssetOpen?.({ assetId, documentId });
          } else if (documentId) {
            onNavigate?.(documentId);
          }
          continue;
        }
        if (event.kind === "fileTreeKeyboardCommand") {
          window.dispatchEvent(new CustomEvent("tree.filetree.keyboard-command", { detail: event }));
          continue;
        }
        if (event.kind === "fileTreeContextMenu") {
          onFileTreeContextMenu?.({
            documentId,
            assetId,
            rowId,
            rowKind,
            x: context?.x ?? 0,
            y: context?.y ?? 0,
          });
          continue;
        }
        if (event.kind === "fileTreeInternalDrop") {
          const payload = buildFileTreeDropPayload(event, {
            rowId: context?.rowId ?? null,
            rowKind: context?.rowKind ?? null,
          });
          const rowIds = normalizeStringArray(event.rowIds);
          onInternalDrop?.({
            targetDocumentId: payload.targetDocumentId,
            targetRowId: payload.targetRowId,
            targetRowKind: payload.targetRowKind,
            targetAssetId: payload.targetAssetId,
            rowIds: rowIds.length > 0 ? rowIds : (context?.rowIds ?? []),
            copy: event.copy === true || context?.copy === true,
          });
          continue;
        }
        if (event.kind === "fileTreeExternalDrop") {
          const payload = buildFileTreeDropPayload(event, {
            rowId: context?.rowId ?? null,
            rowKind: context?.rowKind ?? null,
            files: context?.files,
          });
          if (payload.files) {
            onDropFiles?.({
              targetDocumentId: payload.targetDocumentId,
              targetRowId: payload.targetRowId,
              targetRowKind: payload.targetRowKind,
              targetAssetId: payload.targetAssetId,
              files: payload.files,
            });
          }
        }
      }
    },
    [buildFileTreeDropPayload, onAssetOpen, onDropFiles, onFileTreeContextMenu, onInternalDrop, onNavigate],
  );

  const applyPickerHostEvents = useCallback(
    (hostEvents?: Array<Record<string, unknown>>) => {
      for (const event of hostEvents ?? []) {
        if (event.kind === "pickerPickRoot") {
          onPick?.(null);
          continue;
        }
        if (event.kind === "pickerPickDocument") {
          const documentId = normalizeString(event.documentId);
          if (documentId) {
            onPick?.(documentId);
          }
        }
      }
    },
    [onPick],
  );

  useEffect(() => {
    if (mode !== "page") return;
    const expandedIds = pageItems.filter((item) => item.expandedByDefault).map((item) => item.nodeId);
    setPageState({
      focusedId: focusedDocumentId ?? activeDocumentId,
      expandedIds,
      dropFeedback: null,
    });
  }, [activeDocumentId, focusedDocumentId, mode, pageItems]);

  useEffect(() => {
    if (mode !== "filetree") return;
    const selection = buildTreeShellDomFiletreeSelection(activeDocumentId);
    const shouldBackfillSelection =
      !fileTreeSelectionInitializedRef.current || readFileTreeState().selection.selectedRowIds.length === 0;
    if (shouldBackfillSelection) {
      commitFileTreeSelection(selection);
    }
    updateFileTreeState((prev) => ({
      ...prev,
      activeRowId: activeDocumentId ? `index:${activeDocumentId}` : null,
      selection: shouldBackfillSelection ? selection : prev.selection,
    }));
  }, [activeDocumentId, commitFileTreeSelection, mode, readFileTreeState, updateFileTreeState]);

  useEffect(() => {
    if (mode !== "filetree") return;
    const normalizedSelection = fromLocalSelectionState(
      normalizeVisibleFileTreeSelection(toLocalSelectionState(readFileTreeState().selection), visibleFileTreeRowIds),
    );
    commitFileTreeSelection(normalizedSelection);
  }, [commitFileTreeSelection, mode, readFileTreeState, visibleFileTreeRowIds]);

  useEffect(() => {
    if (mode !== "picker") return;
    setPickerState({ activeItemKey: activePickerItemKey });
  }, [activePickerItemKey, mode, pickerEntries.length]);

  useEffect(() => {
    if (mode !== "picker" || !pickerCommand || pickerCommandSeqRef.current === pickerCommand.seq) {
      return;
    }
    pickerCommandSeqRef.current = pickerCommand.seq;
    void reducePickerAction({ kind: pickerCommand.kind }).then(({ result }) => {
      if (pickerCommand.kind === "pick") {
        applyPickerHostEvents(result?.hostEvents);
      }
    });
  }, [applyPickerHostEvents, mode, pickerCommand, reducePickerAction]);

  const runTreeCommand = useCallback(
    async (payload: Record<string, unknown>) => {
      const response = await fetch("/api/tree/commands", {
        method: "POST",
        credentials: "include",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({ workspaceId, ...payload }),
      });
      const result = await response.json().catch(() => null);
      if (!response.ok) {
        const message = result && typeof result.error === "string" ? result.error : "tree command failed";
        throw new Error(message);
      }
      return result;
    },
    [workspaceId],
  );

  const dispatchPageCommandEvents = useCallback(
    async (commandEvents?: Array<Record<string, unknown>>) => {
      for (const event of commandEvents ?? []) {
        if (event.kind === "createNode") {
          const parentId = normalizeString(event.parentNodeId) || null;
          const result = await runTreeCommand({ action: "create", parentId });
          const commandResult = readTreeCommandResult(result);
          const documentId = readTreeCommandDocumentId(result, commandResult);
          const commandParentId = normalizeString(commandResult.parentId, parentId ?? "") || parentId;
          const commandTitle = normalizeString(commandResult.title, "无标题");
          const commandSortOrder =
            typeof commandResult.sortOrder === "number" && Number.isFinite(commandResult.sortOrder)
              ? commandResult.sortOrder
              : null;
          onTreeMutation?.({
            type: "tree.node.created",
            documentId: documentId || null,
            parentId: commandParentId,
            title: commandTitle,
            sortOrder: commandSortOrder,
            workspaceId: normalizeString(commandResult.workspaceId, workspaceId) || workspaceId,
            updatedAt: normalizeString(commandResult.updatedAt) || null,
            execution: commandResult.execution ?? null,
          });
          continue;
        }
        if (event.kind === "renameNode") {
          const documentId = normalizeString(event.nodeId);
          const title = normalizeString(event.title);
          if (!documentId || !title) continue;
          const result = await runTreeCommand({ action: "rename", documentId, title });
          const commandResult = readTreeCommandResult(result);
          onTreeMutation?.({
            type: "tree.node.renamed",
            documentId,
            title: normalizeString(commandResult.title, title),
            workspaceId: normalizeString(commandResult.workspaceId, workspaceId) || workspaceId,
            updatedAt: normalizeString(commandResult.updatedAt) || null,
            execution: commandResult.execution ?? null,
          });
          continue;
        }
        if (event.kind === "moveSubtree") {
          const documentId = normalizeString(event.sourceNodeId);
          if (!documentId) continue;
          const parentId = normalizeString(event.targetParentId) || null;
          const sortOrder = typeof event.sortOrder === "number" && Number.isFinite(event.sortOrder) ? event.sortOrder : 0;
          const result = await runTreeCommand({ action: "move", documentId, parentId, sortOrder });
          const commandResult = readTreeCommandResult(result);
          const commandParentId = normalizeString(commandResult.parentId, parentId ?? "") || parentId;
          const commandSortOrder =
            typeof commandResult.sortOrder === "number" && Number.isFinite(commandResult.sortOrder)
              ? commandResult.sortOrder
              : sortOrder;
          onTreeMutation?.({
            type: "tree.subtree.moved",
            documentId,
            parentId: commandParentId,
            sortOrder: commandSortOrder,
            workspaceId: normalizeString(commandResult.workspaceId, workspaceId) || workspaceId,
            updatedAt: normalizeString(commandResult.updatedAt) || null,
            execution: commandResult.execution ?? null,
          });
        }
      }
    },
    [onTreeMutation, runTreeCommand, workspaceId],
  );

  const handlePageOpen = useCallback(
    async (nodeId: string) => {
      const { state } = await reducePageAction({ kind: "focus", nodeId });
      const { result } = await reducePageAction({ kind: "openFocused" }, state);
      applyPageHostEvents(result?.hostEvents);
    },
    [applyPageHostEvents, reducePageAction],
  );

  const handlePageToggle = useCallback(
    async (nodeId: string) => {
      const { state } = await reducePageAction({ kind: "toggle", nodeId });
      if (state !== pageState) {
        onPageExpandChange?.({ documentId: nodeId, expanded: state.expandedIds.includes(nodeId) });
      }
    },
    [onPageExpandChange, pageState, reducePageAction],
  );

  const handlePageKeyDown = useCallback(
    async (event: ReactKeyboardEvent<HTMLElement>, nodeId: string) => {
      const visibleIds = visiblePageItems.map((item) => item.nodeId);
      const index = Math.max(0, visibleIds.indexOf(nodeId));
      const focusId = async (nextId: string | null) => {
        if (!nextId) return;
        const { state } = await reducePageAction({ kind: "focus", nodeId: nextId });
        if (state.focusedId === nextId) {
          onPageFocusChange?.({ documentId: nextId });
        }
      };
      if (event.key === "ArrowDown") {
        event.preventDefault();
        await focusId(visibleIds[Math.min(visibleIds.length - 1, index + 1)] ?? null);
      } else if (event.key === "ArrowUp") {
        event.preventDefault();
        await focusId(visibleIds[Math.max(0, index - 1)] ?? null);
      } else if (event.key === "Home") {
        event.preventDefault();
        await focusId(visibleIds[0] ?? null);
      } else if (event.key === "End") {
        event.preventDefault();
        await focusId(visibleIds[visibleIds.length - 1] ?? null);
      } else if (event.key === "ArrowLeft") {
        event.preventDefault();
        if (pageState.expandedIds.includes(nodeId)) await handlePageToggle(nodeId);
      } else if (event.key === "ArrowRight") {
        event.preventDefault();
        if (!pageState.expandedIds.includes(nodeId)) await handlePageToggle(nodeId);
      } else if (event.key === "Enter") {
        event.preventDefault();
        const { state } = await reducePageAction({ kind: "focus", nodeId });
        const { result } = await reducePageAction({ kind: "openFocused" }, state);
        applyPageHostEvents(result?.hostEvents);
      }
    },
    [applyPageHostEvents, handlePageToggle, onPageFocusChange, pageState.expandedIds, reducePageAction, visiblePageItems],
  );

  const handlePageDrop = useCallback(
    async (sourceNodeId: string, targetNodeId: string) => {
      if (!sourceNodeId || !targetNodeId || sourceNodeId === targetNodeId) return;
      const { result } = await reducePageAction({
        kind: "dispatchMoveToTarget",
        sourceNodeId,
        targetNodeId,
        position: "before",
      });
      const commandEvents = result?.commandEvents;
      if (commandEvents?.length) {
        await dispatchPageCommandEvents(commandEvents);
      }
    },
    [dispatchPageCommandEvents, reducePageAction],
  );

  const handlePageAction = useCallback(
    async (kind: "create" | "rename" | "menu", nodeId: string, event?: MouseEvent) => {
      if (kind === "menu") {
        const { state } = await reducePageAction({ kind: "focus", nodeId });
        const { result } = await reducePageAction({ kind: "contextMenuFocused" }, state);
        applyPageHostEvents(result?.hostEvents, event?.currentTarget as HTMLElement | null);
        return;
      }
      const action =
        kind === "create"
          ? { kind: "dispatchCreate", parentNodeId: nodeId }
          : {
              kind: "dispatchRename",
              nodeId,
              title: window.prompt("输入新的标题", itemByNodeId.get(nodeId)?.title ?? "无标题") ?? "",
            };
      if (kind === "rename" && !normalizeString(action.title)) {
        return;
      }
      const { result } = await reducePageAction(action);
      await dispatchPageCommandEvents(result?.commandEvents);
    },
    [applyPageHostEvents, dispatchPageCommandEvents, itemByNodeId, reducePageAction],
  );

  const handleFileTreeOpen = useCallback(
    async (item: TreeShellDomProjectionItem) => {
      const rowId = toRowId(item);
      const { result } = await reduceFileTreeAction({ kind: "openRow", rowId });
      applyFileTreeHostEvents(result?.hostEvents, {
        rowId,
        rowKind: toShellRowKind(item),
      });
    },
    [applyFileTreeHostEvents, reduceFileTreeAction],
  );

  const handleFileTreeContextMenu = useCallback(
    async (item: TreeShellDomProjectionItem, event: MouseEvent) => {
      event.preventDefault();
      const rowId = toRowId(item);
      const state = commitFileTreeSelection(
        fromLocalSelectionState(
          reduceFileTreeSelection(toLocalSelectionState(readFileTreeState().selection), {
            type: "contextmenu",
            rowId,
          }),
        ),
      );
      const selectionVersion = fileTreeSelectionVersionRef.current;
      void reduceFileTreeAction({ kind: "selectContextRow", rowId }, state, { selectionVersion });
      const { result } = await reduceFileTreeAction({ kind: "contextMenuRow", rowId }, state, { selectionVersion });
      applyFileTreeHostEvents(result?.hostEvents, {
        rowId,
        rowKind: toShellRowKind(item),
        x: event.clientX,
        y: event.clientY,
      });
    },
    [applyFileTreeHostEvents, commitFileTreeSelection, readFileTreeState, reduceFileTreeAction],
  );

  const handleFileTreeSelect = useCallback(
    (item: TreeShellDomProjectionItem, event: MouseEvent) => {
      const rowId = toRowId(item);
      const state = commitFileTreeSelection(
        fromLocalSelectionState(
          reduceFileTreeSelection(toLocalSelectionState(readFileTreeState().selection), {
            type: "click",
            rowId,
            visibleRowIds: visibleFileTreeRowIds,
            modifiers: {
              shiftKey: event.shiftKey,
              ctrlKey: event.ctrlKey,
              metaKey: event.metaKey,
            },
          }),
        ),
      );
      const selectionVersion = fileTreeSelectionVersionRef.current;
      void reduceFileTreeAction({
        kind: "selectRow",
        rowId,
        modifiers: {
          shiftKey: event.shiftKey,
          ctrlKey: event.ctrlKey,
          metaKey: event.metaKey,
        },
      }, state, { selectionVersion });
    },
    [commitFileTreeSelection, readFileTreeState, reduceFileTreeAction, visibleFileTreeRowIds],
  );

  const handleFileTreeKeyDown = useCallback(
    async (item: TreeShellDomProjectionItem, event: ReactKeyboardEvent<HTMLElement>) => {
      const actionByKey: Record<string, Record<string, unknown> | undefined> = {
        ArrowDown: { kind: "focusNext" },
        ArrowUp: { kind: "focusPrevious" },
        Home: { kind: "focusFirst" },
        End: { kind: "focusLast" },
        Enter: { kind: "openFocused" },
        F2: { kind: "beginRenameFocused" },
        Delete: { kind: "deleteSelection" },
        Backspace: { kind: "deleteSelection" },
        Escape: { kind: "escape" },
      };
      const shortcut = event.ctrlKey || event.metaKey;
      const action = shortcut && event.key.toLowerCase() === "c"
        ? { kind: "copySelection" }
        : shortcut && event.key.toLowerCase() === "x"
          ? { kind: "cutSelection" }
          : shortcut && event.key.toLowerCase() === "v"
            ? { kind: "pasteIntoFocused" }
            : actionByKey[event.key];
      if (!action) return;
      event.preventDefault();
      const rowId = toRowId(item);
      const currentFileTreeState = readFileTreeState();
      if (action.kind === "deleteSelection") {
        onFileTreeDeleteSelection?.(currentFileTreeState.selection);
        return;
      }
      const { result } = await reduceFileTreeAction(action, {
        ...currentFileTreeState,
        selection: {
          ...currentFileTreeState.selection,
          focusedRowId: currentFileTreeState.selection.focusedRowId ?? rowId,
        },
      });
      applyFileTreeHostEvents(result?.hostEvents, {
        rowId,
        rowKind: toShellRowKind(item),
      });
    },
    [applyFileTreeHostEvents, onFileTreeDeleteSelection, readFileTreeState, reduceFileTreeAction],
  );

  const handleFileTreeDrop = useCallback(
    async (item: TreeShellDomProjectionItem, event: DragEvent<HTMLElement>) => {
      event.preventDefault();
      const rowId = toRowId(item);
      if (event.dataTransfer.files?.length) {
        const { result } = await reduceFileTreeAction({ kind: "dispatchExternalDrop", targetRowId: rowId, fileCount: event.dataTransfer.files.length });
        applyFileTreeHostEvents(result?.hostEvents, {
          rowId,
          rowKind: toShellRowKind(item),
          files: event.dataTransfer.files,
        });
        return;
      }
      const rowIds = readFiletreeDragRowIds(event.dataTransfer);
      if (rowIds.length === 0) return;
      const copy = Boolean(event.altKey);
      const { result } = await reduceFileTreeAction({ kind: "dispatchInternalDrop", targetRowId: rowId, rowIds, copy });
      applyFileTreeHostEvents(result?.hostEvents, {
        rowId,
        rowKind: toShellRowKind(item),
        rowIds,
        copy,
      });
    },
    [applyFileTreeHostEvents, reduceFileTreeAction],
  );

  const handlePickerPick = useCallback(
    async (itemKey: string) => {
      const { state } = await reducePickerAction({ kind: "focus", itemKey, focusDom: false });
      const { result } = await reducePickerAction({ kind: "pick" }, state);
      applyPickerHostEvents(result?.hostEvents);
    },
    [applyPickerHostEvents, reducePickerAction],
  );

  const renderPageRows = (parentId: string): ReactNode => {
    const rows = childrenByParent.get(parentId) ?? [];
    if (parentId === "" && rows.length === 0) {
      return <div className="px-4 py-6 text-sm text-gray-400">当前 projection 没有可渲染的页面。</div>;
    }
    return rows.map((item) => {
      const children = childrenByParent.get(item.nodeId) ?? [];
      const expandable = itemHasVisibleChildren(item, childrenByParent);
      const expanded = pageState.expandedIds.includes(item.nodeId);
      const focused = pageState.focusedId === item.nodeId;
      const active = activeDocumentId === item.nodeId;
      const dropFeedback = pageState.dropFeedback?.targetNodeId === item.nodeId;
      return (
        <div key={item.nodeId} className="tree-node" data-node-id={item.nodeId}>
          <div
            role="treeitem"
            aria-level={item.depth + 1}
            aria-expanded={expandable ? expanded : undefined}
            tabIndex={focused ? 0 : -1}
            className={cn(
              "tree-row group flex min-h-[27px] w-full cursor-pointer items-center gap-2 rounded-[3px] px-3 py-[2px] text-[14px] font-medium text-wolai-text-primary outline-none transition-colors hover:bg-wolai-bg-hover",
              active && "bg-wolai-bg-active text-[#2563eb]",
              dropFeedback && "bg-gray-200/70",
            )}
            style={{ paddingLeft: 8 + item.depth * 12 }}
            data-testid="page-tree-row"
            data-rust-rendered-row="page"
            data-node-id={item.nodeId}
            data-shell-mode="page"
            data-active={active}
            data-focused={focused}
            data-drop-feedback={dropFeedback}
            draggable
            onFocus={() => void reducePageAction({ kind: "focus", nodeId: item.nodeId })}
            onKeyDown={(event) => void handlePageKeyDown(event, item.nodeId)}
            onDragStart={(event) => {
              event.dataTransfer.setData(PAGE_DROP_MIME, item.nodeId);
              event.dataTransfer.setData("text/plain", item.nodeId);
            }}
            onDragOver={(event) => {
              event.preventDefault();
              event.dataTransfer.dropEffect = "move";
            }}
            onDrop={(event) => {
              event.preventDefault();
              const sourceNodeId = normalizeString(event.dataTransfer.getData(PAGE_DROP_MIME), normalizeString(event.dataTransfer.getData("text/plain")));
              void handlePageDrop(sourceNodeId, item.nodeId);
            }}
          >
            {expandable ? (
              <button
                type="button"
                className="tree-toggle flex h-5 w-5 shrink-0 items-center justify-center rounded-md hover:bg-gray-100"
                data-testid="tree-node-toggle"
                aria-label={`${expanded ? "折叠" : "展开"} ${item.title}`}
                onClick={(event) => {
                  event.stopPropagation();
                  void handlePageToggle(item.nodeId);
                }}
              >
                {expanded ? "▾" : "▸"}
              </button>
            ) : (
              <span className="tree-spacer h-5 w-5 shrink-0" aria-hidden="true" />
            )}
            <button
              type="button"
              data-testid="tree-node-open"
              className="tree-link min-w-0 flex-1 truncate text-left"
              onClick={() => void handlePageOpen(item.nodeId)}
            >
              {item.title || "无标题"}
            </button>
            <div className="tree-actions flex items-center gap-1">
              <button type="button" data-testid="tree-action-create" className="rounded-md px-1 text-gray-400 hover:bg-gray-100 hover:text-gray-700" onClick={(event) => {
                event.stopPropagation();
                void handlePageAction("create", item.nodeId, event);
              }}>+</button>
              <button type="button" data-testid="tree-action-rename" className="rounded-md px-1 text-gray-400 hover:bg-gray-100 hover:text-gray-700" onClick={(event) => {
                event.stopPropagation();
                void handlePageAction("rename", item.nodeId, event);
              }}>✎</button>
              <button type="button" data-testid="tree-action-menu" aria-label="更多操作" className="rounded-md px-1 text-gray-400 hover:bg-gray-100 hover:text-gray-700" onClick={(event) => {
                event.stopPropagation();
                void handlePageAction("menu", item.nodeId, event);
              }}>…</button>
            </div>
          </div>
          {expanded && children.length > 0 ? <div className="tree-children">{renderPageRows(item.nodeId)}</div> : null}
        </div>
      );
    });
  };

  const renderFileTreeRows = () => {
    if (fileTreeItems.length === 0) {
      return <div className="px-4 py-6 text-sm text-gray-400">当前 file tree 没有可渲染的页面。</div>;
    }
    return fileTreeItems.map((item) => {
      const rowId = toRowId(item);
      const rowKind = normalizeString(item.rowKind, "doc");
      const documentId = toDocumentId(item);
      const assetId = toAssetId(item);
      const selected = fileTreeState.selection.selectedRowIds.includes(rowId);
      const active = fileTreeState.activeRowId === rowId || activeDocumentId === documentId;
      return (
        <div
          key={rowId}
          role="treeitem"
          aria-level={item.depth + 1}
          tabIndex={fileTreeState.selection.focusedRowId === rowId ? 0 : -1}
          data-testid={toFiletreeTestId(item)}
          data-row-id={rowId}
          data-row-kind={rowKind}
          data-node-id={item.nodeId}
          data-document-id={documentId}
          data-doc-id={documentId}
          data-asset-id={assetId ?? ""}
          data-shell-mode="filetree"
          data-selected={selected}
          data-active={active}
          className={cn(
            "tree-row group flex min-h-[27px] w-full cursor-pointer items-center gap-2 rounded-[3px] px-3 py-[2px] text-[14px] font-medium text-wolai-text-primary transition-colors hover:bg-wolai-bg-hover",
            selected && "bg-wolai-bg-active text-[#2563eb]",
            fileTreeState.dropTargetRowId === rowId && "bg-gray-200/70",
          )}
          style={{ paddingLeft: 8 + item.depth * 16 }}
          draggable
          onClick={(event) => void handleFileTreeSelect(item, event)}
          onDoubleClick={() => void handleFileTreeOpen(item)}
          onKeyDown={(event) => void handleFileTreeKeyDown(item, event)}
          onContextMenu={(event) => void handleFileTreeContextMenu(item, event)}
          onDragStart={(event) => {
            const rowIds = fileTreeState.selection.selectedRowIds.includes(rowId) ? fileTreeState.selection.selectedRowIds : [rowId];
            const payload = JSON.stringify({ type: "mnote-file-tree-dnd", version: 1, rowIds });
            event.dataTransfer.setData(FILETREE_DROP_MIME, payload);
            event.dataTransfer.setData(LEGACY_FILETREE_DROP_MIME, payload);
            event.dataTransfer.setData("text/plain", payload);
          }}
          onDragOver={(event) => {
            event.preventDefault();
            event.dataTransfer.dropEffect = event.altKey ? "copy" : "move";
            void reduceFileTreeAction({ kind: "updateDropTarget", rowId });
          }}
          onDrop={(event) => void handleFileTreeDrop(item, event)}
        >
          <span className="tree-spacer h-5 w-5 shrink-0" aria-hidden="true" />
          <button type="button" className="tree-link min-w-0 flex-1 truncate text-left" onClick={() => void handleFileTreeOpen(item)}>
            {item.title || "无标题"}
          </button>
          <button
            type="button"
            data-testid="filetree-action-menu"
            aria-label="更多操作"
            className="rounded-md px-1 text-gray-400 hover:bg-gray-100 hover:text-gray-700"
            onClick={(event) => {
              event.preventDefault();
              event.stopPropagation();
              void handleFileTreeContextMenu(item, event);
            }}
          >
            …
          </button>
        </div>
      );
    });
  };

  const renderPickerRows = () => {
    if (pickerEntries.length === 0) {
      return <div className="p-4 text-sm text-gray-400">没有匹配结果</div>;
    }
    return pickerEntries.map((item) => {
      const focused = pickerState.activeItemKey === item.itemKey;
      const isRoot = item.itemKey === "__root__";
      return (
        <button
          key={item.itemKey}
          type="button"
          data-testid={isRoot ? "tree-picker-root" : "tree-picker-row"}
          data-node-id={item.documentId ?? ""}
          data-item-key={item.itemKey}
          data-shell-mode="picker"
          data-focused={focused}
          className={cn(
            "tree-row w-full px-4 py-3 text-left transition-colors hover:bg-[#eef2ff]",
            focused && "bg-[#e3ecff]",
          )}
          style={!isRoot ? { paddingLeft: 16 + item.depth * 12 } : undefined}
          onMouseEnter={() => void reducePickerAction({ kind: "focus", itemKey: item.itemKey, focusDom: false })}
          onClick={() => void handlePickerPick(item.itemKey)}
        >
          <div className="text-sm font-medium text-gray-900">{item.title}</div>
          {item.subtitle ? <div className="mt-1 text-xs text-gray-500">{item.subtitle}</div> : null}
        </button>
      );
    });
  };

  return (
    <div
      data-testid={`${surfaceTestId}-dom-host`}
      data-tree-shell-mode={mode}
      data-tree-shell-channel={surfaceTestId}
      data-tree-dom-shell-ready="true"
      data-tree-runtime-artifact-host="rust_tree_shell_runtime_artifact_v1"
      data-tree-browser-bridge="dom_wasm"
      className="h-full w-full overflow-auto bg-white"
    >
      <div id="tree-shell-app" className="min-h-full">
        {mode === "page" ? (
          <div className="tree-root" role="tree" data-rust-page-renderer="dom_v1">
            {renderPageRows("")}
          </div>
        ) : mode === "filetree" ? (
          <div
            className="tree-root"
            role="tree"
            data-rust-filetree-renderer="dom_v1"
            onMouseDown={(event) => {
              if (event.target !== event.currentTarget) return;
              void reduceFileTreeAction({ kind: "clearSelection" });
            }}
          >
            {renderFileTreeRows()}
          </div>
        ) : (
          <div className="tree-root" role="tree" data-rust-picker-renderer="dom_v1">
            {renderPickerRows()}
          </div>
        )}
      </div>
    </div>
  );
}
