//! MNOTE 通用页面布局组件（Wolai / Notion 风格）

use leptos::prelude::*;

const SIDEBAR_TREE_JS: &str = r##"
(function(){
  if (window.__mnoteSidebarTreeRuntimeStarted) return;
  window.__mnoteSidebarTreeRuntimeStarted = true;

  var PAGE_DRAG_MIME = 'application/x-mnote-page-tree-node';
  var FILETREE_DRAG_MIME = 'application/x-mnote-filetree-row-ids';
  var MNOTE_SIDEBAR_TREE_MODE_KEY = 'mnote.sidebar.tree.mode';
  var MNOTE_RECENT_LOCAL_ROOTS_KEY = 'mnote.localFolder.recentRoots';
  var MNOTE_LAST_CLOUD_WORKSPACE_KEY = 'mnote.workspace.lastCloudWorkspaceId';
  var MNOTE_GLOBAL_SHOW_HEADING_NUMBERS_KEY = 'mnote.global.showHeadingNumbers';
  var mnoteNavigationInFlight = '';
  var draggingPageNodeId = '';
  var activePageDropRow = null;
  var draggingFileTreeRowIds = [];
  var activeFileTreeDropRow = null;
  var sidebarFileTreeSelection = {
    selectedRowIds: new Set(),
    anchorRowId: null,
    focusedRowId: null
  };
  var activeTreeContextMenu = null;
  var activeEditorAttachmentLink = null;
  var attachmentActionsHideTimer = 0;
  var pageUiState = {
    pageOptions: null,
    historySnapshots: [],
    pageSettingsOpen: false,
    pageAiOpen: false,
    pageAiBusy: false,
    pageAiMessages: [],
    pageAiSuggestionIndex: 0,
    pageAiProvider: 'hermes',
    pageAiPage: 'chat',
    pageAiRunStatus: 'idle',
    pageAiCurrentRunId: '',
    pageAiAbortController: null,
    pageAiContextScope: 'page',
    pageAiProfiles: [],
    pageAiActiveProfileName: 'default',
    pageAiProfileError: '',
    pageAiProfileMemory: { memory: '', user: '', soul: '' },
    pageAiProfileMemoryDrafts: { memory: '', user: '', soul: '' },
    pageAiProfileMemoryError: '',
    pageAiSkills: { categories: [], archived: [] },
    pageAiSkillQuery: '',
    pageAiSkillError: '',
    pageAiSessions: [],
    pageAiActiveSessionId: ''
  };

  function closestAction(target, selector) {
    return target && typeof target.closest === 'function' ? target.closest(selector) : null;
  }

  function escapeHtml(value) {
    return String(value == null ? '' : value)
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#39;');
  }

  function cssEscape(value) {
    if (window.CSS && typeof window.CSS.escape === 'function') return window.CSS.escape(value);
    return String(value).replace(/["\\]/g, '\\$&');
  }

  function parseJsonScript(id) {
    var node = document.getElementById(id);
    if (!node) return null;
    try {
      return JSON.parse(node.textContent || 'null');
    } catch (_) {
      return null;
    }
  }

  function currentDocumentId() {
    var match = window.location.pathname.match(/^\/documents\/([^\/]+)/);
    return match ? decodeURIComponent(match[1]) : '';
  }

  function currentPageAggregate() {
    return parseJsonScript('__MNOTE_PAGE_AGGREGATE__') || {};
  }

  function textFromUnknown(value) {
    if (value == null) return '';
    if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return String(value);
    if (Array.isArray(value)) return value.map(textFromUnknown).filter(Boolean).join(' ');
    if (typeof value !== 'object') return '';
    var parts = [];
    ['text', 'title', 'content', 'children', 'blocks', 'body'].forEach(function(key) {
      if (Object.prototype.hasOwnProperty.call(value, key)) {
        var text = textFromUnknown(value[key]);
        if (text) parts.push(text);
      }
    });
    return parts.join(' ');
  }

  function readLocalEditorBlocks() {
    var editor = document.querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"] .editor-surface .ProseMirror');
    if (!(editor instanceof HTMLElement)) return [];
    return Array.from(editor.children).filter(function(node) {
      return node instanceof HTMLElement;
    }).map(function(node, index) {
      var tag = String(node.tagName || '').toUpperCase();
      var headingMatch = tag.match(/^H([1-6])$/);
      var type = headingMatch ? 'heading' : 'paragraph';
      var text = searchText(node.textContent || '');
      var id = searchText(node.getAttribute('data-id') || node.id || 'local-block-' + index);
      var props = headingMatch ? { level: Number(headingMatch[1]) } : {};
      return { id: id, type: type, props: props, content: text };
    }).filter(function(block) {
      return block.content || block.type === 'heading';
    });
  }

  function buildPageAiLocalSubtree(blocks, title) {
    var documentId = currentDocumentId() || 'current-page';
    var rootNodeId = 'page:' + documentId;
    var headingCounters = [0, 0, 0, 0, 0, 0];
    var headingStack = [];
    var nodes = [{
      id: rootNodeId,
      nodeId: rootNodeId,
      nodeType: 'page',
      blockId: null,
      blockType: 'page',
      title: title || '',
      parentNodeId: null,
      headingLevel: null
    }];
    var outline = [];
    var evidence = [];
    blocks.forEach(function(block, index) {
      var level = block.type === 'heading' ? Math.max(1, Math.min(6, Number(block.props && block.props.level || 1))) : null;
      if (level != null) {
        while (headingStack.length && headingStack[headingStack.length - 1].level >= level) headingStack.pop();
      }
      var parentNodeId = headingStack.length ? headingStack[headingStack.length - 1].nodeId : rootNodeId;
      var nodeId = 'local-node:' + String(block.id || index);
      var titleText = block.content || (block.type === 'heading' ? '未命名标题' : '');
      nodes.push({
        id: nodeId,
        nodeId: nodeId,
        nodeType: 'block',
        blockId: block.id,
        blockType: block.type,
        title: titleText,
        parentNodeId: parentNodeId,
        headingLevel: level
      });
      if (level != null) {
        headingCounters[level - 1] += 1;
        for (var i = level; i < headingCounters.length; i += 1) headingCounters[i] = 0;
        var numbering = headingCounters.slice(0, level).filter(function(count) { return count > 0; }).join('.');
        outline.push({
          id: 'local-outline:' + String(block.id || index),
          nodeId: nodeId,
          anchorBlockId: block.id,
          level: level,
          title: titleText,
          numbering: numbering
        });
        headingStack.push({ level: level, nodeId: nodeId });
      }
      if (titleText) {
        evidence.push({
          id: 'local-evidence:' + String(block.id || index),
          nodeId: nodeId,
          anchorBlockId: block.id,
          text: titleText,
          kind: block.type
        });
      }
    });
    return {
      projectionId: 'local-editor-dom:' + documentId,
      rootNode: {
        id: rootNodeId,
        documentId: documentId,
        title: title || '',
        nodeType: 'page'
      },
      subtree: {
        rootNodeId: rootNodeId,
        nodes: nodes
      },
      outline: outline,
      evidence: evidence,
      stats: {
        nodeCount: nodes.length,
        headingCount: outline.length,
        evidenceCount: evidence.length
      },
      source: 'local'
    };
  }

  function currentPageAiContextSnapshot() {
    var aggregate = currentPageAggregate();
    var body = aggregate.body || {};
    var serverContent = body.content || null;
    var serverText = searchText(textFromUnknown(serverContent));
    var localBlocks = readLocalEditorBlocks();
    var localText = searchText(localBlocks.map(function(block) { return block.content || ''; }).join(' '));
    var serverSubtree = aggregate.tree && aggregate.tree.pageSubtree ? aggregate.tree.pageSubtree : null;
    var title = aggregate.head && aggregate.head.title ? aggregate.head.title : '';
    if (localBlocks.length && localText && localText !== serverText) {
      return {
        aggregate: aggregate,
        body: Object.assign({}, body, { content: localBlocks }),
        subtree: buildPageAiLocalSubtree(localBlocks, title),
        pageSubtreeSource: 'local'
      };
    }
    return {
      aggregate: aggregate,
      body: body,
      subtree: serverSubtree,
      pageSubtreeSource: serverSubtree ? 'server' : 'none'
    };
  }

  function defaultPageOptions() {
    return {
      wideLayout: false,
      smallText: false,
      layoutDensity: 'normal',
      showHeadingNumbers: false,
      showToc: false,
      showStructure: false,
      protectEditing: false,
      showWordCount: true,
      collapseBacklinks: false,
      pageFont: 'default',
      hideChildPages: false,
      showBlockRefCount: false,
      embedDefaultBlockId: null
    };
  }

  function currentPageOptions() {
    if (pageUiState.pageOptions) return pageUiState.pageOptions;
    var aggregate = currentPageAggregate();
    var current = aggregate && aggregate.layout && aggregate.layout.pageOptions && typeof aggregate.layout.pageOptions === 'object'
      ? aggregate.layout.pageOptions
      : null;
    if (!current) {
      return defaultPageOptions();
    }
    pageUiState.pageOptions = Object.assign(defaultPageOptions(), current);
    return pageUiState.pageOptions;
  }

  function readGlobalShowHeadingNumbers() {
    try {
      var raw = window.localStorage ? window.localStorage.getItem(MNOTE_GLOBAL_SHOW_HEADING_NUMBERS_KEY) : '';
      return raw === 'true' || raw === '1';
    } catch (_) {
      return false;
    }
  }

  function writeGlobalShowHeadingNumbers(value) {
    try {
      if (window.localStorage) {
        window.localStorage.setItem(MNOTE_GLOBAL_SHOW_HEADING_NUMBERS_KEY, value ? 'true' : 'false');
      }
    } catch (_) {}
    document.documentElement.setAttribute('data-global-show-heading-numbers', String(Boolean(value)));
  }

  function effectiveShowHeadingNumbers(options) {
    return readGlobalShowHeadingNumbers();
  }

  function pageOptionIsSupported(name) {
    return name === 'wideLayout'
      || name === 'smallText'
      || name === 'layoutDensity'
      || name === 'pageFont'
      || name === 'showHeadingNumbers';
  }

  function pageOptionDescription(name) {
    if (name === 'wideLayout') return '自适应宽度';
    if (name === 'smallText') return '小字体';
    if (name === 'showToc') return '标题目录';
    if (name === 'showHeadingNumbers') return '标题自动编号';
    if (name === 'protectEditing') return '编辑保护';
    if (name === 'collapseBacklinks') return '折叠反向引用';
    if (name === 'hideChildPages') return '隐藏子页面';
    if (name === 'showBlockRefCount') return '显示块引用数字';
    return name;
  }

  function pageOptionHint(name) {
    if (name === 'wideLayout') return '已接通：主内容列宽度立即变化';
    if (name === 'smallText') return '已接通：正文排版会更紧凑';
    if (name === 'showHeadingNumbers') return '已接通：标题前显示顺序编号';
    if (name === 'layoutDensity') return '已接通：段落与列表间距会变化';
    if (name === 'pageFont') return '已接通：当前页面字体会切换';
    if (name === 'showToc') return '待接线：当前 Rust 壳还没有正式目录面板';
    if (name === 'protectEditing') return '待接线：当前主编辑器只显示降级说明';
    if (name === 'collapseBacklinks') return '待接线：当前 Rust 壳未挂回链面板';
    if (name === 'hideChildPages') return '待接线：当前页面壳还没有子页面块显隐';
    if (name === 'showBlockRefCount') return '待接线：当前页面壳未显示块引用计数';
    return '待接线';
  }

  function ensureHistorySnapshotsSeeded() {
    if (pageUiState.historySnapshots.length > 0) return;
    var aggregate = currentPageAggregate();
    var stats = aggregate && aggregate.stats && typeof aggregate.stats === 'object' ? aggregate.stats : {};
    pageUiState.historySnapshots = [{
      id: 'snapshot-initial',
      timestamp: Date.now(),
      stats: {
        wordCount: Number(stats.wordCount || 0),
        characterCount: Number(stats.characterCount || 0),
        blockCount: Number(stats.blockCount || 0),
        todoTotal: Number(stats.todoTotal || 0),
        todoDone: Number(stats.todoDone || 0)
      }
    }];
  }

  function computeLivePageStats() {
    var aggregate = currentPageAggregate();
    var fallbackStats = aggregate && aggregate.stats && typeof aggregate.stats === 'object' ? aggregate.stats : {};
    var editor = document.querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"] .editor-surface .ProseMirror');
    if (!(editor instanceof HTMLElement)) {
      return {
        wordCount: Number(fallbackStats.wordCount || 0),
        characterCount: Number(fallbackStats.characterCount || 0),
        blockCount: Number(fallbackStats.blockCount || 0),
        todoTotal: Number(fallbackStats.todoTotal || 0),
        todoDone: Number(fallbackStats.todoDone || 0)
      };
    }
    var text = (editor.textContent || '').trim();
    var compact = text.replace(/\s+/g, ' ').trim();
    var wordCount = compact ? compact.split(' ').filter(Boolean).length : 0;
    var characterCount = text.replace(/\s/g, '').length;
    var blockCount = editor.querySelectorAll(':scope > *').length;
    var todos = Array.from(editor.querySelectorAll('input[type="checkbox"]'));
    return {
      wordCount: wordCount || Number(fallbackStats.wordCount || 0),
      characterCount: characterCount || Number(fallbackStats.characterCount || 0),
      blockCount: blockCount || Number(fallbackStats.blockCount || 0),
      todoTotal: todos.length || Number(fallbackStats.todoTotal || 0),
      todoDone: todos.filter(function(node){ return node.checked; }).length || Number(fallbackStats.todoDone || 0)
    };
  }

  function recordPageHistorySnapshot(reason, stats) {
    ensureHistorySnapshotsSeeded();
    pageUiState.historySnapshots = [{
      id: 'snapshot-' + Date.now(),
      timestamp: Date.now(),
      reason: reason || 'save',
      stats: stats || computeLivePageStats()
    }].concat(pageUiState.historySnapshots).slice(0, 15);
    renderPageHistoryDrawer();
  }

  function applyPageOptionsToShell() {
    var options = currentPageOptions();
    var globalShowHeadingNumbers = readGlobalShowHeadingNumbers();
    var showHeadingNumbers = effectiveShowHeadingNumbers(options);
    var shell = document.querySelector('.document-shell');
    var editorRoot = document.querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"]');
    var editorSurface = editorRoot && editorRoot.querySelector('.editor-surface');
    if (shell instanceof HTMLElement) {
      shell.setAttribute('data-page-wide-layout', String(Boolean(options.wideLayout)));
      shell.setAttribute('data-page-small-text', String(Boolean(options.smallText)));
      shell.setAttribute('data-layout-density', String(options.layoutDensity || 'normal'));
      shell.setAttribute('data-page-font', String(options.pageFont || 'default'));
      shell.setAttribute('data-page-show-heading-numbers', String(showHeadingNumbers));
      shell.style.width = '100%';
      shell.style.maxWidth = options.wideLayout ? '980px' : '760px';
    }
    if (editorRoot instanceof HTMLElement) {
      editorRoot.setAttribute('data-page-wide-layout', String(Boolean(options.wideLayout)));
      editorRoot.setAttribute('data-page-small-text', String(Boolean(options.smallText)));
      editorRoot.setAttribute('data-page-show-heading-numbers', String(showHeadingNumbers));
      editorRoot.setAttribute('data-page-embed-default-block-id', options.embedDefaultBlockId == null ? '' : String(options.embedDefaultBlockId));
    }
    if (editorSurface instanceof HTMLElement) {
      editorSurface.setAttribute('data-layout-density', String(options.layoutDensity || 'normal'));
      editorSurface.setAttribute('data-page-font', String(options.pageFont || 'default'));
      editorSurface.setAttribute('data-show-heading-numbers', String(showHeadingNumbers));
    }
    document.documentElement.setAttribute('data-page-wide-layout', String(Boolean(options.wideLayout)));
    document.documentElement.setAttribute('data-page-small-text', String(Boolean(options.smallText)));
    document.documentElement.setAttribute('data-layout-density', String(options.layoutDensity || 'normal'));
    document.documentElement.setAttribute('data-page-font', String(options.pageFont || 'default'));
    document.documentElement.setAttribute('data-global-show-heading-numbers', String(globalShowHeadingNumbers));
    document.documentElement.setAttribute('data-page-show-heading-numbers', String(showHeadingNumbers));
  }

  function normalizeSidebarTreeMode(value) {
    var mode = String(value || '').trim();
    return mode === 'filetree' ? 'filetree' : 'page';
  }

  function readStoredSidebarTreeMode() {
    var params = new URLSearchParams(window.location.search);
    var fromUrl = normalizeSidebarTreeMode(params.get('treeView') || params.get('sidebarTree'));
    if (params.has('treeView') || params.has('sidebarTree')) return fromUrl;
    try {
      var stored = window.sessionStorage ? window.sessionStorage.getItem(MNOTE_SIDEBAR_TREE_MODE_KEY) : '';
      return normalizeSidebarTreeMode(stored);
    } catch (_) {
      return 'page';
    }
  }

  function persistSidebarTreeMode(mode) {
    var normalized = normalizeSidebarTreeMode(mode);
    document.documentElement.setAttribute('data-mnote-sidebar-tree-mode', normalized);
    try {
      if (window.sessionStorage) window.sessionStorage.setItem(MNOTE_SIDEBAR_TREE_MODE_KEY, normalized);
    } catch (_) {}
    return normalized;
  }

  function activeSidebarTreeMode() {
    var active = document.querySelector('[data-mnote-sidebar-tree-tab][aria-selected="true"]');
    if (active instanceof HTMLElement) {
      return normalizeSidebarTreeMode(active.getAttribute('data-mnote-sidebar-tree-tab'));
    }
    return readStoredSidebarTreeMode();
  }

  function resolveWorkspaceId(trigger) {
    var direct = (trigger.getAttribute('data-workspace-id') || '').trim();
    if (direct) return direct;
    var root = trigger.closest('[data-workspace-id]');
    if (root) {
      var value = (root.getAttribute('data-workspace-id') || '').trim();
      if (value) return value;
    }
    return (new URLSearchParams(window.location.search).get('workspaceId') || 'default').trim() || 'default';
  }

  function currentSourceKind() {
    return (new URLSearchParams(window.location.search).get('sourceKind') || 'convex_workspace').trim() || 'convex_workspace';
  }

  function rememberCloudWorkspaceId(workspaceId) {
    var normalized = String(workspaceId || '').trim();
    if (!normalized || normalized === 'local-folder') return;
    try {
      if (window.localStorage) window.localStorage.setItem(MNOTE_LAST_CLOUD_WORKSPACE_KEY, normalized);
    } catch (_) {}
  }

  function readLastCloudWorkspaceId() {
    try {
      var stored = window.localStorage ? window.localStorage.getItem(MNOTE_LAST_CLOUD_WORKSPACE_KEY) : '';
      if (stored && stored.trim()) return stored.trim();
    } catch (_) {}
    var params = new URLSearchParams(window.location.search);
    var fromUrl = (params.get('workspaceId') || '').trim();
    if (fromUrl && currentSourceKind() !== 'local_folder') return fromUrl;
    var fromDom = document.querySelector('[data-workspace-id]');
    if (fromDom instanceof HTMLElement) {
      var value = (fromDom.getAttribute('data-workspace-id') || '').trim();
      if (value && value !== 'local-folder') return value;
    }
    return '';
  }

  function copyWorkspaceSourceParams(targetUrl) {
    var params = new URLSearchParams(window.location.search);
    ['sourceKind', 'rootUri', 'secondaryDocumentId', 'secondarySourceKind', 'secondaryRootUri'].forEach(function(name) {
      var value = (params.get(name) || '').trim();
      if (value) targetUrl.searchParams.set(name, value);
    });
  }

  function currentWorkspaceSourcePayload() {
    var params = new URLSearchParams(window.location.search);
    var payload = {};
    ['sourceKind', 'rootUri'].forEach(function(name) {
      var value = (params.get(name) || '').trim();
      if (value) payload[name] = value;
    });
    return payload;
  }

  function readRecentLocalRoots() {
    try {
      var raw = window.localStorage ? window.localStorage.getItem(MNOTE_RECENT_LOCAL_ROOTS_KEY) : '';
      var parsed = raw ? JSON.parse(raw) : [];
      return Array.isArray(parsed) ? parsed.filter(function(value) {
        return typeof value === 'string' && value.trim();
      }).slice(0, 10) : [];
    } catch (_) {
      return [];
    }
  }

  function rememberLocalRoot(rootUri) {
    try {
      if (!window.localStorage) return;
      var roots = readRecentLocalRoots().filter(function(value) { return value !== rootUri; });
      roots.unshift(rootUri);
      window.localStorage.setItem(MNOTE_RECENT_LOCAL_ROOTS_KEY, JSON.stringify(roots.slice(0, 10)));
    } catch (_) {}
  }

  function pathToFileRootUri(value) {
    var trimmed = String(value || '').trim();
    if (!trimmed) return '';
    if (/^file:\/\//i.test(trimmed)) return trimmed;
    if (trimmed.charAt(0) !== '/') return '';
    return 'file://' + trimmed.split('/').map(function(part, index) {
      return index === 0 ? '' : encodeURIComponent(part);
    }).join('/');
  }

  function fileRootUriToPathInput(rootUri) {
    var value = String(rootUri || '').replace(/^file:\/\//, '');
    try {
      return decodeURIComponent(value);
    } catch (_) {
      return value;
    }
  }

  function openLocalFolderRoot(rootUri) {
    if (currentSourceKind() !== 'local_folder') {
      rememberCloudWorkspaceId(resolveWorkspaceId(document.body));
    }
    rememberLocalRoot(rootUri);
    var targetUrl = new URL('/', window.location.origin);
    targetUrl.searchParams.set('treeView', 'filetree');
    targetUrl.searchParams.set('sourceKind', 'local_folder');
    targetUrl.searchParams.set('rootUri', rootUri);
    window.location.href = targetUrl.toString();
  }

  function switchToCloudWorkspace() {
    var targetUrl = new URL('/', window.location.origin);
    var workspaceId = readLastCloudWorkspaceId();
    if (workspaceId) targetUrl.searchParams.set('workspaceId', workspaceId);
    targetUrl.searchParams.set('sourceKind', 'convex_workspace');
    window.location.href = targetUrl.toString();
  }

  function closeLocalFolderDialog() {
    var existing = document.querySelector('[data-testid="mnote-local-folder-dialog"]');
    if (existing && existing.parentElement) existing.parentElement.removeChild(existing);
  }

  function readPathFromDirectoryFiles(files) {
    var first = files && files.length ? files[0] : null;
    if (!first) return '';
    var rawPath = typeof first.path === 'string' ? first.path : '';
    var relative = typeof first.webkitRelativePath === 'string' ? first.webkitRelativePath : '';
    if (rawPath && relative) {
      var suffix = relative.split('/').filter(Boolean).join('/');
      if (suffix && rawPath.endsWith(suffix)) {
        return rawPath.slice(0, rawPath.length - suffix.length).replace(/[\/\\]$/, '');
      }
    }
    if (rawPath) return rawPath;
    return '';
  }

  function requestBrowserFolderChoice(statusNode) {
    return new Promise(function(resolve) {
      var input = document.createElement('input');
      input.type = 'file';
      input.multiple = true;
      input.setAttribute('webkitdirectory', '');
      input.setAttribute('directory', '');
      input.style.position = 'fixed';
      input.style.left = '-9999px';
      input.addEventListener('change', function() {
        var selectedPath = readPathFromDirectoryFiles(input.files || []);
        if (input.parentElement) input.parentElement.removeChild(input);
        if (!selectedPath && statusNode instanceof HTMLElement) {
          statusNode.textContent = '当前浏览器没有暴露本机绝对路径，请在下方输入路径。';
        }
        resolve(selectedPath);
      }, { once: true });
      document.body.appendChild(input);
      input.click();
    });
  }

  async function requestNativeFolderChoice(statusNode) {
    var desktopPicker = window.__mnoteDesktop && typeof window.__mnoteDesktop.selectLocalFolder === 'function'
      ? window.__mnoteDesktop.selectLocalFolder
      : null;
    if (desktopPicker) {
      var selected = await desktopPicker();
      return typeof selected === 'string' ? selected : '';
    }
    if (typeof window.showDirectoryPicker === 'function') {
      var handle = await window.showDirectoryPicker({ mode: 'read' });
      var handlePath = handle && (handle.path || handle.mnotePath || handle.nativePath);
      if (typeof handlePath === 'string' && handlePath.trim()) return handlePath;
      if (statusNode instanceof HTMLElement) {
        statusNode.textContent = '已选择“' + (handle && handle.name ? handle.name : '文件夹') + '”，但浏览器没有暴露本机绝对路径，请在下方确认路径。';
      }
      return '';
    }
    return requestBrowserFolderChoice(statusNode);
  }

  function openLocalFolderDialog(initialMessage) {
    closeLocalFolderDialog();
    var recent = readRecentLocalRoots();
    var dialog = document.createElement('div');
    dialog.className = 'mnote-local-folder-dialog';
    dialog.setAttribute('data-testid', 'mnote-local-folder-dialog');
    dialog.setAttribute('role', 'dialog');
    dialog.setAttribute('aria-modal', 'true');
    dialog.style.position = 'fixed';
    dialog.style.inset = '0';
    dialog.style.zIndex = '2147483646';
    dialog.style.background = 'rgba(15, 23, 42, 0.28)';
    dialog.style.display = 'flex';
    dialog.style.alignItems = 'center';
    dialog.style.justifyContent = 'center';
    var card = document.createElement('div');
    card.className = 'mnote-local-folder-dialog__card';
    card.style.width = 'min(640px, calc(100vw - 32px))';
    card.style.background = '#fff';
    card.style.border = '1px solid rgba(27, 28, 28, 0.12)';
    card.style.borderRadius = '8px';
    card.style.padding = '16px';
    card.style.boxShadow = '0 18px 48px rgba(15, 23, 42, 0.22)';
    card.style.display = 'grid';
    card.style.gap = '12px';
    var title = document.createElement('h2');
    title.textContent = '打开本地文件夹';
    title.style.margin = '0';
    title.style.fontSize = '18px';
    title.style.lineHeight = '1.4';
    var status = document.createElement('p');
    status.className = 'mnote-local-folder-dialog__status';
    status.setAttribute('data-testid', 'mnote-local-folder-status');
    status.textContent = initialMessage || '选择一个本机文件夹，或输入绝对路径。';
    status.style.margin = '0';
    status.style.color = '#4b5563';
    status.style.fontSize = '13px';
    var input = document.createElement('input');
    input.type = 'text';
    input.autocomplete = 'off';
    input.spellcheck = false;
    input.placeholder = '/mnt/Data1T/mnote/design/04-tree-domain/done';
    input.setAttribute('data-testid', 'mnote-local-folder-path-input');
    input.value = recent.length ? fileRootUriToPathInput(recent[0]) : '';
    input.style.width = '100%';
    input.style.boxSizing = 'border-box';
    input.style.border = '1px solid rgba(27, 28, 28, 0.18)';
    input.style.borderRadius = '6px';
    input.style.padding = '10px 12px';
    input.style.fontSize = '14px';
    var actions = document.createElement('div');
    actions.className = 'mnote-local-folder-dialog__actions';
    actions.style.display = 'flex';
    actions.style.gap = '8px';
    actions.style.justifyContent = 'flex-end';
    var choose = document.createElement('button');
    choose.type = 'button';
    choose.textContent = '选择文件夹';
    choose.setAttribute('data-testid', 'mnote-local-folder-native-picker');
    var cancel = document.createElement('button');
    cancel.type = 'button';
    cancel.textContent = '取消';
    var confirm = document.createElement('button');
    confirm.type = 'button';
    confirm.textContent = '打开';
    confirm.setAttribute('data-testid', 'mnote-local-folder-open-confirm');
    [choose, cancel, confirm].forEach(function(button) {
      button.style.border = '1px solid rgba(27, 28, 28, 0.14)';
      button.style.borderRadius = '6px';
      button.style.padding = '8px 12px';
      button.style.background = '#fff';
      button.style.cursor = 'pointer';
      button.style.fontSize = '14px';
    });
    function submit() {
      var rootUri = pathToFileRootUri(input.value);
      if (!rootUri) {
        status.textContent = '请输入以 / 开头的绝对路径，或 file:// URI。';
        input.focus();
        return;
      }
      closeLocalFolderDialog();
      openLocalFolderRoot(rootUri);
    }
    choose.addEventListener('click', function(event) {
      event.preventDefault();
      requestNativeFolderChoice(status).then(function(selectedPath) {
        if (selectedPath) {
          input.value = selectedPath;
          submit();
        }
      }).catch(function(error) {
        status.textContent = error && error.name === 'AbortError'
          ? '已取消选择。'
          : '无法打开系统文件夹选择器，请输入路径。';
      });
    });
    cancel.addEventListener('click', function(event) {
      event.preventDefault();
      closeLocalFolderDialog();
    });
    confirm.addEventListener('click', function(event) {
      event.preventDefault();
      submit();
    });
    input.addEventListener('keydown', function(event) {
      if (event.key === 'Enter') {
        event.preventDefault();
        submit();
      }
    });
    actions.appendChild(choose);
    actions.appendChild(cancel);
    actions.appendChild(confirm);
    card.appendChild(title);
    card.appendChild(status);
    card.appendChild(input);
    if (recent.length) {
      var recentTitle = document.createElement('div');
      recentTitle.style.fontSize = '12px';
      recentTitle.style.color = '#6b7280';
      recentTitle.textContent = '最近使用';
      card.appendChild(recentTitle);
      var recentList = document.createElement('div');
      recentList.className = 'mnote-local-folder-dialog__recent';
      recent.slice(0, 5).forEach(function(rootUri) {
        var button = document.createElement('button');
        button.type = 'button';
        button.textContent = rootUri.replace(/^file:\/\//, '');
        button.addEventListener('click', function(event) {
          event.preventDefault();
          closeLocalFolderDialog();
          openLocalFolderRoot(rootUri);
        });
        recentList.appendChild(button);
      });
      card.appendChild(recentList);
    }
    card.appendChild(actions);
    dialog.appendChild(card);
    dialog.addEventListener('click', function(event) {
      if (event.target === dialog) closeLocalFolderDialog();
    });
    document.body.appendChild(dialog);
    input.focus();
    input.select();
  }

  function requestOpenLocalFolder() {
    openLocalFolderDialog('');
  }

  function closeWorkspaceSourceMenu() {
    var existing = document.querySelector('[data-testid="mnote-workspace-source-menu"]');
    if (existing && existing.parentElement) existing.parentElement.removeChild(existing);
    document.querySelectorAll('[data-testid="mnote-workspace-source-trigger"]').forEach(function(trigger) {
      if (trigger instanceof HTMLElement) trigger.setAttribute('aria-expanded', 'false');
    });
  }

  function workspaceSourceLabel(rootUri) {
    var label = String(rootUri || '').replace(/^file:\/\//, '');
    try {
      label = decodeURIComponent(label);
    } catch (_) {}
    return label || '本地文件夹';
  }

  function openWorkspaceSourceMenu(trigger) {
    closeWorkspaceSourceMenu();
    if (!(trigger instanceof HTMLElement)) return;
    trigger.setAttribute('aria-expanded', 'true');
    var menu = document.createElement('div');
    menu.className = 'mnote-workspace-source-menu';
    menu.setAttribute('data-testid', 'mnote-workspace-source-menu');
    menu.setAttribute('role', 'menu');
    var cloudButton = document.createElement('button');
    cloudButton.type = 'button';
    cloudButton.className = 'mnote-workspace-source-menu__item';
    cloudButton.setAttribute('data-testid', 'mnote-switch-cloud-workspace');
    cloudButton.setAttribute('role', 'menuitem');
    cloudButton.textContent = '云空间';
    cloudButton.addEventListener('click', function(event) {
      event.preventDefault();
      closeWorkspaceSourceMenu();
      switchToCloudWorkspace();
    });
    menu.appendChild(cloudButton);
    var recent = readRecentLocalRoots();
    if (recent.length) {
      var label = document.createElement('div');
      label.className = 'mnote-workspace-source-menu__label';
      label.textContent = '最近本地文件夹';
      menu.appendChild(label);
      recent.slice(0, 8).forEach(function(rootUri) {
        var button = document.createElement('button');
        button.type = 'button';
        button.className = 'mnote-workspace-source-menu__item';
        button.setAttribute('data-testid', 'mnote-recent-local-root');
        button.setAttribute('data-root-uri', rootUri);
        button.setAttribute('role', 'menuitem');
        button.textContent = workspaceSourceLabel(rootUri);
        button.addEventListener('click', function(event) {
          event.preventDefault();
          closeWorkspaceSourceMenu();
          openLocalFolderRoot(rootUri);
        });
        menu.appendChild(button);
      });
    }
    var openOther = document.createElement('button');
    openOther.type = 'button';
    openOther.className = 'mnote-workspace-source-menu__item mnote-workspace-source-menu__item--primary';
    openOther.setAttribute('data-testid', 'mnote-open-other-local-folder');
    openOther.setAttribute('role', 'menuitem');
    openOther.textContent = '打开其他本地文件夹';
    openOther.addEventListener('click', function(event) {
      event.preventDefault();
      closeWorkspaceSourceMenu();
      requestOpenLocalFolder();
    });
    menu.appendChild(openOther);
    trigger.closest('[data-testid="wolai-workspace-identity"]')?.appendChild(menu);
  }

  function setCommandPending(trigger, pending) {
    if (!(trigger instanceof HTMLElement)) return;
    trigger.setAttribute('data-pending', pending ? 'true' : 'false');
    if ('disabled' in trigger) {
      if (pending) trigger.setAttribute('disabled', 'disabled');
      else trigger.removeAttribute('disabled');
    }
  }

  async function dispatchTreeCommand(trigger, body) {
    setCommandPending(trigger, true);
    var commandBody = Object.assign({}, currentWorkspaceSourcePayload(), body || {});
    try {
      var response = await fetch('/api/tree/commands', {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify(commandBody)
      });
      var payload = await response.json().catch(function(){ return null; });
      if (!response.ok || !payload || !payload.result) {
        throw new Error((payload && payload.message) || 'tree_command_failed_' + response.status);
      }
      window.dispatchEvent(new CustomEvent('tree:local-command', { detail: { body: commandBody, result: payload.result } }));
      setCommandPending(trigger, false);
      return payload.result;
    } catch (error) {
      setCommandPending(trigger, false);
      if (trigger instanceof HTMLElement) {
        trigger.setAttribute('data-error', error instanceof Error ? error.message : String(error));
      }
      throw error;
    }
  }

  function navigateToDocument(nodeId, workspaceId, options) {
    if (!nodeId) return;
    var treeView = normalizeSidebarTreeMode(options && options.treeView ? options.treeView : activeSidebarTreeMode());
    persistSidebarTreeMode(treeView);
    if (currentDocumentId() === nodeId && window.location.pathname.indexOf('/documents/') === 0) {
      document.querySelectorAll('.tree-row[data-active="true"], .tree-row[data-selected="true"]').forEach(function(row) {
        if (row instanceof HTMLElement) {
          row.setAttribute('data-active', 'false');
          row.setAttribute('data-selected', 'false');
        }
      });
      document.querySelectorAll('.tree-row[data-node-id="' + cssEscape(nodeId) + '"], .tree-row[data-document-id="' + cssEscape(nodeId) + '"], .tree-row[data-doc-id="' + cssEscape(nodeId) + '"]').forEach(function(row) {
        if (row instanceof HTMLElement) {
          if (row.getAttribute('data-shell-mode') === 'filetree') {
            row.setAttribute('data-selected', String(row.getAttribute('data-row-id') === 'index:' + nodeId));
          } else {
            row.setAttribute('data-active', 'true');
          }
        }
      });
      return;
    }
    var targetUrl = new URL('/documents/' + encodeURIComponent(nodeId), window.location.origin);
    if (workspaceId) targetUrl.searchParams.set('workspaceId', workspaceId);
    if (treeView === 'filetree') targetUrl.searchParams.set('treeView', 'filetree');
    copyWorkspaceSourceParams(targetUrl);
    var url = targetUrl.pathname + targetUrl.search;
    if (mnoteNavigationInFlight === url) return;
    if (typeof window.__mnoteDocumentPaneRuntime?.openPrimaryDocument === 'function') {
      mnoteNavigationInFlight = url;
      document.documentElement.setAttribute('data-mnote-navigation-pending', 'true');
      document.documentElement.setAttribute('data-mnote-navigation-target', nodeId);
      window.__mnoteDocumentPaneRuntime.openPrimaryDocument({
        documentId: nodeId,
        workspaceId: workspaceId || '',
        sourceKind: targetUrl.searchParams.get('sourceKind') || 'convex_workspace',
        rootUri: targetUrl.searchParams.get('rootUri') || '',
        url: targetUrl,
      }).then(function(){
        if (mnoteNavigationInFlight === url) mnoteNavigationInFlight = '';
        document.documentElement.removeAttribute('data-mnote-navigation-pending');
      }).catch(function(error){
        console.warn('mnote pane 内导航失败，将回退整页导航', error);
        if (window.__mnoteTreeLiveEventSource && typeof window.__mnoteTreeLiveEventSource.close === 'function') {
          window.__mnoteTreeLiveEventSource.close();
        }
        window.location.assign(url);
      });
      return;
    }
    mnoteNavigationInFlight = url;
    document.documentElement.setAttribute('data-mnote-navigation-pending', 'true');
    document.documentElement.setAttribute('data-mnote-navigation-target', nodeId);
    if (window.__mnoteTreeLiveEventSource && typeof window.__mnoteTreeLiveEventSource.close === 'function') {
      window.__mnoteTreeLiveEventSource.close();
    }
    window.location.assign(url);
  }

  async function createPage(trigger, parentId) {
    var workspaceId = resolveWorkspaceId(trigger);
    var effectiveParentId = (parentId || trigger.getAttribute('data-parent-id') || '').trim();
    if (!workspaceId) return;
    var result = await dispatchTreeCommand(trigger, {
      action: 'create',
      workspaceId: workspaceId,
      parentId: effectiveParentId || null,
      title: '新页面'
    });
    var nextWorkspaceId = result.workspaceId || workspaceId;
    navigateToDocument(result.documentId, nextWorkspaceId, { treeView: activeSidebarTreeMode() });
  }

  function applySidebarTreeTab(mode, shell) {
    mode = persistSidebarTreeMode(mode);
    shell = shell || document.querySelector('[data-testid="wolai-sidebar-page-tree-shell"]');
    if (!shell) return;
    var tabs = shell.querySelectorAll('[data-mnote-sidebar-tree-tab]');
    for (var i = 0; i < tabs.length; i++) {
      var isActive = tabs[i].getAttribute('data-mnote-sidebar-tree-tab') === mode;
      tabs[i].setAttribute('aria-selected', isActive ? 'true' : 'false');
      tabs[i].classList.toggle('wolai-sidebar-tab-active', isActive);
      tabs[i].classList.toggle('wolai-sidebar-tab-muted', !isActive);
    }
    var panels = shell.querySelectorAll('[data-mnote-sidebar-tree-panel]');
    for (var j = 0; j < panels.length; j++) {
      var isCurrentPanel = panels[j].getAttribute('data-mnote-sidebar-tree-panel') === mode;
      panels[j].hidden = !isCurrentPanel;
    }
  }

  function switchSidebarTreeTab(trigger) {
    var mode = trigger ? trigger.getAttribute('data-mnote-sidebar-tree-tab') : 'page';
    applySidebarTreeTab(mode, trigger ? trigger.closest('[data-testid="wolai-sidebar-page-tree-shell"]') : null);
  }

  function restoreSidebarTreeTab() {
    applySidebarTreeTab(readStoredSidebarTreeMode(), null);
  }

  function updateTitleEverywhere(documentId, title) {
    if (!documentId) return;
    var escaped = cssEscape(documentId);
    var escapedDocRowId = cssEscape('doc:' + documentId);
    var selectors = [
      '.tree-row[data-shell-mode="page"][data-node-id="' + escaped + '"] > .tree-link > .tree-link-title',
      '.tree-row[data-shell-mode="filetree"][data-row-id="' + escapedDocRowId + '"] > .tree-link > .tree-link-title',
      '.wolai-page-row[data-node-id="' + escaped + '"] > .wolai-row-title',
      'a[href="/documents/' + escaped + '"] > .wolai-row-title',
      'a[href^="/documents/' + escaped + '?"] > .wolai-row-title'
    ];
    selectors.forEach(function(selector) {
      document.querySelectorAll(selector).forEach(function(node) {
        if (node instanceof HTMLElement) node.textContent = title;
      });
    });
  }

  function documentIdFromDelta(data) {
    return String(data && (data.documentId || data.id || (data.document && data.document.id) || (data.node && data.node.id) || (data.args && data.args.documentId)) || '').trim();
  }

  function parentIdFromDelta(data) {
    if (!data || typeof data !== 'object') return null;
    if (Object.prototype.hasOwnProperty.call(data, 'parentId')) return data.parentId ? String(data.parentId).trim() : null;
    if (Object.prototype.hasOwnProperty.call(data, 'parent_id')) return data.parent_id ? String(data.parent_id).trim() : null;
    if (data.args && Object.prototype.hasOwnProperty.call(data.args, 'parentId')) return data.args.parentId ? String(data.args.parentId).trim() : null;
    return null;
  }

  function treeRootForMode(mode) {
    var id = mode === 'filetree' ? 'sidebar-file-tree-root' : 'sidebar-tree-root';
    return document.querySelector('#' + id + ' .tree-root');
  }

  function rowSelectorForDocument(mode, documentId) {
    var escaped = cssEscape(documentId);
    if (mode === 'filetree') {
      return '.tree-row[data-shell-mode="filetree"][data-doc-id="' + escaped + '"], .tree-row[data-shell-mode="filetree"][data-document-id="' + escaped + '"]';
    }
    return '.tree-row[data-shell-mode="page"][data-node-id="' + escaped + '"]';
  }

  function ensureTreeChildren(parentRow) {
    var parentNode = parentRow ? parentRow.closest('.tree-node') : null;
    if (!parentNode) return null;
    var children = parentNode.querySelector(':scope > .tree-children');
    if (!children) {
      children = document.createElement('ul');
      children.className = 'tree-children';
      parentNode.appendChild(children);
    }
    children.classList.remove('tree-children--collapsed');
    parentRow.setAttribute('aria-expanded', 'true');
    var toggle = parentRow.querySelector('[data-rust-action="toggle"]');
    if (toggle) toggle.setAttribute('aria-expanded', 'true');
    return children;
  }

  function removeDocumentRowForMode(mode, documentId) {
    var row = document.querySelector(rowSelectorForDocument(mode, documentId));
    var node = row ? row.closest('.tree-node') : null;
    if (!node || !node.parentElement) return false;
    node.parentElement.removeChild(node);
    return true;
  }

  function commandDocumentId(result, fallback) {
    var value = result && (
      result.documentId ||
      result.id ||
      result.nodeId ||
      (result.document && (result.document.id || result.document.documentId)) ||
      (result.node && (result.node.id || result.node.documentId)) ||
      (result.payload && result.payload.documentId)
    );
    return String(value || fallback || '').trim();
  }

  function commandDocumentTitle(result, fallback) {
    var value = result && (
      result.title ||
      (result.document && result.document.title) ||
      (result.node && result.node.title) ||
      (result.payload && result.payload.title)
    );
    return String(value || fallback || '新页面').trim() || '新页面';
  }

  function normalizeDocumentRowId(value) {
    return String(value || '').trim().replace(/^doc:/, '').replace(/^index:/, '');
  }

  function appendRenderedTreeNode(container, html) {
    if (!container || !html) return false;
    var empty = container.querySelector(':scope > .tree-empty');
    if (empty && empty.parentElement) empty.parentElement.removeChild(empty);
    var template = document.createElement('template');
    template.innerHTML = html;
    var node = template.content.firstElementChild;
    if (!node) return false;
    container.appendChild(node);
    return true;
  }

  function localPageInsertDepth(parentId) {
    if (!parentId) return 0;
    var parentRow = document.querySelector(rowSelectorForDocument('page', parentId));
    var depth = parentRow ? Number(parentRow.getAttribute('data-depth') || 0) : 0;
    return Number.isFinite(depth) ? depth + 1 : 1;
  }

  function upsertPageDocumentRow(documentId, parentId, title) {
    if (!documentId) return false;
    var existing = document.querySelector(rowSelectorForDocument('page', documentId));
    if (existing instanceof HTMLElement) {
      updateTitleEverywhere(documentId, title);
      moveDocumentRowForMode('page', documentId, parentId || null);
      return true;
    }
    var root = treeRootForMode('page');
    if (!root) return false;
    var targetContainer = root;
    if (parentId) {
      var parentRow = document.querySelector(rowSelectorForDocument('page', parentId));
      targetContainer = ensureTreeChildren(parentRow);
      if (!targetContainer) targetContainer = root;
    }
    var grouped = new Map();
    grouped.set(parentId || '', [{
      id: documentId,
      nodeId: documentId,
      documentId: documentId,
      parentNodeId: parentId || '',
      rowKind: 'document',
      title: title,
      expandable: false,
      childCount: 0
    }]);
    return appendRenderedTreeNode(targetContainer, renderPageRows(parentId || '', grouped, currentDocumentId(), localPageInsertDepth(parentId)));
  }

  function fileTreeParentContainer(parentId) {
    var root = treeRootForMode('filetree');
    if (!root) return null;
    if (!parentId) return root;
    var parentRow = document.querySelector('.tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape('doc:' + parentId) + '"]');
    var children = ensureTreeChildren(parentRow);
    return children || root;
  }

  function localFileInsertDepth(parentId) {
    if (!parentId) return 0;
    var parentRow = document.querySelector('.tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape('doc:' + parentId) + '"]');
    var level = parentRow ? Number(parentRow.getAttribute('aria-level') || 1) : 1;
    return Number.isFinite(level) ? level : 1;
  }

  function upsertFileDocumentRows(documentId, parentId, title) {
    if (!documentId) return false;
    var existing = document.querySelector('.tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape('doc:' + documentId) + '"]');
    if (existing instanceof HTMLElement) {
      updateTitleEverywhere(documentId, title);
      moveDocumentRowForMode('filetree', documentId, parentId || null);
      return true;
    }
    var targetContainer = fileTreeParentContainer(parentId);
    if (!targetContainer) return false;
    var parentNodeId = parentId ? 'doc:' + parentId : '';
    var docNodeId = 'doc:' + documentId;
    var indexNodeId = 'index:' + documentId;
    var depth = localFileInsertDepth(parentId);
    var grouped = new Map();
    grouped.set(parentNodeId, [{
      id: docNodeId,
      nodeId: docNodeId,
      rowId: docNodeId,
      rowKind: 'document',
      title: title,
      documentId: documentId,
      parentNodeId: parentNodeId,
      depth: depth,
      expandable: true,
      childCount: 1
    }]);
    grouped.set(docNodeId, [{
      id: indexNodeId,
      nodeId: indexNodeId,
      rowId: indexNodeId,
      rowKind: 'index',
      title: 'index.md',
      documentId: documentId,
      parentNodeId: docNodeId,
      depth: depth + 1,
      expandable: false,
      childCount: 0
    }]);
    return appendRenderedTreeNode(targetContainer, renderFileRows(parentNodeId, grouped, currentDocumentId()));
  }

  function applyCreatedDocumentLocally(result, parentId, fallbackTitle) {
    var documentId = commandDocumentId(result, null);
    if (!documentId) return false;
    var normalizedParentId = normalizeDocumentRowId(parentId);
    var title = commandDocumentTitle(result, fallbackTitle);
    var pageChanged = upsertPageDocumentRow(documentId, normalizedParentId, title);
    var fileChanged = upsertFileDocumentRows(documentId, normalizedParentId, title);
    if (pageChanged || fileChanged) {
      document.documentElement.setAttribute('data-mnote-tree-local-command-applied', 'create');
    }
    return pageChanged || fileChanged;
  }

  function moveDocumentRowForMode(mode, documentId, parentId) {
    var row = document.querySelector(rowSelectorForDocument(mode, documentId));
    var node = row ? row.closest('.tree-node') : null;
    var root = treeRootForMode(mode);
    if (!row || !node || !root) return false;
    var targetContainer = root;
    if (parentId) {
      var parentRow = document.querySelector(rowSelectorForDocument(mode, parentId));
      targetContainer = ensureTreeChildren(parentRow);
      if (!targetContainer) return false;
      row.setAttribute('data-parent-id', parentId);
    } else {
      row.removeAttribute('data-parent-id');
    }
    targetContainer.appendChild(node);
    return true;
  }

  function applyMoveDocumentDelta(data) {
    var documentId = documentIdFromDelta(data);
    if (!documentId) return false;
    var parentId = parentIdFromDelta(data);
    var movedPage = moveDocumentRowForMode('page', documentId, parentId);
    var movedFile = moveDocumentRowForMode('filetree', documentId, parentId);
    return movedPage || movedFile;
  }

  function applyRemoveDocumentDelta(data) {
    var documentId = documentIdFromDelta(data);
    if (!documentId) return false;
    var removedPage = removeDocumentRowForMode('page', documentId);
    var removedFile = removeDocumentRowForMode('filetree', documentId);
    return removedPage || removedFile;
  }

  function readProjection(value) {
    if (!value || typeof value !== 'object') return null;
    if (value.result && typeof value.result === 'object') return value.result;
    if (value.snapshot && value.snapshot.tree) return value.snapshot.tree;
    if (value.data && value.data.tree) return value.data.tree;
    if (value.tree && typeof value.tree === 'object') return value.tree;
    return value;
  }

  function readSidebarDataset(value) {
    if (!value || typeof value !== 'object') return null;
    if (value.snapshot && value.snapshot.dataset && typeof value.snapshot.dataset === 'object') return value.snapshot.dataset;
    if (value.data && value.data.dataset && typeof value.data.dataset === 'object') return value.data.dataset;
    if (value.dataset && typeof value.dataset === 'object') return value.dataset;
    if (value.sidebar && typeof value.sidebar === 'object') return value.sidebar;
    return null;
  }

  function readDatasetProjection(value, snakeCaseKey, camelCaseKey) {
    var dataset = readSidebarDataset(value);
    if (!dataset || typeof dataset !== 'object') return null;
    if (dataset[snakeCaseKey] && typeof dataset[snakeCaseKey] === 'object') return dataset[snakeCaseKey];
    if (dataset[camelCaseKey] && typeof dataset[camelCaseKey] === 'object') return dataset[camelCaseKey];
    return null;
  }

  function setTreeLiveApplyError(reason) {
    document.documentElement.setAttribute('data-mnote-tree-live-apply-error', String(reason || 'tree_live_apply_failed'));
  }

  function projectionItems(projection) {
    var resolved = readProjection(projection);
    return resolved && Array.isArray(resolved.items) ? resolved.items : [];
  }

  function hasProjectionItems(projection) {
    var resolved = readProjection(projection);
    return Boolean(resolved && Array.isArray(resolved.items));
  }

  function nodeIdOf(item) {
    return String(item && (item.nodeId || item.id || item.documentId) || '').trim();
  }

  function rowIdOf(item) {
    return String(item && (item.rowId || item.nodeId || item.id) || '').trim();
  }

  function parentIdOf(item) {
    return String(item && (item.parentNodeId || item.parentId || '') || '').trim();
  }

  function titleOf(item) {
    return String(item && item.title || '无标题').trim() || '无标题';
  }

  function groupRowsByParent(rows) {
    var ids = new Set(rows.map(nodeIdOf).filter(Boolean));
    var grouped = new Map();
    rows.forEach(function(item) {
      var parentId = parentIdOf(item);
      if (!ids.has(parentId)) parentId = '';
      if (!grouped.has(parentId)) grouped.set(parentId, []);
      grouped.get(parentId).push(item);
    });
    return grouped;
  }

  function pageTreeChevronSvg() {
    return '<svg class="tree-toggle-icon" viewBox="0 0 20 20" width="20" height="20" aria-hidden="true" focusable="false"><path d="M7.84 14.955c.206 0 .37-.07.505-.21l4.277-4.179a.79.79 0 0 0 .264-.574.78.78 0 0 0-.258-.574L8.35 5.24a.7.7 0 0 0-.51-.21.721.721 0 0 0-.498 1.247l3.814 3.721-3.814 3.709a.721.721 0 0 0 .498 1.248"></path></svg>';
  }

  function renderPageRows(parentId, grouped, activeId, inheritedDepth) {
    var computedDepth = Number(inheritedDepth || 0);
    return (grouped.get(parentId) || []).map(function(item) {
      var nodeId = nodeIdOf(item);
      var title = titleOf(item);
      var depth = computedDepth;
      var parent = parentIdOf(item);
      var children = grouped.get(nodeId) || [];
      var expandable = Boolean(item.expandable || item.childCount > 0 || children.length);
      var expanded = expandable && item.expandedByDefault !== false;
      var meta = item && item.resourceMeta && typeof item.resourceMeta === 'object' ? item.resourceMeta : {};
      var openable = Boolean(meta.documentId || item.documentId || !String(nodeId).startsWith('local-dir:'));
      var toggle = expandable
        ? '<button type="button" class="tree-toggle" data-testid="tree-node-toggle" data-rust-action="toggle" data-node-id="' + escapeHtml(nodeId) + '" aria-label="' + (expanded ? '折叠 ' : '展开 ') + escapeHtml(title) + '" aria-expanded="' + String(expanded) + '">' + pageTreeChevronSvg() + '</button>'
        : '<span class="tree-spacer" aria-hidden="true"></span>';
      var childHtml = expandable
        ? '<ul class="tree-children' + (expanded ? '' : ' tree-children--collapsed') + '">' + renderPageRows(nodeId, grouped, activeId, depth + 1) + '</ul>'
        : '';
      var currentAttr = nodeId === activeId ? ' aria-current="page" aria-selected="true"' : ' aria-selected="false"';
      return '<li class="tree-node" data-node-id="' + escapeHtml(nodeId) + '"><div class="tree-row" role="treeitem" aria-level="' + (depth + 1) + '" aria-expanded="' + String(expandable && expanded) + '" data-rust-rendered-row="page" data-testid="wolai-sidebar-row" data-tree-testid="tree-node-open" data-node-id="' + escapeHtml(nodeId) + '"' + (parent ? ' data-parent-id="' + escapeHtml(parent) + '"' : '') + ' data-depth="' + depth + '" data-shell-mode="page" data-active="' + String(nodeId === activeId) + '"' + currentAttr + ' data-focused="false" data-page-openable="' + String(openable) + '" data-draggable="true" draggable="true" tabindex="-1">' + toggle + '<button type="button" class="tree-link" data-testid="tree-node-open" data-rust-action="open" data-node-id="' + escapeHtml(nodeId) + '" data-page-openable="' + String(openable) + '"><span class="tree-link-title">' + escapeHtml(title) + '</span></button><div class="tree-actions"><button type="button" class="tree-action" data-testid="tree-action-menu" data-rust-action="menu" data-node-id="' + escapeHtml(nodeId) + '" aria-label="更多操作">…</button><button type="button" class="tree-action" data-testid="tree-action-create" data-rust-action="create" data-node-id="' + escapeHtml(nodeId) + '" aria-label="新建子页面">+</button></div></div>' + childHtml + '</li>';
    }).join('');
  }

  function renderPageProjection(projection) {
    var tree = document.getElementById('sidebar-tree-root');
    if (!tree) return false;
    var rows = projectionItems(projection).filter(function(item) {
      return String(item.rowKind || 'document') === 'document';
    });
    var activeId = currentDocumentId();
    tree.innerHTML = '<ul class="tree-root" role="tree" data-rust-page-renderer="initial_v1">' + (rows.length ? renderPageRows('', groupRowsByParent(rows), activeId, 0) : '<li class="tree-empty" data-rust-rendered-row="page-empty">暂无页面</li>') + '</ul>';
    return true;
  }

  function fileDocumentId(item) {
    var meta = item && item.resourceMeta && typeof item.resourceMeta === 'object' ? item.resourceMeta : {};
    if (meta.documentId) return String(meta.documentId).trim();
    if (item && item.documentId) return String(item.documentId).trim();
    if (item && item.rowKind === 'document') return nodeIdOf(item);
    if (item && item.rowKind === 'index') return nodeIdOf(item).replace(/^index:/, '');
    return '';
  }

  function fileAssetId(item) {
    var meta = item && item.resourceMeta && typeof item.resourceMeta === 'object' ? item.resourceMeta : {};
    if (meta.assetId) return String(meta.assetId).trim();
    if (item && item.assetId) return String(item.assetId).trim();
    if (item && item.rowKind === 'asset') return nodeIdOf(item).replace(/^asset:/, '');
    return '';
  }

  function fileObjectIdentity(item) {
    var meta = item && item.resourceMeta && typeof item.resourceMeta === 'object' ? item.resourceMeta : {};
    if (meta.objectIdentity && typeof meta.objectIdentity === 'object') return meta.objectIdentity;
    var rowKind = String(item && item.rowKind || '');
    var documentId = fileDocumentId(item) || null;
    var assetId = fileAssetId(item) || null;
    var iconKind = iconKindOf(item);
    var objectKind = rowKind === 'document' ? 'page' : rowKind === 'index' ? 'index' : iconKind === 'mindmap' ? 'mindmap' : 'attachment';
    return { objectKind: objectKind, documentId: documentId, blockId: null, assetId: assetId };
  }

  function objectIdentityAttr(identity) {
    try {
      return JSON.stringify(identity || {});
    } catch (_error) {
      return '';
    }
  }

  function iconKindOf(item) {
    return String(item && (item.iconHint || item.rowKind || 'file') || 'file').trim() || 'file';
  }

  function renderFileRows(parentId, grouped, activeId) {
    return (grouped.get(parentId) || []).map(function(item) {
      var nodeId = nodeIdOf(item);
      var rowId = rowIdOf(item);
      var rowKind = String(item.rowKind || 'document');
      var title = titleOf(item);
      var depth = Number(item.depth || 0);
      var parent = parentIdOf(item);
      var documentId = fileDocumentId(item);
      var assetId = fileAssetId(item);
      var objectIdentity = fileObjectIdentity(item);
      var children = grouped.get(nodeId) || [];
      var expandable = Boolean(item.expandable || item.childCount > 0 || children.length);
      var expanded = expandable && item.expandedByDefault !== false;
      var selected = rowId === 'index:' + activeId;
      var testId = rowKind === 'document' ? 'filetree-doc-row' : rowKind === 'index' ? 'filetree-index-row' : 'filetree-asset-row';
      var toggle = expandable
        ? '<button type="button" class="tree-toggle" data-testid="filetree-toggle" data-rust-action="toggle" data-row-id="' + escapeHtml(rowId) + '" aria-label="' + (expanded ? '折叠 ' : '展开 ') + escapeHtml(title) + '">' + (expanded ? '▾' : '▸') + '</button>'
        : '<span class="tree-spacer" aria-hidden="true"></span>';
      var createAction = rowKind === 'document'
        ? '<button type="button" class="tree-action" data-testid="filetree-create" data-rust-action="create" data-row-id="' + escapeHtml(rowId) + '" data-node-id="' + escapeHtml(documentId || nodeId) + '" aria-label="新建子页面">+</button>'
        : '';
      var childHtml = expandable
        ? '<ul class="tree-children' + (expanded ? '' : ' tree-children--collapsed') + '">' + renderFileRows(nodeId, grouped, activeId) + '</ul>'
        : '';
      return '<li class="tree-node" data-node-id="' + escapeHtml(nodeId) + '"><div class="tree-row" role="treeitem" aria-level="' + (depth + 1) + '" aria-expanded="' + String(expandable && expanded) + '" data-rust-rendered-row="filetree" data-testid="' + testId + '" data-row-id="' + escapeHtml(rowId) + '" data-row-kind="' + escapeHtml(rowKind) + '" data-node-id="' + escapeHtml(nodeId) + '"' + (parent ? ' data-parent-id="' + escapeHtml(parent) + '"' : '') + ' data-document-id="' + escapeHtml(documentId) + '" data-doc-id="' + escapeHtml(documentId) + '" data-asset-id="' + escapeHtml(assetId) + '" data-object-identity="' + escapeHtml(objectIdentityAttr(objectIdentity)) + '" data-object-kind="' + escapeHtml(String(objectIdentity.objectKind || '')) + '" data-shell-mode="filetree" data-selected="' + String(selected) + '" data-active="false" draggable="true">' + toggle + '<span class="tree-kind-badge" data-kind="' + escapeHtml(iconKindOf(item)) + '" aria-hidden="true"></span><button type="button" class="tree-link" data-rust-action="open" data-row-id="' + escapeHtml(rowId) + '" data-document-id="' + escapeHtml(documentId) + '" data-asset-id="' + escapeHtml(assetId) + '"><span class="tree-link-title">' + escapeHtml(title) + '</span></button><div class="tree-actions">' + createAction + '<button type="button" class="tree-action" data-testid="filetree-action-menu" data-rust-action="menu" data-row-id="' + escapeHtml(rowId) + '" aria-label="更多操作">…</button></div></div>' + childHtml + '</li>';
    }).join('');
  }

  function renderFileProjection(projection) {
    var tree = document.getElementById('sidebar-file-tree-root');
    if (!tree) return false;
    var rows = projectionItems(projection);
    var activeId = currentDocumentId();
    tree.innerHTML = '<ul class="tree-root" role="tree" data-rust-filetree-renderer="initial_v1">' + (rows.length ? renderFileRows('', groupRowsByParent(rows), activeId) : '<li class="tree-empty" data-rust-rendered-row="filetree-empty">暂无文件或页面</li>') + '</ul>';
    return true;
  }

  function renderSidebarSnapshot(payload) {
    var renderedPage = hasProjectionItems(payload) ? renderPageProjection(payload) : false;
    var fileProjection = readDatasetProjection(payload, 'kernel_file_tree_projection', 'kernelFileTreeProjection');
    var renderedFile = fileProjection && hasProjectionItems(fileProjection) ? renderFileProjection(fileProjection) : false;
    return renderedPage || renderedFile;
  }

  function isTitleOnlyDocumentPatch(candidate) {
    if (!candidate || typeof candidate !== 'object') return false;
    var allowedKeys = {
      id: true,
      documentId: true,
      title: true,
      updatedAt: true,
      updated_at: true
    };
    return Object.keys(candidate).every(function(key) {
      return allowedKeys[key] === true;
    });
  }

  function deltaNeedsProjectionRefresh(payload) {
    var data = payload && (payload.data || payload.delta || payload);
    var op = data && typeof data.op === 'string' ? String(data.op).trim() : '';
    if (!op || op === 'noop') return false;
    if (op === 'upsert_document') {
      return !isTitleOnlyDocumentPatch(data.node || data.document || null);
    }
    if (op === 'upsert_documents') {
      var documents = Array.isArray(data.upsertDocuments) ? data.upsertDocuments : Array.isArray(data.upsert_documents) ? data.upsert_documents : [];
      if (documents.length > 0 && documents.every(isTitleOnlyDocumentPatch)) {
        return false;
      }
    }
    return true;
  }

  function toggleChildren(row, button) {
    var li = row && row.parentElement;
    var children = li ? li.querySelector(':scope > .tree-children') : null;
    if (!children) return;
    children.classList.toggle('tree-children--collapsed');
    var collapsed = children.classList.contains('tree-children--collapsed');
    row.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
    if (button && button.getAttribute('data-testid') === 'tree-node-toggle') {
      button.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
    } else if (button) {
      button.textContent = collapsed ? '▸' : '▾';
    }
  }

  function dispatchSidebarEvent(name, detail) {
    document.documentElement.setAttribute('data-mnote-last-tree-action', name);
    window.dispatchEvent(new CustomEvent(name, { detail: detail }));
  }

  function inferOnlyOfficeFileType(fileName, mimeType) {
    var name = String(fileName || '').trim().toLowerCase();
    var mt = String(mimeType || '').trim().toLowerCase();
    var ext = name.indexOf('.') >= 0 ? name.split('.').pop() : '';
    if (['doc', 'docx', 'odt', 'rtf'].indexOf(ext) >= 0) return ext;
    if (['ppt', 'pptx', 'odp'].indexOf(ext) >= 0) return ext;
    if (['xls', 'xlsx', 'ods', 'csv'].indexOf(ext) >= 0) return ext;
    if (isNonOfficeAttachmentName(name, ext)) return '';
    if (mt.indexOf('wordprocessingml') >= 0) return 'docx';
    if (mt.indexOf('presentationml') >= 0) return 'pptx';
    if (mt.indexOf('spreadsheetml') >= 0) return 'xlsx';
    return '';
  }

  function buildOnlyOfficeOpenUrl(input) {
    var target = new URL('/onlyoffice', window.location.origin);
    target.searchParams.set('fileUrl', input.fileUrl || '');
    target.searchParams.set('fileName', input.fileName || '未命名资源');
    target.searchParams.set('fileType', input.fileType || 'docx');
    if (input.assetId) target.searchParams.set('assetId', input.assetId);
    if (input.documentId) target.searchParams.set('documentId', input.documentId);
    if (input.userId) target.searchParams.set('userId', input.userId);
    target.searchParams.set('mode', input.mode || 'edit');
    return target.toString();
  }

  function buildOnlyOfficeOpenPath(input) {
    var params = new URLSearchParams();
    params.set('fileUrl', input.fileUrl || '');
    params.set('fileName', input.fileName || '未命名资源');
    params.set('fileType', input.fileType || 'docx');
    if (input.assetId) params.set('assetId', input.assetId);
    if (input.documentId) params.set('documentId', input.documentId);
    if (input.userId) params.set('userId', input.userId);
    params.set('mode', input.mode || 'edit');
    return '/onlyoffice?' + params.toString();
  }

  function buildMindmapOpenPath(documentId, assetId) {
    var doc = String(documentId || '').trim();
    var map = String(assetId || '').trim();
    if (!doc || !map) return '';
    return '/mindmap/' + encodeURIComponent(doc) + '/' + encodeURIComponent(map);
  }

  function isMindmapAssetDetail(detail) {
    var assetId = String(detail && detail.assetId || '').trim();
    var assetType = String(detail && detail.assetType || '').trim();
    if (assetType === 'mindmap') return true;
    return assetId.indexOf('mindmap_') === 0 || assetId.indexOf('mindmap-') === 0;
  }

  function readFileTreeObjectIdentity(row) {
    if (!row) return null;
    var raw = row.getAttribute('data-object-identity') || '';
    if (!raw) return null;
    try {
      var parsed = JSON.parse(raw);
      return parsed && typeof parsed === 'object' ? parsed : null;
    } catch (_error) {
      return null;
    }
  }

  async function fetchCurrentOnlyOfficeUserId() {
    try {
      var response = await fetch('/api/auth/whoami', {
        method: 'GET',
        credentials: 'include',
        cache: 'no-store'
      });
      var payload = await response.json().catch(function() { return null; });
      return String(payload && payload.userId || '').trim();
    } catch (_error) {
      return '';
    }
  }

  async function openConvexAssetFromFileTree(detail) {
    var assetId = String(detail && detail.assetId || '').trim();
    if (!assetId) return;
    var documentId = String(detail && detail.documentId || '').trim();
    if (isMindmapAssetDetail(detail) && documentId) {
      var mindmapPath = buildMindmapOpenPath(documentId, assetId);
      if (mindmapPath) {
        document.documentElement.setAttribute('data-mnote-last-mindmap-asset-open-mode', 'mindmap-object-shell');
        document.documentElement.setAttribute('data-mnote-last-mindmap-asset-id', assetId);
        window.location.assign(mindmapPath);
      }
      return;
    }
    try {
      var response = await fetch('/api/media/sign?assetId=' + encodeURIComponent(assetId), {
        method: 'GET',
        credentials: 'include'
      });
      var payload = await response.json().catch(function() { return null; });
      if (!response.ok) {
        throw new Error(payload && payload.error ? payload.error : '生成签名链接失败');
      }
      var asset = payload && payload.asset && typeof payload.asset === 'object' ? payload.asset : {};
      var fileUrl = String(payload && payload.signedUrl || asset.file_url || asset.signed_url || '').trim();
      if (!fileUrl) throw new Error('附件链接不可用');
      var fileName = String(asset.file_name || detail.fileName || '未命名资源').trim() || '未命名资源';
      var fileType = inferOnlyOfficeFileType(fileName, asset.mime_type);
      if (fileType) {
        var userId = await fetchCurrentOnlyOfficeUserId();
        window.open(buildOnlyOfficeOpenUrl({
          fileUrl: fileUrl,
          fileName: fileName,
          fileType: fileType,
          assetId: assetId,
          documentId: String(asset.document_id || detail.documentId || '').trim(),
          userId: userId,
          mode: 'edit'
        }), '_blank', 'noopener,noreferrer');
        return;
      }
      if (isCodeAttachmentFileName(fileName) && String(asset.document_id || detail.documentId || '').trim() === currentDocumentId()) {
        await openCodeEditorAttachment({
          href: fileUrl,
          fileUrl: fileUrl,
          fileName: fileName,
          assetId: assetId,
          documentId: String(asset.document_id || detail.documentId || '').trim(),
          fileSize: uploadedFileSize(asset)
        });
        return;
      }
      window.open(fileUrl, '_blank', 'noopener,noreferrer');
    } catch (error) {
      window.alert(error && error.message ? error.message : '打开附件失败');
    }
  }

  window.addEventListener('tree.asset.open', function(event) {
    void openConvexAssetFromFileTree(event.detail || {});
  });

  function fileTreeRowsForUploadPreflight() {
    return Array.from(document.querySelectorAll('.tree-row[data-shell-mode="filetree"]')).map(function(row) {
      return {
        rowId: row.getAttribute('data-row-id') || '',
        rowKind: row.getAttribute('data-row-kind') || '',
        documentId: row.getAttribute('data-document-id') || row.getAttribute('data-doc-id') || null,
        assetId: row.getAttribute('data-asset-id') || null,
        assetDocumentId: row.getAttribute('data-document-id') || row.getAttribute('data-doc-id') || null,
        assetType: row.querySelector('.tree-kind-badge') ? row.querySelector('.tree-kind-badge').getAttribute('data-kind') : null,
        storagePath: null
      };
    }).filter(function(row) {
      return row.rowId || row.documentId || row.assetId;
    });
  }

  function fileTreeDocumentWorkspacesForUploadPreflight(workspaceId) {
    var seen = new Set();
    return fileTreeRowsForUploadPreflight().filter(function(row) {
      if (!row.documentId || seen.has(row.documentId)) return false;
      seen.add(row.documentId);
      return true;
    }).map(function(row) {
      return { documentId: row.documentId, workspaceId: workspaceId || null };
    });
  }

  async function preflightFileTreeUploadTarget(detail) {
    var workspaceId = String(detail && detail.workspaceId || resolveWorkspaceId(document.body) || '').trim();
    var body = {
      workspaceId: workspaceId || null,
      targetDocumentId: detail && detail.documentId ? String(detail.documentId) : null,
      targetRowId: detail && detail.targetRowId ? String(detail.targetRowId) : null,
      focusedRowId: sidebarFileTreeSelection.focusedRowId || null,
      activeDocumentId: currentDocumentId() || null,
      rows: fileTreeRowsForUploadPreflight(),
      documentWorkspaces: fileTreeDocumentWorkspacesForUploadPreflight(workspaceId)
    };
    var response = await fetch('/api/tree/filetree/upload-target-preflight', {
      method: 'POST',
      credentials: 'include',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify(body)
    });
    var payload = await response.json().catch(function() { return null; });
    if (!response.ok || !payload || !payload.plan) {
      throw new Error(payload && payload.error ? payload.error : '文件树上传目标预检失败');
    }
    return payload.plan;
  }

  function fallbackFileTreeUploadTarget(detail) {
    var workspaceId = String(detail && detail.workspaceId || resolveWorkspaceId(document.body) || '').trim();
    var documentId = String(detail && detail.documentId || currentDocumentId() || '').trim();
    if (!workspaceId || !documentId) {
      throw new Error('请选择一个目标页面后再拖入文件');
    }
    return {
      workspaceId: workspaceId,
      targetDocumentId: documentId,
      targetMindmapId: null,
      targetSubPath: null
    };
  }

  async function resolveFileTreeUploadTarget(detail) {
    try {
      return await preflightFileTreeUploadTarget(detail || {});
    } catch (error) {
      console.warn('[mnote upload] upload target preflight fallback', error);
      return fallbackFileTreeUploadTarget(detail || {});
    }
  }

  function uploadedAssetTitle(asset) {
    return String(asset && (asset.file_name || asset.title || asset.name) || '未命名附件').trim() || '未命名附件';
  }

  function uploadedAssetUrl(asset) {
    return String(asset && (asset.file_url || asset.signedUrl || asset.signed_url || asset.thumbnail_url) || '').trim();
  }

  function uploadedAssetType(asset) {
    return String(asset && (asset.asset_type || asset.assetType || asset.mime_type || '') || '').trim();
  }

  function uploadedAssetExtension(asset) {
    var match = uploadedAssetTitle(asset).toLowerCase().match(/\.([a-z0-9]+)$/);
    return match ? match[1] : '';
  }

  function isNonOfficeAttachmentName(name, ext) {
    var codeFileNames = [
      '.dockerignore', '.editorconfig', '.env', '.eslintrc', '.gitattributes', '.gitignore', '.npmrc', '.prettierrc',
      'dockerfile', 'makefile', 'cmakelists.txt', 'gemfile', 'rakefile', 'procfile'
    ];
    return [
      'pdf', 'toml', 'json', 'yaml', 'yml', 'md', 'markdown', 'txt', 'ini', 'env', 'xml', 'html', 'htm', 'css', 'scss',
      'less', 'js', 'jsx', 'ts', 'tsx', 'mjs', 'cjs', 'py', 'rs', 'go', 'java', 'c', 'cpp', 'h', 'hpp', 'cs',
      'php', 'rb', 'sh', 'bash', 'zsh', 'sql', 'lock', 'log', 'vue', 'svelte', 'astro', 'jsonc', 'json5',
      'mts', 'cts', 'lua', 'dart', 'kt', 'kts', 'swift', 'scala', 'gradle', 'groovy', 'clj',
      'ex', 'exs', 'erl', 'hrl', 'fs', 'fsx', 'r', 'jl', 'm', 'mm', 'pl', 'pm', 'ps1', 'bat', 'cmd',
      'psm1', 'psd1', 'dockerfile', 'containerfile', 'proto', 'graphql', 'gql', 'prisma', 'tf', 'tfvars',
      'hcl', 'nix', 'cmake', 'bazel', 'bzl', 'properties', 'conf', 'cfg', 'config', 'service', 'desktop',
      'gitignore', 'gitattributes', 'editorconfig', 'npmrc', 'prettierrc', 'eslintrc'
    ].indexOf(ext) >= 0 || codeFileNames.indexOf(name) >= 0;
  }

  function attachmentExtensionFromFileName(fileName) {
    var name = String(fileName || '').trim().toLowerCase();
    return name.indexOf('.') >= 0 ? name.split('.').pop() : '';
  }

  function isPdfAttachmentFileName(fileName) {
    return attachmentExtensionFromFileName(fileName) === 'pdf';
  }

  function isCodeAttachmentFileName(fileName) {
    var name = String(fileName || '').trim().toLowerCase();
    var ext = attachmentExtensionFromFileName(name);
    return ext !== 'pdf' && isNonOfficeAttachmentName(name, ext);
  }

  function inferCodeAttachmentLanguage(fileName) {
    var name = String(fileName || '').trim().toLowerCase();
    var ext = attachmentExtensionFromFileName(name);
    var byName = {
      'dockerfile': 'dockerfile',
      'makefile': 'makefile',
      'cmakelists.txt': 'cmake',
      '.gitignore': 'gitignore',
      '.gitattributes': 'gitattributes',
      '.editorconfig': 'ini',
      '.env': 'dotenv'
    };
    if (byName[name]) return byName[name];
    var byExt = {
      bash: 'bash', bat: 'batch', c: 'c', cjs: 'javascript', cmd: 'batch', conf: 'text', cpp: 'cpp',
      cs: 'csharp', css: 'css', cts: 'typescript', dart: 'dart', dockerfile: 'dockerfile', env: 'dotenv',
      go: 'go', gql: 'graphql', gradle: 'groovy', graphql: 'graphql', h: 'c', hcl: 'hcl', hpp: 'cpp',
      htm: 'html', html: 'html', ini: 'ini', java: 'java', js: 'javascript', json: 'json', json5: 'json',
      jsonc: 'jsonc', jsx: 'javascript', kt: 'kotlin', kts: 'kotlin', less: 'less', log: 'text',
      lua: 'lua', m: 'objective-c', markdown: 'markdown', md: 'markdown', mjs: 'javascript',
      mts: 'typescript', nix: 'nix', php: 'php', pl: 'perl', pm: 'perl', prisma: 'prisma',
      proto: 'protobuf', ps1: 'powershell', py: 'python', r: 'r', rb: 'ruby', rs: 'rust',
      scss: 'scss', sh: 'bash', sql: 'sql', svelte: 'svelte', swift: 'swift', tf: 'terraform',
      tfvars: 'terraform', toml: 'toml', ts: 'typescript', tsx: 'typescript', txt: 'text',
      vue: 'vue', xml: 'xml', yaml: 'yaml', yml: 'yaml', zsh: 'bash'
    };
    return byExt[ext] || 'text';
  }

  function attachmentClassForFileName(fileName) {
    var name = String(fileName || '').trim().toLowerCase();
    var ext = name.indexOf('.') >= 0 ? name.split('.').pop() : '';
    if (['doc', 'docx', 'odt', 'rtf'].indexOf(ext) >= 0) return 'mnote-uploaded-attachment-row mnote-uploaded-attachment-word';
    if (['ppt', 'pptx', 'odp'].indexOf(ext) >= 0) return 'mnote-uploaded-attachment-row mnote-uploaded-attachment-ppt';
    if (['xls', 'xlsx', 'ods', 'csv'].indexOf(ext) >= 0) return 'mnote-uploaded-attachment-row mnote-uploaded-attachment-sheet';
    if (ext === 'pdf') return 'mnote-uploaded-attachment-row mnote-uploaded-attachment-pdf';
    if (isNonOfficeAttachmentName(name, ext)) {
      return 'mnote-uploaded-attachment-row mnote-uploaded-attachment-code';
    }
    return 'mnote-uploaded-attachment-row mnote-uploaded-attachment-file';
  }

  function uploadedAttachmentClass(asset) {
    return attachmentClassForFileName(uploadedAssetTitle(asset));
  }

  function buildOnlyOfficeAssetOpenUrl(asset, userId) {
    var title = uploadedAssetTitle(asset);
    var fileType = inferOnlyOfficeFileType(title, asset && asset.mime_type);
    if (!fileType) return '';
    var assetId = String(asset && asset.id || '').trim();
    return buildOnlyOfficeOpenUrl({
      fileUrl: assetId ? '' : uploadedAssetUrl(asset),
      fileName: title,
      fileType: fileType,
      assetId: assetId,
      documentId: String(asset && (asset.document_id || asset.documentId) || currentDocumentId() || '').trim(),
      userId: userId || '',
      mode: 'edit'
    });
  }

  function uploadedFileSize(asset) {
    var size = Number(asset && (asset.file_size || asset.fileSize) || 0);
    if (!Number.isFinite(size) || size <= 0) return '';
    if (size >= 1024 * 1024) return (size / 1024 / 1024).toFixed(size >= 10 * 1024 * 1024 ? 1 : 2) + ' MB';
    if (size >= 1024) return (size / 1024).toFixed(size >= 100 * 1024 ? 0 : 2) + ' KB';
    return String(Math.round(size)) + ' B';
  }

  var attachmentMetaCache = Object.create(null);
  var attachmentMetaPending = Object.create(null);
  var legacyOfficeAttachmentIndex = null;
  var legacyOfficeAttachmentIndexPending = null;

  function parseCurrentWorkspaceId() {
    return (new URLSearchParams(window.location.search).get('workspaceId') || '').trim();
  }

  async function fetchLegacyOfficeAttachmentIndex() {
    if (legacyOfficeAttachmentIndex) return legacyOfficeAttachmentIndex;
    if (legacyOfficeAttachmentIndexPending) return legacyOfficeAttachmentIndexPending;
    var documentId = currentDocumentId();
    var workspaceId = parseCurrentWorkspaceId();
    if (!documentId || !workspaceId) {
      legacyOfficeAttachmentIndex = Object.create(null);
      return legacyOfficeAttachmentIndex;
    }
    legacyOfficeAttachmentIndexPending = fetch(
      '/api/tree/projections/file?documentId=' + encodeURIComponent(documentId) + '&workspaceId=' + encodeURIComponent(workspaceId),
      {
        method: 'GET',
        credentials: 'include',
        cache: 'no-store'
      }
    ).then(function(response) {
      return response.json().catch(function() { return null; }).then(function(payload) {
        var items = payload && payload.ok && payload.result && Array.isArray(payload.result.items)
          ? payload.result.items
          : [];
        var index = Object.create(null);
        items.forEach(function(item) {
          if (!item || item.rowKind !== 'asset') return;
          var title = String(item.title || '').trim();
          if (!title || index[title]) return;
          var fileType = inferOnlyOfficeFileType(title, '');
          if (!fileType) return;
          var rowId = String(item.rowId || '').trim();
          var assetId = String(item.assetId || '').trim();
          if (!assetId && rowId.indexOf('asset:') === 0) assetId = rowId.slice('asset:'.length);
          if (!assetId) return;
          index[title] = {
            assetId: assetId,
            fileName: title,
            fileType: fileType,
            documentId: documentId
          };
        });
        legacyOfficeAttachmentIndex = index;
        return index;
      });
    }).catch(function() {
      var empty = Object.create(null);
      legacyOfficeAttachmentIndex = empty;
      return empty;
    }).finally(function() {
      legacyOfficeAttachmentIndexPending = null;
    });
    return legacyOfficeAttachmentIndexPending;
  }

  async function healLegacyOfficeAttachmentParagraphs() {
    var editor = document.querySelector('.editor-surface .ProseMirror');
    if (!(editor instanceof HTMLElement)) return;
    var index = await fetchLegacyOfficeAttachmentIndex();
    var paragraphs = Array.from(editor.querySelectorAll('p'));
    paragraphs.forEach(function(paragraph) {
      if (!(paragraph instanceof HTMLParagraphElement)) return;
      if (paragraph.querySelector('a, img, video, audio, table, iframe, canvas')) return;
      if (paragraph.childNodes.length !== 1 || paragraph.firstChild?.nodeType !== Node.TEXT_NODE) return;
      var fileName = String(paragraph.textContent || '').trim();
      if (!fileName) return;
      var detail = index[fileName];
      if (!detail) return;
      var link = document.createElement('a');
      link.textContent = fileName;
      link.setAttribute('href', buildOnlyOfficeOpenPath({
        fileUrl: '',
        fileName: detail.fileName,
        fileType: detail.fileType,
        assetId: detail.assetId,
        documentId: detail.documentId || currentDocumentId() || '',
        userId: '',
        mode: 'edit'
      }));
      link.setAttribute('data-mnote-attachment-link', 'true');
      link.setAttribute('data-asset-id', detail.assetId);
      attachmentClassForFileName(fileName).split(/\s+/).forEach(function(name) {
        if (name) link.classList.add(name);
      });
      paragraph.replaceChildren(link);
      enhanceEditorAttachmentLink(link);
    });
  }

  function applyEditorAttachmentMeta(link, meta) {
    if (!(link instanceof HTMLAnchorElement) || !meta) return;
    if (meta.assetId) link.setAttribute('data-asset-id', meta.assetId);
    if (meta.fileSize) link.setAttribute('data-file-size', meta.fileSize);
  }

  async function hydrateEditorAttachmentMeta(link) {
    if (!(link instanceof HTMLAnchorElement)) return;
    var detail = detailFromEditorAttachmentLink(link);
    var assetId = String(detail && detail.assetId || '').trim();
    if (!assetId) return;
    if (attachmentMetaCache[assetId]) {
      applyEditorAttachmentMeta(link, attachmentMetaCache[assetId]);
      return;
    }
    if (attachmentMetaPending[assetId]) {
      try { await attachmentMetaPending[assetId]; } catch (_) {}
      applyEditorAttachmentMeta(link, attachmentMetaCache[assetId]);
      return;
    }
    attachmentMetaPending[assetId] = fetch('/api/media/sign?assetId=' + encodeURIComponent(assetId), {
      method: 'GET',
      credentials: 'include',
      cache: 'no-store'
    }).then(function(response) {
      return response.json().catch(function() { return null; }).then(function(payload) {
        if (!response.ok || !payload) return null;
        var asset = payload.asset && typeof payload.asset === 'object' ? payload.asset : {};
        var meta = {
          assetId: assetId,
          fileSize: uploadedFileSize(asset)
        };
        attachmentMetaCache[assetId] = meta;
        return meta;
      });
    }).catch(function() {
      return null;
    }).finally(function() {
      delete attachmentMetaPending[assetId];
    });
    try {
      var meta = await attachmentMetaPending[assetId];
      applyEditorAttachmentMeta(link, meta);
    } catch (_) {}
  }

  function revealFileTreeRow(row) {
    if (!(row instanceof HTMLElement)) return;
    var node = row.closest('.tree-node');
    while (node && node.parentElement) {
      if (node.parentElement.classList && node.parentElement.classList.contains('tree-children')) {
        node.parentElement.classList.remove('tree-children--collapsed');
        var parentNode = node.parentElement.closest('.tree-node');
        var parentRow = parentNode ? parentNode.querySelector(':scope > .tree-row') : null;
        if (parentRow instanceof HTMLElement) {
          parentRow.setAttribute('aria-expanded', 'true');
          var toggle = parentRow.querySelector('[data-rust-action="toggle"]');
          if (toggle) toggle.setAttribute('aria-expanded', 'true');
        }
      }
      node = node.parentElement.closest('.tree-node');
    }
    try { row.scrollIntoView({ block: 'nearest' }); } catch (_) {}
  }

  function revealFileTreeAssetRow(assetId) {
    if (!assetId) return false;
    var row = document.querySelector('.tree-row[data-shell-mode="filetree"][data-asset-id="' + cssEscape(assetId) + '"]');
    if (!(row instanceof HTMLElement)) return false;
    revealFileTreeRow(row);
    return true;
  }

  function appendUploadedAssetRow(asset, documentId) {
    var assetId = String(asset && asset.id || '').trim();
    if (!assetId) return;
    if (revealFileTreeAssetRow(assetId)) return;
    var targetDocumentId = String(documentId || asset.document_id || asset.documentId || currentDocumentId() || '').trim();
    var parentRow = targetDocumentId
      ? document.querySelector('.tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape('doc:' + targetDocumentId) + '"]')
      : null;
    if (!parentRow) parentRow = document.querySelector('.tree-row[data-shell-mode="filetree"][data-row-kind="document"]');
    var root = document.querySelector('#sidebar-file-tree-root .tree-root');
    if (!root && !parentRow) return;
    var parentLi = parentRow ? parentRow.closest('.tree-node') : null;
    var children = parentLi ? parentLi.querySelector(':scope > .tree-children') : null;
    if (parentLi && !children) {
      children = document.createElement('ul');
      children.className = 'tree-children';
      parentLi.appendChild(children);
    }
    if (children) {
      children.classList.remove('tree-children--collapsed');
      if (parentRow) {
        parentRow.setAttribute('aria-expanded', 'true');
        var toggle = parentRow.querySelector('[data-rust-action="toggle"]');
        if (toggle) toggle.setAttribute('aria-expanded', 'true');
      }
    }
    var container = children || root;
    var li = document.createElement('li');
    li.className = 'tree-node';
    li.setAttribute('data-node-id', 'asset:' + assetId);
    var title = uploadedAssetTitle(asset);
    var iconKind = uploadedAssetType(asset) || 'file';
    li.innerHTML =
      '<div class="tree-row" role="treeitem" aria-level="' + (parentRow ? '2' : '1') + '" aria-expanded="false" data-rust-rendered-row="filetree" data-testid="filetree-asset-row" data-row-id="' + escapeHtml('asset:' + assetId) + '" data-row-kind="asset" data-node-id="' + escapeHtml('asset:' + assetId) + '" data-document-id="' + escapeHtml(targetDocumentId) + '" data-doc-id="' + escapeHtml(targetDocumentId) + '" data-asset-id="' + escapeHtml(assetId) + '" data-shell-mode="filetree" data-selected="false" data-active="false" draggable="true">' +
      '<span class="tree-spacer" aria-hidden="true"></span><span class="tree-kind-badge" data-kind="' + escapeHtml(iconKind) + '" aria-hidden="true"></span>' +
      '<button type="button" class="tree-link" data-rust-action="open" data-row-id="' + escapeHtml('asset:' + assetId) + '" data-document-id="' + escapeHtml(targetDocumentId) + '" data-asset-id="' + escapeHtml(assetId) + '"><span class="tree-link-title">' + escapeHtml(title) + '</span></button>' +
      '<div class="tree-actions"><button type="button" class="tree-action" data-testid="filetree-action-menu" data-rust-action="menu" data-row-id="' + escapeHtml('asset:' + assetId) + '" aria-label="更多操作">…</button></div></div>';
    container.appendChild(li);
    revealFileTreeRow(li.querySelector('.tree-row'));
    document.documentElement.setAttribute('data-mnote-last-upload-asset-id', assetId);
  }

  function removeFileTreeAssetRow(assetId) {
    var normalized = String(assetId || '').trim();
    if (!normalized) return false;
    var row = document.querySelector('.tree-row[data-shell-mode="filetree"][data-asset-id="' + cssEscape(normalized) + '"]');
    var node = row ? row.closest('.tree-node') : null;
    if (!node || !node.parentElement) return false;
    node.parentElement.removeChild(node);
    return true;
  }

  function applyAssetsChangedToFileTree(detail) {
    detail = detail || {};
    var docId = String(detail.docId || detail.documentId || currentDocumentId() || '').trim();
    var changed = false;
    var explicitAsset = detail.asset && typeof detail.asset === 'object' ? detail.asset : null;
    if (explicitAsset) {
      appendUploadedAssetRow(explicitAsset, docId);
      changed = true;
    }
    var mindmapAssetIds = Array.isArray(detail.mindmapAssetIds) ? detail.mindmapAssetIds : [];
    if (detail.mindmapDeleted === true) {
      mindmapAssetIds.forEach(function(assetId) {
        if (removeFileTreeAssetRow(assetId)) changed = true;
      });
    }
    if (changed) {
      document.documentElement.setAttribute('data-mnote-assets-local-applied', 'true');
    }
  }

  function mindmapAssetFromTarget(documentId, mindmapId) {
    var docId = String(documentId || currentDocumentId() || '').trim();
    var assetId = String(mindmapId || '').trim();
    if (!docId || !assetId) return null;
    var fileName = 'mindmap-' + assetId + '.json';
    return {
      id: assetId,
      document_id: docId,
      asset_type: 'mindmap',
      file_name: fileName,
      file_url: '/documents/' + encodeURIComponent(docId) + '/' + encodeURIComponent(fileName)
    };
  }

  function parseMindmapApiTarget(input) {
    try {
      var rawUrl = typeof input === 'string'
        ? input
        : input && typeof input.url === 'string'
          ? input.url
          : '';
      if (!rawUrl) return null;
      var url = new URL(rawUrl, window.location.origin);
      var match = /^\/api\/mindmap\/([^/]+)\/([^/]+)$/.exec(url.pathname);
      if (!match) return null;
      return {
        documentId: decodeURIComponent(match[1]),
        mindmapId: decodeURIComponent(match[2])
      };
    } catch (_) {
      return null;
    }
  }

  function requestMethod(input, init) {
    return String(
      init && init.method
        ? init.method
        : input && typeof input.method === 'string'
          ? input.method
          : 'GET'
    ).toUpperCase();
  }

  function requestBodyHasMindmapCreateOnly(init) {
    var body = init && typeof init.body === 'string' ? init.body : '';
    if (!body) return false;
    try {
      var payload = JSON.parse(body);
      return payload && payload.createOnly === true;
    } catch (_) {
      return false;
    }
  }

  function applyMindmapApiMutationToFileTree(target) {
    if (!target || !target.documentId || !target.mindmapId) return;
    var asset = mindmapAssetFromTarget(target.documentId, target.mindmapId);
    if (!asset) return;
    appendUploadedAssetRow(asset, target.documentId);
    document.documentElement.setAttribute('data-mnote-assets-local-applied', 'true');
    document.documentElement.setAttribute('data-mnote-last-mindmap-asset-id', target.mindmapId);
  }

  function installMindmapAssetFetchObserver() {
    if (window.__mnoteMindmapAssetFetchObserverInstalled === true) return;
    if (typeof window.fetch !== 'function') return;
    window.__mnoteMindmapAssetFetchObserverInstalled = true;
    var originalFetch = window.fetch.bind(window);
    window.fetch = function(input, init) {
      var target = parseMindmapApiTarget(input);
      var method = requestMethod(input, init);
      var createOnly = requestBodyHasMindmapCreateOnly(init || {});
      return originalFetch(input, init).then(function(response) {
        if (target && method === 'POST' && response && response.ok) {
          if (createOnly || target.documentId === currentDocumentId()) {
            applyMindmapApiMutationToFileTree(target);
          }
        }
        return response;
      });
    };
  }

  async function insertUploadedAssetIntoEditor(asset) {
    var editorRoot = document.querySelector('.editor-surface .ProseMirror');
    var editor = editorRoot && editorRoot.editor;
    if (!editor || !editor.chain) return false;
    var title = uploadedAssetTitle(asset);
    var url = uploadedAssetUrl(asset);
    var type = uploadedAssetType(asset);
    var assetId = String(asset && asset.id || '').trim();
    var sizeLabel = uploadedFileSize(asset);
    try {
      if (type === 'image' && url) {
        return editor.chain().focus().setImage({ src: url, alt: title, title: title }).run() === true;
      }
      var userId = '';
      var onlyOfficeUrl = buildOnlyOfficeAssetOpenUrl(asset, userId);
      if (onlyOfficeUrl && assetId) {
        userId = await fetchCurrentOnlyOfficeUserId();
        onlyOfficeUrl = buildOnlyOfficeAssetOpenUrl(asset, userId);
      }
      var href = onlyOfficeUrl || url;
      if (href) {
        var storedHref = onlyOfficeUrl
          ? buildOnlyOfficeOpenPath({
              fileUrl: '',
              fileName: title,
              fileType: inferOnlyOfficeFileType(title, asset && asset.mime_type) || 'docx',
              assetId: assetId,
              documentId: String(asset && (asset.document_id || asset.documentId) || currentDocumentId() || '').trim(),
              userId: userId || '',
              mode: 'edit'
            })
          : href;
        var inserted = editor.chain().focus().insertContent({
          type: 'paragraph',
          content: [{
            type: 'text',
            text: title,
            marks: [{
              type: 'link',
              attrs: {
                href: storedHref,
                target: '_blank',
                rel: 'noopener noreferrer nofollow',
                class: uploadedAttachmentClass(asset)
              }
            }]
          }]
        }).run() === true;
        window.setTimeout(function() {
          enhanceEditorAttachmentLinks();
          var selector = assetId
            ? '.editor-surface .ProseMirror a[href*="' + cssEscape(assetId) + '"]'
            : '.editor-surface .ProseMirror a';
          var link = document.querySelector(selector);
          if (link instanceof HTMLElement) {
            link.setAttribute('data-mnote-attachment-link', 'true');
            if (assetId) link.setAttribute('data-asset-id', assetId);
            if (sizeLabel) link.setAttribute('data-file-size', sizeLabel);
          }
        }, 0);
        return inserted;
      }
    } catch (error) {
      console.warn('[mnote upload] insert uploaded asset failed', error);
    }
    return false;
  }

  async function uploadFileToMediaAsset(file, plan, options) {
    var form = new FormData();
    form.append('file', file);
    form.append('workspaceId', plan.workspaceId);
    form.append('documentId', plan.targetDocumentId);
    if (plan.targetMindmapId) form.append('mindmapId', plan.targetMindmapId);
    var response = await fetch('/api/media/upload', {
      method: 'POST',
      credentials: 'include',
      body: form
    });
    var payload = await response.json().catch(function() { return null; });
    if (!response.ok || !payload || !payload.asset) {
      throw new Error(payload && payload.error ? payload.error : '上传失败');
    }
    appendUploadedAssetRow(payload.asset, plan.targetDocumentId);
    if (options && options.insertIntoEditor) {
      await insertUploadedAssetIntoEditor(payload.asset);
    }
    window.dispatchEvent(new CustomEvent('wolai:assets-changed', {
      detail: { docId: plan.targetDocumentId, asset: payload.asset, assetIds: [payload.asset.id] }
    }));
    return payload.asset;
  }

  window.addEventListener('wolai:assets-changed', function(event) {
    applyAssetsChangedToFileTree(event.detail || {});
  });
  installMindmapAssetFetchObserver();

  async function uploadFilesWithResolvedTarget(files, detail, options) {
    var list = Array.from(files || []).filter(Boolean);
    if (!list.length) return [];
    var plan = await resolveFileTreeUploadTarget(detail || {});
    var uploaded = [];
    var errors = [];
    for (var i = 0; i < list.length; i += 1) {
      try {
        uploaded.push(await uploadFileToMediaAsset(list[i], plan, options || {}));
      } catch (error) {
        errors.push(list[i].name + ': ' + (error && error.message ? error.message : '上传失败'));
      }
    }
    if (errors.length) {
      window.alert('部分文件上传失败：\n' + errors.slice(0, 6).join('\n') + (errors.length > 6 ? '\n...' : ''));
    }
    return uploaded;
  }

  function openEditorUploadFilePicker(detail) {
    var input = document.createElement('input');
    input.type = 'file';
    input.multiple = detail && detail.multiple !== false;
    if (detail && detail.accept) input.accept = String(detail.accept);
    input.style.position = 'fixed';
    input.style.left = '-9999px';
    input.style.top = '-9999px';
    document.body.appendChild(input);
    input.addEventListener('change', function() {
      var files = Array.from(input.files || []);
      input.remove();
      void uploadFilesWithResolvedTarget(files, {
        workspaceId: resolveWorkspaceId(document.body),
        documentId: currentDocumentId(),
        targetRowId: null
      }, {
        insertIntoEditor: detail && detail.insertIntoEditor !== false
      });
    }, { once: true });
    input.click();
  }

  window.addEventListener('mnote:editor-upload-request', function(event) {
    openEditorUploadFilePicker(event.detail || {});
  });

  window.addEventListener('tree.filetree.external-drop', function(event) {
    var detail = event.detail || {};
    void uploadFilesWithResolvedTarget(detail.files || [], detail, {
      insertIntoEditor: String(detail.documentId || '') === currentDocumentId()
    });
  });

  document.addEventListener('dragover', function(event) {
    var editorTarget = closestAction(event.target, '[data-testid="mnote-leptos-tiptap-editor-stage"], .editor-surface .ProseMirror');
    var hasFiles = event.dataTransfer && Array.prototype.indexOf.call(event.dataTransfer.types || [], 'Files') >= 0;
    if (!editorTarget || !hasFiles) return;
    event.preventDefault();
    event.stopPropagation();
    if (event.dataTransfer) event.dataTransfer.dropEffect = 'copy';
  }, true);

  document.addEventListener('drop', function(event) {
    var editorTarget = closestAction(event.target, '[data-testid="mnote-leptos-tiptap-editor-stage"], .editor-surface .ProseMirror');
    var files = event.dataTransfer ? Array.from(event.dataTransfer.files || []) : [];
    if (!editorTarget || !files.length) return;
    event.preventDefault();
    event.stopPropagation();
    void uploadFilesWithResolvedTarget(files, {
      workspaceId: resolveWorkspaceId(document.body),
      documentId: currentDocumentId(),
      targetRowId: null
    }, {
      insertIntoEditor: true
    });
  }, true);

  function rowTitle(row) {
    var title = row ? row.querySelector(':scope > .tree-link > .tree-link-title') : null;
    return title && title.textContent ? title.textContent.trim() : '无标题';
  }

  function rowCenter(row) {
    var rect = row.getBoundingClientRect();
    return { x: rect.left + Math.min(rect.width - 12, 180), y: rect.top + Math.min(rect.height, 22) };
  }

  function closeTreeContextMenu() {
    if (activeTreeContextMenu && activeTreeContextMenu.parentElement) {
      activeTreeContextMenu.parentElement.removeChild(activeTreeContextMenu);
    }
    activeTreeContextMenu = null;
  }

  function copyTreeContextValue(value, actionName) {
    var text = String(value || '');
    var done = function() {
      document.documentElement.setAttribute('data-mnote-tree-context-last-copy', actionName || 'copy');
    };
    if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
      return navigator.clipboard.writeText(text).then(done).catch(function(){});
    }
    var textarea = document.createElement('textarea');
    textarea.value = text;
    textarea.setAttribute('readonly', 'readonly');
    textarea.style.position = 'fixed';
    textarea.style.left = '-9999px';
    document.body.appendChild(textarea);
    textarea.select();
    try { document.execCommand('copy'); } catch (_) {}
    document.body.removeChild(textarea);
    done();
    return Promise.resolve();
  }

  function documentHref(documentId, workspaceId) {
    var url = new URL('/documents/' + encodeURIComponent(documentId), window.location.origin);
    if (workspaceId) url.searchParams.set('workspaceId', workspaceId);
    copyWorkspaceSourceParams(url);
    return url.toString();
  }

  function convertToPreviousSiblingChild(trigger, detail) {
    var documentId = detail.documentId || '';
    var row = document.querySelector('#sidebar-tree-root .tree-row[data-node-id="' + cssEscape(documentId) + '"]');
    if (!(row instanceof HTMLElement)) return;
    var parentId = row.getAttribute('data-parent-id') || '';
    var siblings = Array.from(document.querySelectorAll('#sidebar-tree-root .tree-row[data-shell-mode="page"]')).filter(function(candidate) {
      return (candidate.getAttribute('data-parent-id') || '') === parentId;
    });
    var index = siblings.indexOf(row);
    if (index <= 0) {
      window.alert('当前页面前面没有同级页面。');
      return;
    }
    var previous = siblings[index - 1];
    var previousId = previous.getAttribute('data-node-id') || '';
    var children = previous.parentElement ? previous.parentElement.querySelectorAll(':scope > .tree-children > .tree-node > .tree-row[data-shell-mode="page"]') : [];
    void dispatchTreeCommand(trigger || row, {
      action: 'move',
      workspaceId: detail.workspaceId || resolveWorkspaceId(row),
      documentId: documentId,
      parentId: previousId,
      sortOrder: children.length
    });
  }

  function handleTreeContextMenuAction(action, detail, trigger) {
    closeTreeContextMenu();
    detail = detail || {};
    if (detail.contextKind === 'attachment') {
      if (action === 'copy-link') {
        void copyTreeContextValue(detail.href || '', 'attachment-copy-link');
        return;
      }
      if (action === 'download') {
        openEditorAttachmentDownload(detail);
        return;
      }
      if (action === 'popup-preview') {
        openEditorAttachmentDetail(detail);
        return;
      }
      if (action === 'right-preview') {
        dispatchSidebarEvent('tree.attachment.open-right', detail);
        return;
      }
      if (action === 'copy-id') {
        void copyTreeContextValue(detail.assetId || '', 'attachment-copy-id');
        return;
      }
      dispatchSidebarEvent('tree.attachment.action', { action: action, attachment: detail });
      return;
    }
    var documentId = detail.documentId || '';
    var workspaceId = detail.workspaceId || resolveWorkspaceId(trigger || document.body);
    var title = detail.title || '无标题';
    if (action === 'open-right') {
      dispatchSidebarEvent('tree.page.open-right', detail);
      return;
    }
    if (action === 'share') {
      dispatchSidebarEvent('tree.page.share', detail);
      return;
    }
    if (action === 'copy-link') {
      void copyTreeContextValue(documentHref(documentId, workspaceId), 'copy-link');
      return;
    }
    if (action === 'copy-link-title') {
      void copyTreeContextValue(title + ' ' + documentHref(documentId, workspaceId), 'copy-link-title');
      return;
    }
    if (action === 'copy-reference-inline') {
      void copyTreeContextValue('((' + title + ' ' + documentId + '))', 'copy-reference-inline');
      return;
    }
    if (action === 'copy-reference-embed') {
      void copyTreeContextValue('{{' + title + ' ' + documentId + '}}', 'copy-reference-embed');
      return;
    }
    if (action === 'copy-id') {
      void copyTreeContextValue(documentId || detail.assetId || detail.rowId || '', 'copy-id');
      return;
    }
    if (action === 'duplicate') {
      dispatchSidebarEvent('tree.page.duplicate', detail);
      return;
    }
    if (action === 'rename') {
      var nextTitle = window.prompt('重命名页面', title);
      if (nextTitle && nextTitle.trim() && documentId) {
        void dispatchTreeCommand(trigger || document.body, {
          action: 'rename',
          workspaceId: workspaceId,
          documentId: documentId,
          title: nextTitle.trim()
        }).then(function(){ updateTitleEverywhere(documentId, nextTitle.trim()); });
      }
      return;
    }
    if (action === 'create-child') {
      void createPage(trigger || document.body, documentId);
      return;
    }
    if (action === 'convert-child') {
      convertToPreviousSiblingChild(trigger, detail);
      return;
    }
    if (action === 'delete-trash' && documentId) {
      if (!window.confirm('确定要将“' + title + '”删除到垃圾桶吗？')) return;
      void dispatchTreeCommand(trigger || document.body, {
        action: 'purge',
        workspaceId: workspaceId,
        documentId: documentId
      });
    }
  }

  function appendTreeContextMenuButton(menu, item, detail, trigger) {
    if (item.separator) {
      var sep = document.createElement('div');
      sep.className = 'mnote-tree-context-menu__separator';
      sep.setAttribute('role', 'separator');
      menu.appendChild(sep);
      return;
    }
    var button = document.createElement('button');
    button.type = 'button';
    button.className = item.danger ? 'mnote-tree-context-menu__item mnote-tree-context-menu__item--danger' : 'mnote-tree-context-menu__item';
    button.setAttribute('role', 'menuitem');
    button.setAttribute('data-action', item.action);
    button.disabled = item.disabled === true;
    var icon = document.createElement('span');
    icon.className = 'material-symbols-outlined mnote-tree-context-menu__icon';
    icon.setAttribute('aria-hidden', 'true');
    icon.setAttribute('data-icon', item.icon || 'radio_button_unchecked');
    var label = document.createElement('span');
    label.className = 'mnote-tree-context-menu__label';
    label.textContent = item.label;
    button.appendChild(icon);
    button.appendChild(label);
    if (item.shortcut) {
      var shortcut = document.createElement('span');
      shortcut.className = 'mnote-tree-context-menu__shortcut';
      shortcut.textContent = item.shortcut;
      button.appendChild(shortcut);
    }
    button.addEventListener('click', function(event) {
      event.preventDefault();
      event.stopPropagation();
      handleTreeContextMenuAction(item.action, detail, trigger);
    });
    menu.appendChild(button);
  }

  function openTreeContextMenu(kind, detail, x, y, trigger) {
    closeTreeContextMenu();
    detail = Object.assign({}, detail || {}, { contextKind: kind });
    var menu = document.createElement('div');
    menu.className = 'mnote-tree-context-menu';
    menu.setAttribute('role', 'menu');
    menu.setAttribute('data-testid', 'mnote-tree-context-menu');
    menu.setAttribute('data-kind', kind);
    var isAttachment = kind === 'attachment';
    var isAsset = kind === 'filetree' && detail.assetId && detail.rowKind !== 'document' && detail.rowKind !== 'index';
    var items = isAttachment ? [
      { action: 'duplicate', icon: 'file_copy', label: '拷贝副本', shortcut: 'Ctrl + D' },
      { action: 'delete-trash', icon: 'delete', label: '删除', shortcut: 'Del', danger: true },
      { separator: true },
      { action: 'copy-link', icon: 'link', label: '复制链接' },
      { action: 'move-embed', icon: 'subdirectory_arrow_right', label: '移动/嵌入到...', shortcut: 'Alt+Shift+M/G' },
      { action: 'history', icon: 'history', label: '块历史...' },
      { separator: true },
      { action: 'popup-preview', icon: 'preview', label: '弹窗预览' },
      { action: 'right-preview', icon: 'right_panel_open', label: '右侧预览' },
      { action: 'download', icon: 'download', label: '下载' },
      { action: 'replace-file', icon: 'sync', label: '更换文件' },
      { action: 'rename-attachment', icon: 'drive_file_rename_outline', label: '重命名' },
      { action: 'comment', icon: 'mode_comment', label: '评论', shortcut: 'Ctrl+Alt+M' },
      { action: 'caption', icon: 'notes', label: '添加说明文字' },
      { separator: true },
      { action: 'color', icon: 'format_paint', label: '颜色' }
    ] : isAsset ? [
      { action: 'open-right', icon: 'open_in_new', label: '在右侧边栏打开', shortcut: 'Alt + O' },
      { action: 'copy-id', icon: 'tag', label: '复制资源 ID' }
    ] : [
      { action: 'open-right', icon: 'right_panel_open', label: '在右侧边栏打开', shortcut: 'Alt+' },
      { separator: true },
      { action: 'copy-link', icon: 'link', label: '复制访问链接' },
      { action: 'copy-reference-inline', icon: 'content_copy', label: '复制页面引用链接' },
      { action: 'copy-id', icon: 'tag', label: '复制页面ID' },
      { separator: true },
      { action: 'duplicate', icon: 'file_copy', label: '拷贝副本' },
      { separator: true },
      { action: 'rename', icon: 'edit', label: '重命名' },
      { separator: true },
      { action: 'delete-trash', icon: 'delete', label: '删除', shortcut: 'Del', danger: true }
    ];
    items.forEach(function(item) { appendTreeContextMenuButton(menu, item, detail, trigger); });
    document.body.appendChild(menu);
    var rect = menu.getBoundingClientRect();
    var left = Math.min(Math.max(8, x || 8), Math.max(8, window.innerWidth - rect.width - 8));
    var top = Math.min(Math.max(8, y || 8), Math.max(8, window.innerHeight - rect.height - 8));
    menu.style.left = left + 'px';
    menu.style.top = top + 'px';
    activeTreeContextMenu = menu;
  }

  function openPageTreeContextMenu(row, x, y, trigger) {
    if (!(row instanceof HTMLElement)) return;
    var documentId = row.getAttribute('data-node-id') || '';
    openTreeContextMenu('page', {
      documentId: documentId,
      rowId: documentId,
      rowKind: 'document',
      title: rowTitle(row),
      workspaceId: resolveWorkspaceId(row)
    }, x, y, trigger || row);
  }

  function openFileTreeContextMenu(row, x, y, trigger) {
    if (!(row instanceof HTMLElement)) return;
    var documentId = row.getAttribute('data-document-id') || row.getAttribute('data-doc-id') || '';
    openTreeContextMenu('filetree', {
      documentId: documentId,
      rowId: row.getAttribute('data-row-id') || '',
      rowKind: row.getAttribute('data-row-kind') || '',
      assetId: row.getAttribute('data-asset-id') || '',
      title: rowTitle(row),
      workspaceId: resolveWorkspaceId(row)
    }, x, y, trigger || row);
  }

  function visibleFileTreeRows() {
    return Array.from(document.querySelectorAll('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"]'))
      .filter(function(row) { return row instanceof HTMLElement && row.offsetParent !== null; });
  }

  function syncSidebarFileTreeSelection() {
    var rows = visibleFileTreeRows();
    rows.forEach(function(row) {
      var rowId = row.getAttribute('data-row-id') || '';
      row.setAttribute('data-selected', String(Boolean(rowId && sidebarFileTreeSelection.selectedRowIds.has(rowId))));
      row.setAttribute('data-focused', String(rowId === sidebarFileTreeSelection.focusedRowId));
    });
    window.dispatchEvent(new CustomEvent('tree.filetree.selection.changed', {
      detail: {
        selectedRowIds: Array.from(sidebarFileTreeSelection.selectedRowIds),
        anchorRowId: sidebarFileTreeSelection.anchorRowId,
        focusedRowId: sidebarFileTreeSelection.focusedRowId
      }
    }));
  }

  function selectSidebarFileTreeRow(row, modifiers) {
    if (!(row instanceof HTMLElement)) return [];
    var rowId = row.getAttribute('data-row-id') || '';
    if (!rowId) return [];
    var rows = visibleFileTreeRows();
    var visibleRowIds = rows.map(function(item) { return item.getAttribute('data-row-id') || ''; }).filter(Boolean);
    var selected = new Set(sidebarFileTreeSelection.selectedRowIds);
    var shiftKey = Boolean(modifiers && modifiers.shiftKey);
    var ctrlKey = Boolean(modifiers && (modifiers.ctrlKey || modifiers.metaKey));
    if (shiftKey && sidebarFileTreeSelection.anchorRowId) {
      var anchorIndex = visibleRowIds.indexOf(sidebarFileTreeSelection.anchorRowId);
      var targetIndex = visibleRowIds.indexOf(rowId);
      if (anchorIndex >= 0 && targetIndex >= 0) {
        selected = new Set(visibleRowIds.slice(Math.min(anchorIndex, targetIndex), Math.max(anchorIndex, targetIndex) + 1));
      } else {
        selected = new Set([rowId]);
        sidebarFileTreeSelection.anchorRowId = rowId;
      }
    } else if (ctrlKey) {
      if (selected.has(rowId) && selected.size > 1) selected.delete(rowId);
      else selected.add(rowId);
      sidebarFileTreeSelection.anchorRowId = rowId;
    } else {
      selected = new Set([rowId]);
      sidebarFileTreeSelection.anchorRowId = rowId;
    }
    sidebarFileTreeSelection.selectedRowIds = selected;
    sidebarFileTreeSelection.focusedRowId = rowId;
    syncSidebarFileTreeSelection();
    return Array.from(selected);
  }

  function selectedSidebarFileTreeRowIdsForDrag(row) {
    var rowId = row instanceof HTMLElement ? row.getAttribute('data-row-id') || '' : '';
    if (rowId && sidebarFileTreeSelection.selectedRowIds.has(rowId)) {
      return Array.from(sidebarFileTreeSelection.selectedRowIds);
    }
    return rowId ? [rowId] : [];
  }

  function ensureSearchModal() {
    var existing = document.querySelector('[data-testid="wolai-search-modal"]');
    if (existing instanceof HTMLElement) return existing;

    var overlay = document.createElement('div');
    overlay.className = 'wolai-search-overlay';
    overlay.setAttribute('data-testid', 'wolai-search-modal');
    overlay.setAttribute('role', 'dialog');
    overlay.setAttribute('aria-modal', 'true');
    overlay.hidden = true;

    overlay.innerHTML = '' +
      '<div class="wolai-search-dialog">' +
        '<div class="wolai-search-input-row">' +
          '<span class="material-symbols-outlined wolai-search-input-icon" data-icon="search" aria-hidden="true"></span>' +
          '<input data-testid="wolai-search-input" class="wolai-search-input" type="search" autocomplete="off" placeholder="在当前工作区中搜索" />' +
          '<button type="button" class="wolai-search-close" data-testid="wolai-search-close" aria-label="关闭搜索">×</button>' +
        '</div>' +
        '<div class="wolai-search-options" data-testid="wolai-search-options" aria-label="搜索选项" hidden>' +
          '<div class="wolai-search-options-left">' +
            '<span class="wolai-search-switch-control"><span>仅匹配标题</span><button type="button" class="wolai-search-switch" data-search-switch="title" role="switch" aria-checked="false" aria-label="仅匹配标题"></button></span>' +
            '<span class="wolai-search-switch-control"><span>精确匹配</span><button type="button" class="wolai-search-switch" data-search-switch="exact" role="switch" aria-checked="false" aria-label="精确匹配"></button></span>' +
            '<span class="wolai-search-sort-control"><span>按编辑时间</span><button type="button" class="wolai-search-sort-value" data-search-sort="updated" aria-label="按编辑时间范围">所有</button></span>' +
            '<span class="wolai-search-sort-control"><span>按创建时间</span><button type="button" class="wolai-search-sort-value" data-search-sort="created" aria-label="按创建时间范围">所有</button></span>' +
          '</div>' +
          '<div class="wolai-search-options-right">' +
            '<span class="wolai-search-switch-control"><span>页面内搜索</span><button type="button" class="wolai-search-switch" data-search-switch="page" role="switch" aria-checked="false" aria-label="页面内搜索"></button></span>' +
          '</div>' +
        '</div>' +
        '<div class="wolai-search-result-meta" data-testid="wolai-search-result-meta"></div>' +
        '<div class="wolai-search-results" data-testid="wolai-search-results" data-search-results-owner="rust-kernel"></div>' +
      '</div>';

    document.body.appendChild(overlay);
    var input = overlay.querySelector('[data-testid="wolai-search-input"]');
    var closeButton = overlay.querySelector('[data-testid="wolai-search-close"]');
    if (input) input.addEventListener('input', scheduleSearchResultsRender);
    overlay.querySelectorAll('[data-search-switch]').forEach(function(button) {
      button.addEventListener('click', function() {
        var isOn = button.getAttribute('aria-checked') !== 'true';
        button.setAttribute('aria-checked', isOn ? 'true' : 'false');
        button.classList.toggle('is-on', isOn);
        scheduleSearchResultsRender();
      });
    });
    if (closeButton) closeButton.addEventListener('click', closeSearchModal);
    overlay.addEventListener('click', function(event) {
      if (event.target === overlay) closeSearchModal();
    });
    return overlay;
  }

  var activeSearchRequestId = 0;
  var searchRenderTimer = 0;

  function searchText(value) {
    return String(value == null ? '' : value).replace(/\s+/g, ' ').trim();
  }

  function currentWorkspaceName() {
    var name = document.querySelector('.sidebar-workspace-name');
    return searchText(name && name.textContent) || '当前工作区';
  }

  function currentDocumentId() {
    var shell = document.querySelector('.document-shell[data-document-id]');
    var bodyId = document.body && document.body.getAttribute('data-document-id');
    return searchText(shell && shell.getAttribute('data-document-id')) || searchText(bodyId);
  }

  function searchSwitchValue(overlay, name) {
    var button = overlay.querySelector('[data-search-switch="' + name + '"]');
    return button instanceof HTMLElement && button.getAttribute('aria-checked') === 'true';
  }

  function highlightedHtml(value) {
    return escapeHtml(value)
      .replace(/&lt;mark&gt;/g, '<mark>')
      .replace(/&lt;\/mark&gt;/g, '</mark>');
  }

  function highlightSearchTitle(title, query) {
    var cleanTitle = searchText(title);
    var cleanQuery = searchText(query);
    if (!cleanQuery) return escapeHtml(cleanTitle);
    var index = cleanTitle.toLowerCase().indexOf(cleanQuery.toLowerCase());
    if (index < 0) return escapeHtml(cleanTitle);
    return escapeHtml(cleanTitle.slice(0, index)) +
      '<mark>' + escapeHtml(cleanTitle.slice(index, index + cleanQuery.length)) + '</mark>' +
      escapeHtml(cleanTitle.slice(index + cleanQuery.length));
  }

  function renderSearchRecentState(overlay) {
    var options = overlay.querySelector('[data-testid="wolai-search-options"]');
    var meta = overlay.querySelector('[data-testid="wolai-search-result-meta"]');
    var results = overlay.querySelector('[data-testid="wolai-search-results"]');
    if (options instanceof HTMLElement) options.hidden = true;
    if (meta) meta.innerHTML = '<span>最近浏览</span><span>Ctrl + Enter 新窗口打开 / Alt + Enter 右侧边栏打开</span>';
    if (results) {
      results.innerHTML = '<div class="wolai-search-recent" data-testid="wolai-search-recent">暂无最近浏览</div>';
    }
  }

  function scheduleSearchResultsRender() {
    window.clearTimeout(searchRenderTimer);
    searchRenderTimer = window.setTimeout(function() { void renderSearchResults(); }, 120);
  }

  async function renderSearchResults() {
    var overlay = document.querySelector('[data-testid="wolai-search-modal"]');
    if (!(overlay instanceof HTMLElement)) return;
    var input = overlay.querySelector('[data-testid="wolai-search-input"]');
    var options = overlay.querySelector('[data-testid="wolai-search-options"]');
    var meta = overlay.querySelector('[data-testid="wolai-search-result-meta"]');
    var results = overlay.querySelector('[data-testid="wolai-search-results"]');
    if (!input || !meta || !results) return;
    var query = searchText(input.value);
    if (!query) {
      activeSearchRequestId += 1;
      renderSearchRecentState(overlay);
      return;
    }
    if (options instanceof HTMLElement) options.hidden = false;
    var requestId = ++activeSearchRequestId;
    meta.innerHTML = '<span>搜索中…</span><span>Ctrl + Enter 新窗口打开 / Alt + Enter 右侧边栏打开</span>';
    results.innerHTML = '<div class="wolai-search-empty" data-search-loading="true">搜索中…</div>';
    try {
      var response = await fetch('/api/search/documents', {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({
          workspaceId: resolveWorkspaceId(document.body),
          documentId: currentDocumentId() || null,
          query: query,
          limit: 30,
          filters: {
            titleOnly: searchSwitchValue(overlay, 'title'),
            exact: searchSwitchValue(overlay, 'exact'),
            includeOcr: false,
            onlyCurrentPage: searchSwitchValue(overlay, 'page'),
            timeRange: 'any',
            timeField: 'updated'
          }
        })
      });
      var payload = await response.json();
      if (requestId !== activeSearchRequestId) return;
      var items = Array.isArray(payload.results) ? payload.results : [];
      meta.innerHTML = '<span>共 ' + items.length + ' 条匹配结果</span><span>Ctrl + Enter 新窗口打开 / Alt + Enter 右侧边栏打开</span>';
      if (!items.length) {
        results.innerHTML = '<div class="wolai-search-empty" data-search-empty="true">没有搜索结果</div>';
        return;
      }
      results.innerHTML = items.map(function(item) {
        var title = searchText(item.title || item.name || item.documentTitle || '无标题');
        var snippet = searchText(item.snippet || (Array.isArray(item.evidence) && item.evidence[0] && item.evidence[0].snippet) || '');
        var path = searchText(item.path || item.parentTitle || item.workspaceName || currentWorkspaceName());
        return '<button type="button" class="wolai-search-result-row" data-testid="wolai-search-result-row" data-search-result-owner="rust-kernel" data-document-id="' + escapeHtml(item.documentId || item.nodeId || item.id || '') + '">' +
          '<span class="material-symbols-outlined wolai-search-result-icon" data-icon="article" aria-hidden="true"></span>' +
          '<span class="wolai-search-result-main"><span class="wolai-search-result-title">' + highlightSearchTitle(title, query) + '</span>' +
          '<span class="wolai-search-result-snippet">' + (snippet ? highlightedHtml(snippet) : escapeHtml(path)) + '</span></span>' +
        '</button>';
      }).join('');
    } catch (error) {
      if (requestId !== activeSearchRequestId) return;
      meta.innerHTML = '<span>共 0 条匹配结果</span><span>Ctrl + Enter 新窗口打开 / Alt + Enter 右侧边栏打开</span>';
      results.innerHTML = '<div class="wolai-search-empty" data-search-empty="true">没有搜索结果</div>';
    }
  }

  function isSearchModalOpen() {
    var overlay = document.querySelector('[data-testid="wolai-search-modal"]');
    return overlay instanceof HTMLElement && !overlay.hidden;
  }

  function openSearchModal() {
    var overlay = ensureSearchModal();
    overlay.hidden = false;
    document.documentElement.setAttribute('data-mnote-search-modal-open', 'true');
    var input = overlay.querySelector('[data-testid="wolai-search-input"]');
    if (input) input.setAttribute('placeholder', '在 ' + currentWorkspaceName() + ' 中搜索');
    void renderSearchResults();
    if (input) {
      setTimeout(function() { input.focus(); input.select(); }, 0);
    }
  }

  function closeSearchModal() {
    var overlay = document.querySelector('[data-testid="wolai-search-modal"]');
    if (overlay instanceof HTMLElement) overlay.hidden = true;
    document.documentElement.removeAttribute('data-mnote-search-modal-open');
  }

  function toggleSearchModal() {
    if (isSearchModalOpen()) closeSearchModal();
    else openSearchModal();
  }

  function updatePageSettingsTriggerState() {
    var trigger = document.querySelector('[data-testid="wolai-page-settings-trigger"]');
    if (!(trigger instanceof HTMLElement)) return;
    trigger.setAttribute('data-state', pageUiState.pageSettingsOpen ? 'open' : 'closed');
    trigger.setAttribute('aria-expanded', pageUiState.pageSettingsOpen ? 'true' : 'false');
  }

  function updatePageAiTriggerState() {
    var trigger = document.querySelector('[data-testid="wolai-floating-ai"]');
    if (!(trigger instanceof HTMLElement)) return;
    trigger.setAttribute('data-state', pageUiState.pageAiOpen ? 'open' : 'closed');
    trigger.setAttribute('aria-expanded', pageUiState.pageAiOpen ? 'true' : 'false');
  }

  function createPageOptionRow(key, type) {
    var inputType = type || 'checkbox';
    var supported = pageOptionIsSupported(key);
    if (inputType === 'checkbox') {
      return '' +
        '<label class="wolai-page-setting-row' + (supported ? '' : ' is-pending') + '" data-page-setting-row="' + key + '">' +
          '<span class="wolai-page-setting-copy">' +
            '<span class="wolai-page-setting-label">' + escapeHtml(pageOptionDescription(key)) + '</span>' +
            '<span class="wolai-page-setting-hint">' + escapeHtml(pageOptionHint(key)) + '</span>' +
          '</span>' +
          '<input type="checkbox" class="wolai-page-setting-checkbox" data-page-option-checkbox="' + key + '"' + (supported ? '' : ' data-setting-pending="true"') + ' />' +
        '</label>';
    }
    return '' +
      '<label class="wolai-page-setting-row" data-page-setting-row="' + key + '">' +
        '<span class="wolai-page-setting-copy">' +
          '<span class="wolai-page-setting-label">' + escapeHtml(pageOptionDescription(key)) + '</span>' +
          '<span class="wolai-page-setting-hint">' + escapeHtml(pageOptionHint(key)) + '</span>' +
        '</span>' +
        '<select class="wolai-page-setting-select" data-page-option-select="' + key + '">' +
          '<option value="compact">紧凑</option>' +
          '<option value="normal">默认</option>' +
          '<option value="spacious">宽松</option>' +
        '</select>' +
      '</label>';
  }

  function createGlobalHeadingNumbersRow() {
    return '' +
      '<label class="wolai-page-setting-row" data-page-setting-row="globalShowHeadingNumbers">' +
        '<span class="wolai-page-setting-copy">' +
          '<span class="wolai-page-setting-label">标题自动编号</span>' +
        '</span>' +
        '<input type="checkbox" class="wolai-page-setting-checkbox" data-global-option-checkbox="showHeadingNumbers" />' +
      '</label>';
  }

  function renderGlobalOptions(popover) {
    var globalHeadingNumbers = readGlobalShowHeadingNumbers();
    popover.querySelectorAll('[data-global-option-checkbox="showHeadingNumbers"]').forEach(function(input) {
      input.checked = globalHeadingNumbers;
    });
  }

  function createPageFontRow() {
    return '' +
      '<label class="wolai-page-setting-row" data-page-setting-row="pageFont">' +
        '<span class="wolai-page-setting-copy">' +
          '<span class="wolai-page-setting-label">页面字体</span>' +
          '<span class="wolai-page-setting-hint">已接通：仅对当前页面生效</span>' +
        '</span>' +
        '<select class="wolai-page-setting-select" data-page-option-select="pageFont">' +
          '<option value="default">默认</option>' +
          '<option value="song">宋体</option>' +
          '<option value="kai">楷体</option>' +
        '</select>' +
      '</label>';
  }

  function ensurePageHistoryDrawer() {
    var existing = document.querySelector('[data-testid="wolai-page-history-drawer"]');
    if (existing instanceof HTMLElement) return existing;
    var drawer = document.createElement('aside');
    drawer.className = 'wolai-page-history-drawer';
    drawer.setAttribute('data-testid', 'wolai-page-history-drawer');
    drawer.setAttribute('data-mnote-surface', 'page-history');
    drawer.hidden = true;
    drawer.innerHTML = '' +
      '<div class="wolai-page-history-panel">' +
        '<div class="wolai-page-history-header">' +
          '<div><div class="wolai-page-history-title">页面历史</div><div class="wolai-page-history-subtitle">当前会话内最近保存的 15 个快照。</div></div>' +
          '<button type="button" class="wolai-surface-close" data-page-history-action="close" aria-label="关闭页面历史">×</button>' +
        '</div>' +
        '<div class="wolai-page-history-list" data-page-history-list></div>' +
      '</div>';
    drawer.addEventListener('click', function(event) {
      if (event.target === drawer) closePageHistoryDrawer();
    });
    document.body.appendChild(drawer);
    return drawer;
  }

  function renderPageHistoryDrawer() {
    var drawer = ensurePageHistoryDrawer();
    var list = drawer.querySelector('[data-page-history-list]');
    if (!(list instanceof HTMLElement)) return;
    ensureHistorySnapshotsSeeded();
    list.innerHTML = pageUiState.historySnapshots.length
      ? pageUiState.historySnapshots.map(function(snapshot) {
          var stats = snapshot.stats || {};
          var label = new Date(snapshot.timestamp).toLocaleString('zh-CN', {
            year: 'numeric',
            month: '2-digit',
            day: '2-digit',
            hour: '2-digit',
            minute: '2-digit'
          });
          return '' +
            '<div class="wolai-page-history-item">' +
              '<div class="wolai-page-history-item-copy">' +
                '<div class="wolai-page-history-item-title">' + escapeHtml(label) + '</div>' +
                '<div class="wolai-page-history-item-meta">字数 ' + Number(stats.wordCount || 0) + ' · 字符 ' + Number(stats.characterCount || 0) + ' · 块数 ' + Number(stats.blockCount || 0) + '</div>' +
              '</div>' +
              '<button type="button" class="wolai-page-history-item-ghost" data-page-history-action="noop">仅查看</button>' +
            '</div>';
        }).join('')
      : '<div class="wolai-page-history-empty">尚未产生历史快照，编辑后会自动生成。</div>';
  }

  function openPageHistoryDrawer() {
    renderPageHistoryDrawer();
    var drawer = ensurePageHistoryDrawer();
    drawer.hidden = false;
    document.documentElement.setAttribute('data-mnote-page-history-open', 'true');
  }

  function closePageHistoryDrawer() {
    var drawer = document.querySelector('[data-testid="wolai-page-history-drawer"]');
    if (drawer instanceof HTMLElement) drawer.hidden = true;
    document.documentElement.removeAttribute('data-mnote-page-history-open');
  }

  function ensurePageShareDialog() {
    var existing = document.querySelector('[data-testid="wolai-page-share-dialog"]');
    if (existing instanceof HTMLElement) return existing;
    var dialog = document.createElement('div');
    dialog.className = 'wolai-page-share-dialog';
    dialog.setAttribute('data-testid', 'wolai-page-share-dialog');
    dialog.setAttribute('data-mnote-surface', 'page-share');
    dialog.hidden = true;
    dialog.innerHTML = '' +
      '<div class="wolai-page-share-card" role="dialog" aria-modal="true">' +
        '<div class="wolai-page-share-header">' +
          '<div><div class="wolai-page-share-title">公开分享页面</div><div class="wolai-page-share-subtitle">当前 3000 公开入口由 mnote-web 持有。</div></div>' +
          '<button type="button" class="wolai-surface-close" data-page-share-action="close" aria-label="关闭公开分享页面">×</button>' +
        '</div>' +
        '<div class="wolai-page-share-state">' +
          '<span class="wolai-public-pill wolai-public-pill--inline">全网公开</span>' +
          '<span class="wolai-page-share-copy">任何拥有链接的人都可以访问当前页面。</span>' +
        '</div>' +
        '<div class="wolai-page-share-url-row">' +
          '<input class="wolai-page-share-url" type="text" readonly data-page-share-url value="" />' +
          '<button type="button" class="wolai-page-share-copy-button" data-page-share-action="copy-link">复制链接</button>' +
        '</div>' +
        '<div class="wolai-page-share-footer">更多共享者、群组公开和权限策略仍待接线。</div>' +
      '</div>';
    dialog.addEventListener('click', function(event) {
      if (event.target === dialog) closePageShareDialog();
    });
    document.body.appendChild(dialog);
    return dialog;
  }

  function openPageShareDialog() {
    var dialog = ensurePageShareDialog();
    var input = dialog.querySelector('[data-page-share-url]');
    if (input instanceof HTMLInputElement) input.value = window.location.href;
    dialog.hidden = false;
    document.documentElement.setAttribute('data-mnote-page-share-open', 'true');
  }

  function closePageShareDialog() {
    var dialog = document.querySelector('[data-testid="wolai-page-share-dialog"]');
    if (dialog instanceof HTMLElement) dialog.hidden = true;
    document.documentElement.removeAttribute('data-mnote-page-share-open');
  }

  function pageAiSuggestions() {
    var title = searchText(document.querySelector('[data-page-title-current="true"]')?.textContent) || '当前页面';
    return [
      '帮我总结《' + title + '》当前内容',
      '把当前页面改写得更简洁一些',
      '提炼当前页的关键待办和行动项',
      '基于当前页内容生成一个三段式摘要'
    ];
  }

  function pageAiStorageKey() {
    return 'hermes_page_ai_session:' + currentDocumentId();
  }

  function pageAiNewSession(title) {
    var now = Date.now();
    return {
      id: 'sess_' + now + '_' + Math.random().toString(16).slice(2, 8),
      title: title || '新会话',
      profile: pageAiCurrentProfile(),
      createdAt: now,
      updatedAt: now,
      messages: []
    };
  }

  function pageAiNormalizeSessions(sessions) {
    return (Array.isArray(sessions) ? sessions : [])
      .slice(0, 20)
      .map(function(session) {
        return {
          id: String(session && session.id || '').trim() || pageAiNewSession().id,
          title: String(session && session.title || '').trim() || '新会话',
          profile: String(session && session.profile || pageAiCurrentProfile()).trim() || 'default',
          createdAt: Number(session && session.createdAt || Date.now()),
          updatedAt: Number(session && session.updatedAt || Date.now()),
          messages: Array.isArray(session && session.messages) ? session.messages.slice(-40) : []
        };
      })
      .sort(function(a, b) {
        return Number(b.updatedAt || 0) - Number(a.updatedAt || 0);
      });
  }

  function pageAiLoadSessions() {
    if (pageUiState.pageAiSessions.length && pageUiState.pageAiActiveSessionId) return;
    try {
      var raw = window.localStorage.getItem(pageAiStorageKey());
      var parsed = raw ? JSON.parse(raw) : null;
      var activeId = String(parsed && parsed.activeSessionId || '').trim();
      var activeProfile = String(parsed && parsed.activeProfileName || '').trim();
      if (activeProfile) pageAiSetActiveProfile(activeProfile);
      if (activeId) {
        pageUiState.pageAiSessions = [pageAiNewSession('当前页问答')];
        pageUiState.pageAiSessions[0].id = activeId;
        pageUiState.pageAiActiveSessionId = activeId;
        pageUiState.pageAiMessages = [];
        return;
      }
    } catch (_) {}
    var fresh = pageAiNewSession();
    pageUiState.pageAiSessions = [fresh];
    pageUiState.pageAiActiveSessionId = fresh.id;
    pageUiState.pageAiMessages = [];
  }

  function pageAiPersistSessions() {
    document.documentElement.setAttribute('data-mnote-page-ai-session-owner', 'hermes');
    document.documentElement.setAttribute('data-mnote-page-ai-session-key', pageAiStorageKey());
    try {
      window.localStorage.setItem(pageAiStorageKey(), JSON.stringify({
        activeSessionId: pageUiState.pageAiActiveSessionId,
        activeProfileName: pageAiCurrentProfile()
      }));
    } catch (_) {}
  }

  async function pageAiEnsureHermesSession(forceCreate) {
    pageAiLoadSessions();
    var current = pageAiCurrentSession();
    if (!forceCreate && current && String(current.id || '').startsWith('mnote_') && current.profile === pageAiCurrentProfile()) return current;
    var response = await fetch('/api/hermes/client/sessions', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({
        workspaceId: resolveWorkspaceId(document.body),
        documentId: currentDocumentId(),
        traceId: 'page-ai-' + Date.now().toString(36),
        profile: pageAiCurrentProfile(),
        title: current && current.title ? current.title : '当前页问答'
      })
    });
    var payload = await response.json().catch(function(){ return null; });
    if (!response.ok || !payload || payload.ok !== true) {
      var code = payload && payload.code ? payload.code : 'hermes_session_failed_' + response.status;
      throw new Error(code);
    }
    var session = {
      id: String(payload.sessionId || '').trim(),
      title: String(payload.title || '当前页问答'),
      profile: String(payload.profile || pageAiCurrentProfile()).trim() || 'default',
      createdAt: Date.now(),
      updatedAt: Date.now(),
      messages: pageUiState.pageAiMessages.slice()
    };
    pageUiState.pageAiSessions = pageAiNormalizeSessions([session]);
    pageUiState.pageAiActiveSessionId = session.id;
    pageAiPersistSessions();
    renderPageAiConversation();
    return session;
  }

  async function pageAiRestoreHermesSession() {
    var current = pageAiCurrentSession();
    if (!current || !String(current.id || '').startsWith('mnote_')) return;
    var response = await fetch('/api/hermes/client/sessions/' + encodeURIComponent(current.id), {
      headers: { 'accept': 'application/json' }
    });
    if (!response.ok) return;
    var payload = await response.json().catch(function(){ return null; });
    var session = payload && (payload.session || (payload.upstream && payload.upstream.session) || payload);
    var messages = session && Array.isArray(session.messages) ? session.messages : [];
    if (session && (session.profile || session.profileName)) {
      current.profile = String(session.profile || session.profileName || current.profile || pageAiCurrentProfile());
    }
    if (!messages.length) return;
    pageUiState.pageAiMessages = messages.slice(-40).map(function(message) {
      return {
        role: message.role === 'user' ? 'user' : (message.role === 'tool' ? 'tool' : 'assistant'),
        content: String(message.content || '')
      };
    });
    current.messages = pageUiState.pageAiMessages.slice();
    current.updatedAt = Date.now();
    renderPageAiConversation();
  }

  function pageAiCurrentSession() {
    return pageUiState.pageAiSessions.find(function(session) {
      return session.id === pageUiState.pageAiActiveSessionId;
    }) || null;
  }

  function pageAiSetActiveSession(sessionId) {
    var session = pageUiState.pageAiSessions.find(function(item) { return item.id === sessionId; });
    if (!session) return;
    pageUiState.pageAiActiveSessionId = session.id;
    pageUiState.pageAiMessages = Array.isArray(session.messages) ? session.messages.slice() : [];
    pageUiState.pageAiPage = 'chat';
    pageAiPersistSessions();
    renderPageAiControls();
    renderPageAiConversation();
  }

  function pageAiStartNewSession() {
    var session = pageAiNewSession();
    pageUiState.pageAiSessions = pageAiNormalizeSessions([session].concat(pageUiState.pageAiSessions));
    pageUiState.pageAiActiveSessionId = session.id;
    pageUiState.pageAiMessages = [];
    pageUiState.pageAiPage = 'chat';
    pageAiPersistSessions();
    renderPageAiControls();
    renderPageAiConversation();
  }

  function pageAiProviderLabel(provider) {
    if (provider === 'codex') return 'Codex';
    if (provider === 'claudecode') return 'ClaudeCode';
    return 'Hermes';
  }

  function pageAiNormalizeArray(value) {
    return Array.isArray(value) ? value : [];
  }

  function pageAiUnwrapUpstream(payload) {
    if (payload && typeof payload === 'object' && payload.upstream) return payload.upstream;
    return payload || null;
  }

  function pageAiProfileValue(profile) {
    if (profile && typeof profile === 'object') {
      return String(profile.name || profile.profile || profile.id || '').trim();
    }
    return String(profile || '').trim();
  }

  function pageAiCurrentProfile() {
    var active = String(pageUiState.pageAiActiveProfileName || '').trim();
    if (active) return active;
    var selected = pageUiState.pageAiProfiles.find(function(profile) {
      return profile && profile.active;
    });
    return pageAiProfileValue(selected) || 'default';
  }

  function pageAiNormalizeProfiles(payload) {
    var upstream = pageAiUnwrapUpstream(payload);
    var profiles = pageAiNormalizeArray(upstream && upstream.profiles ? upstream.profiles : upstream);
    return profiles.map(function(profile) {
      return {
        name: pageAiProfileValue(profile) || 'default',
        active: Boolean(profile && profile.active),
        model: String(profile && profile.model || '').trim(),
        gateway: String(profile && profile.gateway || '').trim(),
        alias: String(profile && profile.alias || '').trim()
      };
    });
  }

  function pageAiNormalizeSkills(payload) {
    var upstream = pageAiUnwrapUpstream(payload);
    var categories = pageAiNormalizeArray(upstream && upstream.categories ? upstream.categories : []);
    var archived = pageAiNormalizeArray(upstream && upstream.archived ? upstream.archived : []);
    return {
      categories: categories.map(function(category) {
        return {
          name: String(category && category.name || '').trim() || 'misc',
          description: String(category && category.description || '').trim(),
          skills: pageAiNormalizeArray(category && category.skills).map(function(skill) {
            return {
              name: String(skill && skill.name || '').trim(),
              description: String(skill && skill.description || '').trim(),
              enabled: skill && skill.enabled !== false,
              source: String(skill && skill.source || 'local').trim() || 'local',
              modified: Boolean(skill && skill.modified)
            };
          })
        };
      }),
      archived: archived.map(function(skill) {
        return {
          name: String(skill && skill.name || '').trim(),
          description: String(skill && skill.description || '').trim(),
          enabled: skill && skill.enabled !== false,
          source: String(skill && skill.source || 'local').trim() || 'local',
          modified: Boolean(skill && skill.modified)
        };
      })
    };
  }

  function pageAiSkillListEntries() {
    var result = [];
    pageAiNormalizeArray(pageUiState.pageAiSkills.categories).forEach(function(category) {
      pageAiNormalizeArray(category.skills).forEach(function(skill) {
        result.push({
          category: category.name,
          name: skill.name,
          description: skill.description,
          enabled: skill.enabled !== false,
          source: skill.source || 'local',
          modified: Boolean(skill.modified)
        });
      });
    });
    return result.concat(pageAiNormalizeArray(pageUiState.pageAiSkills.archived));
  }

  function pageAiSetActiveProfile(profileName) {
    var next = String(profileName || '').trim() || 'default';
    pageUiState.pageAiActiveProfileName = next;
    document.documentElement.setAttribute('data-mnote-page-ai-profile', next);
  }

  function pageAiSetRunStatus(status, runId) {
    pageUiState.pageAiRunStatus = status || 'idle';
    pageUiState.pageAiCurrentRunId = runId || pageUiState.pageAiCurrentRunId || '';
    document.documentElement.setAttribute('data-mnote-page-ai-run-status', pageUiState.pageAiRunStatus);
    if (pageUiState.pageAiCurrentRunId) {
      document.documentElement.setAttribute('data-mnote-page-ai-run-id', pageUiState.pageAiCurrentRunId);
    }
  }

  function pageAiSetContextScope(scope) {
    var next = String(scope || '').trim() || 'page';
    pageUiState.pageAiContextScope = next;
    document.documentElement.setAttribute('data-mnote-page-ai-context-scope', next);
  }

  function pageAiRunStatusLabel(status) {
    if (status === 'queued') return '排队中';
    if (status === 'running') return '运行中';
    if (status === 'tool_calling') return '调用工具';
    if (status === 'completed') return '已完成';
    if (status === 'failed') return '失败';
    if (status === 'aborted') return '已停止';
    return '空闲';
  }

  function pageAiMemoryFileLabel(section) {
    if (section === 'soul') return 'SOUL.md';
    if (section === 'user') return 'USER.md';
    return 'MEMORY.md';
  }

  function pageAiContextScopeLabel(scope) {
    if (scope === 'selection') return '当前选区';
    if (scope === 'block') return '当前块';
    if (scope === 'options') return '页面设置';
    return '当前页';
  }

  function pageAiSetDraftForSection(section, value) {
    pageUiState.pageAiProfileMemoryDrafts[section] = String(value == null ? '' : value);
  }

  function pageAiApplyProfileMemory(payload) {
    var upstream = pageAiUnwrapUpstream(payload) || {};
    pageUiState.pageAiProfileMemory = {
      memory: String(upstream.memory || ''),
      user: String(upstream.user || ''),
      soul: String(upstream.soul || '')
    };
    pageUiState.pageAiProfileMemoryDrafts = {
      memory: pageUiState.pageAiProfileMemory.memory,
      user: pageUiState.pageAiProfileMemory.user,
      soul: pageUiState.pageAiProfileMemory.soul
    };
  }

  function pageAiFilteredSkillEntries() {
    var query = String(pageUiState.pageAiSkillQuery || '').trim().toLowerCase();
    return pageAiSkillListEntries().filter(function(skill) {
      if (!query) return true;
      return String(skill.name || '').toLowerCase().indexOf(query) >= 0
        || String(skill.description || '').toLowerCase().indexOf(query) >= 0
        || String(skill.category || '').toLowerCase().indexOf(query) >= 0;
    });
  }

  async function pageAiLoadProfiles() {
    try {
      var response = await fetch('/api/hermes/client/profiles', {
        headers: { 'accept': 'application/json' }
      });
      var payload = await response.json().catch(function(){ return null; });
      if (!response.ok) throw new Error(payload && payload.code ? payload.code : 'profiles_failed_' + response.status);
      var profiles = pageAiNormalizeProfiles(payload);
      pageUiState.pageAiProfiles = profiles.length ? profiles : [{ name: 'default', active: true }];
      var active = pageAiProfileValue(pageUiState.pageAiProfiles.find(function(profile) { return profile.active; }));
      pageAiSetActiveProfile(active || pageAiCurrentProfile());
      pageUiState.pageAiProfileError = '';
    } catch (error) {
      pageUiState.pageAiProfileError = error instanceof Error ? error.message : String(error);
      if (!pageUiState.pageAiProfiles.length) pageUiState.pageAiProfiles = [{ name: 'default', active: true }];
      pageAiSetActiveProfile(pageAiCurrentProfile());
    }
    renderPageAiProviderButtons();
    renderPageAiControls();
  }

  async function pageAiSwitchProfile(profileName) {
    var next = String(profileName || '').trim();
    if (!next) return;
    pageAiSetActiveProfile(next);
    renderPageAiProviderButtons();
    renderPageAiControls();
    try {
      var response = await fetch('/api/hermes/client/profiles/active', {
        method: 'PUT',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({ name: next })
      });
      var payload = await response.json().catch(function(){ return null; });
      if (!response.ok) throw new Error(payload && payload.code ? payload.code : 'profile_switch_failed_' + response.status);
      pageUiState.pageAiSessions = pageUiState.pageAiSessions.filter(function(session) {
        return session && session.profile === next;
      });
      pageUiState.pageAiActiveSessionId = '';
      pageUiState.pageAiMessages = [];
      pageAiPersistSessions();
      await pageAiEnsureHermesSession(true);
      await pageAiLoadProfileMemory();
      await pageAiLoadSkills();
      renderPageAiControls();
    } catch (error) {
      pageUiState.pageAiProfileError = error instanceof Error ? error.message : String(error);
    }
    renderPageAiConversation();
  }

  async function pageAiLoadProfileMemory() {
    try {
      var response = await fetch('/api/hermes/client/profile-memory?profile=' + encodeURIComponent(pageAiCurrentProfile()), {
        headers: { 'accept': 'application/json' }
      });
      var payload = await response.json().catch(function(){ return null; });
      if (!response.ok) throw new Error(payload && payload.code ? payload.code : 'profile_memory_failed_' + response.status);
      pageAiApplyProfileMemory(payload);
      pageUiState.pageAiProfileMemoryError = '';
    } catch (error) {
      pageUiState.pageAiProfileMemoryError = error instanceof Error ? error.message : String(error);
    }
    renderPageAiControls();
    renderPageAiConversation();
  }

  async function pageAiSaveProfileMemory(section) {
    var normalized = String(section || '').trim();
    if (['memory', 'user', 'soul'].indexOf(normalized) < 0) return;
    var content = String(pageUiState.pageAiProfileMemoryDrafts[normalized] || '');
    try {
      var response = await fetch('/api/hermes/client/profile-memory', {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({
          profile: pageAiCurrentProfile(),
          section: normalized,
          content: content
        })
      });
      var payload = await response.json().catch(function(){ return null; });
      if (!response.ok) throw new Error(payload && payload.code ? payload.code : 'profile_memory_save_failed_' + response.status);
      pageUiState.pageAiProfileMemory[normalized] = content;
      pageUiState.pageAiProfileMemoryError = '';
      document.documentElement.setAttribute('data-mnote-page-ai-memory-saved', normalized);
    } catch (error) {
      pageUiState.pageAiProfileMemoryError = error instanceof Error ? error.message : String(error);
    }
    renderPageAiControls();
    renderPageAiConversation();
  }

  async function pageAiLoadSkills() {
    try {
      var response = await fetch('/api/hermes/client/skills?profile=' + encodeURIComponent(pageAiCurrentProfile()), {
        headers: { 'accept': 'application/json' }
      });
      var payload = await response.json().catch(function(){ return null; });
      if (!response.ok) throw new Error(payload && payload.code ? payload.code : 'skills_failed_' + response.status);
      pageUiState.pageAiSkills = pageAiNormalizeSkills(payload);
      pageUiState.pageAiSkillError = '';
    } catch (error) {
      pageUiState.pageAiSkillError = error instanceof Error ? error.message : String(error);
      pageUiState.pageAiSkills = { categories: [], archived: [] };
    }
    renderPageAiControls();
    renderPageAiConversation();
  }

  async function pageAiToggleSkill(skillName, enabled) {
    var name = String(skillName || '').trim();
    if (!name) return;
    var previous = null;
    pageAiSkillListEntries().forEach(function(skill) {
      if (skill.name === name && previous == null) previous = skill.enabled !== false;
    });
    try {
      var response = await fetch('/api/hermes/client/skills/toggle', {
        method: 'PUT',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({
          profile: pageAiCurrentProfile(),
          name: name,
          enabled: Boolean(enabled)
        })
      });
      var payload = await response.json().catch(function(){ return null; });
      if (!response.ok) throw new Error(payload && payload.code ? payload.code : 'skill_toggle_failed_' + response.status);
      pageAiNormalizeArray(pageUiState.pageAiSkills.categories).forEach(function(category) {
        pageAiNormalizeArray(category.skills).forEach(function(skill) {
          if (skill.name === name) skill.enabled = Boolean(enabled);
        });
      });
      document.documentElement.setAttribute('data-mnote-page-ai-skill-toggled', name);
      pageUiState.pageAiSkillError = '';
    } catch (error) {
      pageUiState.pageAiSkillError = error instanceof Error ? error.message : String(error);
      if (previous != null) {
        pageAiNormalizeArray(pageUiState.pageAiSkills.categories).forEach(function(category) {
          pageAiNormalizeArray(category.skills).forEach(function(skill) {
            if (skill.name === name) skill.enabled = previous;
          });
        });
      }
    }
    renderPageAiControls();
    renderPageAiConversation();
  }

  function renderPageAiProviderButtons() {
    var drawer = ensurePageAiDrawer();
    drawer.querySelectorAll('[data-page-ai-provider]').forEach(function(button) {
      var provider = button.getAttribute('data-page-ai-provider') || 'hermes';
      var active = provider === pageUiState.pageAiProvider;
      button.classList.toggle('is-active', active);
      button.setAttribute('aria-pressed', active ? 'true' : 'false');
    });
    var subtitle = drawer.querySelector('.wolai-page-ai-subtitle');
    if (subtitle instanceof HTMLElement) {
      subtitle.textContent = '当前页面上下文优先 · ' + pageAiProviderLabel(pageUiState.pageAiProvider);
    }
  }

  function renderPageAiControls() {
    var drawer = ensurePageAiDrawer();
    var activeProfile = pageAiCurrentProfile();
    var profileSelect = drawer.querySelector('[data-page-ai-profile-select]');
    if (profileSelect instanceof HTMLSelectElement) {
      var profiles = pageUiState.pageAiProfiles.length ? pageUiState.pageAiProfiles : [{ name: activeProfile, active: true }];
      profileSelect.innerHTML = profiles.map(function(profile) {
        var name = pageAiProfileValue(profile) || 'default';
        var label = profile.alias ? name + ' · ' + profile.alias : name;
        return '<option value="' + escapeHtml(name) + '"' + (name === activeProfile ? ' selected' : '') + '>' + escapeHtml(label) + '</option>';
      }).join('');
      profileSelect.value = activeProfile;
    }
    var runStatus = drawer.querySelector('[data-page-ai-run-status]');
    if (runStatus instanceof HTMLElement) runStatus.textContent = pageAiRunStatusLabel(pageUiState.pageAiRunStatus);
    var sessionNode = drawer.querySelector('[data-page-ai-session-status]');
    if (sessionNode instanceof HTMLElement) {
      var session = pageAiCurrentSession();
      sessionNode.textContent = session && session.id ? String(session.title || '当前页问答') : '等待 Hermes session';
    }
    var scopeSelect = drawer.querySelector('[data-page-ai-context-scope]');
    if (scopeSelect instanceof HTMLSelectElement) scopeSelect.value = pageUiState.pageAiContextScope;
    var scopeLabel = drawer.querySelector('[data-page-ai-context-scope-label]');
    if (scopeLabel instanceof HTMLElement) scopeLabel.textContent = pageAiContextScopeLabel(pageUiState.pageAiContextScope);
    drawer.querySelectorAll('[data-page-ai-tab]').forEach(function(button) {
      var target = button.getAttribute('data-page-ai-tab') || 'chat';
      var active = target === pageUiState.pageAiPage;
      button.classList.toggle('is-active', active);
      button.setAttribute('aria-selected', active ? 'true' : 'false');
    });
    drawer.querySelectorAll('[data-page-ai-panel]').forEach(function(panel) {
      if (panel instanceof HTMLElement) {
        panel.hidden = panel.getAttribute('data-page-ai-panel') !== pageUiState.pageAiPage;
      }
    });
    var profileError = drawer.querySelector('[data-page-ai-profile-error]');
    if (profileError instanceof HTMLElement) {
      profileError.textContent = pageUiState.pageAiProfileError || '';
      profileError.hidden = !pageUiState.pageAiProfileError;
    }
    var memoryError = drawer.querySelector('[data-page-ai-memory-error]');
    if (memoryError instanceof HTMLElement) {
      memoryError.textContent = pageUiState.pageAiProfileMemoryError || '';
      memoryError.hidden = !pageUiState.pageAiProfileMemoryError;
    }
    var agentPanel = drawer.querySelector('[data-page-ai-agent-panel]');
    if (agentPanel instanceof HTMLElement) {
      agentPanel.innerHTML = ['soul', 'user', 'memory'].map(function(section) {
        var label = pageAiMemoryFileLabel(section);
        var value = pageUiState.pageAiProfileMemoryDrafts[section] || '';
        return '' +
          '<section class="wolai-page-ai-memory-card">' +
            '<div class="wolai-page-ai-memory-head">' +
              '<div>' +
                '<div class="wolai-page-ai-memory-title">' + escapeHtml(label) + '</div>' +
                '<div class="wolai-page-ai-memory-scope">保存到 Hermes profile: ' + escapeHtml(activeProfile) + '</div>' +
              '</div>' +
              '<button type="button" class="wolai-page-ai-ghost" data-page-ai-memory-save="' + escapeHtml(section) + '">保存</button>' +
            '</div>' +
            '<textarea class="wolai-page-ai-memory-editor" data-page-ai-memory-editor="' + escapeHtml(section) + '" spellcheck="false">' + escapeHtml(value) + '</textarea>' +
          '</section>';
      }).join('');
    }
    var skillError = drawer.querySelector('[data-page-ai-skill-error]');
    if (skillError instanceof HTMLElement) {
      skillError.textContent = pageUiState.pageAiSkillError || '';
      skillError.hidden = !pageUiState.pageAiSkillError;
    }
    var skillSearch = drawer.querySelector('[data-page-ai-skill-search]');
    if (skillSearch instanceof HTMLInputElement && document.activeElement !== skillSearch) {
      skillSearch.value = pageUiState.pageAiSkillQuery;
    }
    var skillList = drawer.querySelector('[data-page-ai-skill-list]');
    if (skillList instanceof HTMLElement) {
      var skills = pageAiFilteredSkillEntries();
      if (!skills.length) {
        skillList.innerHTML = '<div class="wolai-page-ai-empty">没有匹配的 Hermes skill。</div>';
      } else {
        skillList.innerHTML = skills.map(function(skill) {
          var source = skill.source || 'local';
          var sourceText = source + (skill.modified ? ' · modified' : '');
          return '' +
            '<div class="wolai-page-ai-skill-row">' +
              '<div class="wolai-page-ai-skill-copy">' +
                '<div class="wolai-page-ai-skill-name">' + escapeHtml(skill.name) + '</div>' +
                '<div class="wolai-page-ai-skill-desc">' + escapeHtml(skill.description || '无描述') + '</div>' +
                '<div class="wolai-page-ai-skill-source">' + escapeHtml(sourceText) + '</div>' +
              '</div>' +
              '<button type="button" class="wolai-page-ai-skill-switch' + (skill.enabled !== false ? ' is-on' : '') + '" data-page-ai-skill-toggle="' + escapeHtml(skill.name) + '" aria-pressed="' + (skill.enabled !== false ? 'true' : 'false') + '">' +
                '<span></span>' +
              '</button>' +
            '</div>';
        }).join('');
      }
    }
  }

  function humanizePageAiResponse(rawText, promptText) {
    var providerLabel = pageAiProviderLabel(pageUiState.pageAiProvider);
    var text = String(rawText || '').trim();
    if (!text) {
      return providerLabel + ' 已收到你的问题“' + searchText(promptText) + '”，但这次没有返回可读内容。';
    }
    if (text.startsWith('{')) {
      try {
        var payload = JSON.parse(text);
        var operation = payload && payload.operation ? payload.operation : {};
        var normalized = operation && operation.normalized_input ? operation.normalized_input : {};
        var args = normalized && normalized.args ? normalized.args : {};
        var documentId = searchText(args.documentId || args.pageId || currentDocumentId());
        var toolName = searchText(operation.tool_name || normalized.toolName || 'doc_get');
        return providerLabel + ' 已收到你的问题“' + searchText(promptText) + '”。当前已通过 Hermes 调用 mnote plugin 工具，目标页面是 ' + (documentId || '当前页面') + '，本次选择的工具是 ' + toolName + '。如果你继续追问，我会沿当前页面上下文继续回复。';
      } catch (_) {
        return providerLabel + ' 已返回结果，但当前结果是结构化文本，已为你保留原始内容。';
      }
    }
    return text;
  }

  function ensurePageAiDrawer() {
    var existing = document.querySelector('[data-testid="wolai-page-ai-drawer"]');
    if (existing instanceof HTMLElement) return existing;
    var drawer = document.createElement('aside');
    drawer.className = 'wolai-page-ai-drawer';
    drawer.setAttribute('data-testid', 'wolai-page-ai-drawer');
    drawer.setAttribute('data-mnote-surface', 'page-ai');
    drawer.hidden = true;
    drawer.innerHTML = '' +
      '<div class="wolai-page-ai-panel" role="dialog" aria-modal="false">' +
        '<div class="wolai-page-ai-header">' +
          '<div class="wolai-page-ai-header-copy">' +
            '<h2 class="wolai-page-ai-title" data-title="page-ai-title">页面 AI</h2>' +
            '<div class="wolai-page-ai-subtitle">当前页面上下文优先 · Hermes</div>' +
          '</div>' +
          '<button type="button" class="wolai-surface-close" data-page-ai-action="close" aria-label="关闭页面 AI">×</button>' +
        '</div>' +
        '<div class="wolai-page-ai-statusbar">' +
          '<div class="wolai-page-ai-statusitem">' +
            '<span class="wolai-page-ai-statuslabel">会话</span>' +
            '<span class="wolai-page-ai-statusvalue" data-page-ai-session-status>等待 Hermes session</span>' +
          '</div>' +
          '<div class="wolai-page-ai-statusitem">' +
            '<span class="wolai-page-ai-statuslabel">状态</span>' +
            '<span class="wolai-page-ai-statusvalue" data-page-ai-run-status>空闲</span>' +
          '</div>' +
          '<div class="wolai-page-ai-statusitem">' +
            '<span class="wolai-page-ai-statuslabel">上下文</span>' +
            '<span class="wolai-page-ai-statusvalue" data-page-ai-context-scope-label>当前页</span>' +
          '</div>' +
          '<a class="wolai-page-ai-settings-link" href="/auth" target="_blank" rel="noreferrer">Hermes 设置</a>' +
        '</div>' +
        '<div class="wolai-page-ai-topbar">' +
          '<label class="wolai-page-ai-profile-select">' +
            '<span>agent / profile</span>' +
            '<select data-page-ai-profile-select></select>' +
          '</label>' +
          '<label class="wolai-page-ai-context-select">' +
            '<span>上下文</span>' +
            '<select data-page-ai-context-scope>' +
              '<option value="page">当前页</option>' +
              '<option value="selection">当前选区</option>' +
              '<option value="block">当前块</option>' +
              '<option value="options">页面设置</option>' +
            '</select>' +
          '</label>' +
        '</div>' +
        '<div class="wolai-page-ai-inline-error" data-page-ai-profile-error hidden></div>' +
        '<div class="wolai-page-ai-body">' +
          '<div class="wolai-page-ai-tabs" role="tablist" aria-label="页面 AI 面板">' +
            '<button type="button" class="wolai-page-ai-tab is-active" data-page-ai-tab="chat" role="tab" aria-selected="true">chat</button>' +
            '<button type="button" class="wolai-page-ai-tab" data-page-ai-tab="agent" role="tab" aria-selected="false">agent</button>' +
            '<button type="button" class="wolai-page-ai-tab" data-page-ai-tab="skills" role="tab" aria-selected="false">skills</button>' +
            '<button type="button" class="wolai-page-ai-tab" data-page-ai-tab="history" role="tab" aria-selected="false">history</button>' +
          '</div>' +
          '<section class="wolai-page-ai-panel-section" data-page-ai-panel="chat">' +
            '<div class="wolai-page-ai-suggestions">' +
              '<div class="wolai-page-ai-suggestions-header">' +
                '<span>推荐问题</span>' +
                '<div class="wolai-page-ai-intents">' +
                  '<button type="button" class="wolai-page-ai-ghost" data-page-ai-intent="create-summary">创建 Summary</button>' +
                  '<button type="button" class="wolai-page-ai-ghost" data-page-ai-intent="create-ai-note">创建 AI Note</button>' +
                  '<button type="button" class="wolai-page-ai-ghost" data-page-ai-action="rotate">换一换</button>' +
                '</div>' +
              '</div>' +
              '<div class="wolai-page-ai-suggestion-list" data-page-ai-suggestion-list></div>' +
            '</div>' +
            '<div class="wolai-page-ai-conversation" data-page-ai-conversation></div>' +
          '</section>' +
          '<section class="wolai-page-ai-panel-section" data-page-ai-panel="agent" hidden>' +
            '<div class="wolai-page-ai-inline-error" data-page-ai-memory-error hidden></div>' +
            '<div class="wolai-page-ai-memory-grid" data-page-ai-agent-panel></div>' +
          '</section>' +
          '<section class="wolai-page-ai-panel-section" data-page-ai-panel="skills" hidden>' +
            '<div class="wolai-page-ai-skills-toolbar">' +
              '<label class="wolai-page-ai-skill-search">' +
                '<span>搜索技能</span>' +
                '<input type="search" data-page-ai-skill-search placeholder="搜索技能…" />' +
              '</label>' +
            '</div>' +
            '<div class="wolai-page-ai-inline-error" data-page-ai-skill-error hidden></div>' +
            '<div class="wolai-page-ai-skill-list" data-page-ai-skill-list></div>' +
          '</section>' +
          '<section class="wolai-page-ai-panel-section" data-page-ai-panel="history" hidden>' +
            '<div class="wolai-page-ai-conversation" data-page-ai-conversation></div>' +
          '</section>' +
        '</div>' +
        '<div class="wolai-page-ai-footer">' +
          '<div class="wolai-page-ai-toolbar">' +
            '<button type="button" class="wolai-page-ai-tab is-active" data-page-ai-action="new-session" aria-label="当前已是新会话">新会话</button>' +
            '<button type="button" class="wolai-page-ai-tab" data-page-ai-action="history">历史会话</button>' +
          '</div>' +
          '<div class="wolai-page-ai-input-row">' +
            '<textarea class="wolai-page-ai-input" data-page-ai-input rows="3" placeholder="问我你想知道的"></textarea>' +
            '<button type="button" class="wolai-page-ai-send" data-page-ai-action="send" aria-label="发送">↑</button>' +
          '</div>' +
        '</div>' +
      '</div>';
    document.body.appendChild(drawer);
    return drawer;
  }

  function renderPageAiSuggestions() {
    var drawer = ensurePageAiDrawer();
    var list = drawer.querySelector('[data-page-ai-panel="chat"] [data-page-ai-suggestion-list]');
    if (!(list instanceof HTMLElement)) return;
    var suggestions = pageAiSuggestions();
    var offset = pageUiState.pageAiSuggestionIndex % suggestions.length;
    var ordered = suggestions.slice(offset).concat(suggestions.slice(0, offset)).slice(0, 3);
    list.innerHTML = ordered.map(function(text) {
      return '<button type="button" class="wolai-page-ai-suggestion" data-page-ai-suggestion="' + escapeHtml(text) + '">' + escapeHtml(text) + '</button>';
    }).join('');
  }

  function renderPageAiConversation() {
    var drawer = ensurePageAiDrawer();
    var conversation = drawer.querySelector('[data-page-ai-panel="' + cssEscape(pageUiState.pageAiPage || 'chat') + '"] [data-page-ai-conversation]');
    if (!(conversation instanceof HTMLElement)) return;
    if (pageUiState.pageAiPage === 'history') {
      if (!pageUiState.pageAiSessions.length) {
        conversation.innerHTML = '<div class="wolai-page-ai-empty">尚未产生历史会话。</div>';
        return;
      }
      conversation.innerHTML = pageUiState.pageAiSessions.map(function(session) {
        var preview = Array.isArray(session.messages) && session.messages.length
          ? session.messages.slice(-1)[0].content
          : '暂无消息';
        var active = session.id === pageUiState.pageAiActiveSessionId;
        return '' +
          '<button type="button" class="wolai-page-ai-message wolai-page-ai-message--history' + (active ? ' is-active' : '') + '" data-page-ai-session="' + escapeHtml(session.id) + '">' +
            '<div class="wolai-page-ai-message-role">会话</div>' +
            '<div class="wolai-page-ai-message-text"><strong>' + escapeHtml(session.title || '新会话') + '</strong><br />' + escapeHtml(preview) + '</div>' +
          '</button>';
      }).join('');
      return;
    }
    if (!pageUiState.pageAiMessages.length) {
      conversation.innerHTML = '<div class="wolai-page-ai-empty">围绕当前页面提问，我会优先使用当前页内容、页面结构和已保存设置。</div>';
      return;
    }
    conversation.innerHTML = pageUiState.pageAiMessages.map(function(item) {
      var roleLabel = item.role === 'user' ? '你' : (item.role === 'tool' ? '工具' : 'AI');
      return '' +
        '<div class="wolai-page-ai-message wolai-page-ai-message--' + escapeHtml(item.role || 'assistant') + '">' +
          '<div class="wolai-page-ai-message-role">' + escapeHtml(roleLabel) + '</div>' +
          '<div class="wolai-page-ai-message-text">' + escapeHtml(item.content || '') + '</div>' +
        '</div>';
    }).join('');
    conversation.scrollTop = conversation.scrollHeight;
  }

  function openPageAiDrawer() {
    pageAiLoadSessions();
    renderPageAiSuggestions();
    renderPageAiConversation();
    renderPageAiProviderButtons();
    renderPageAiControls();
    var drawer = ensurePageAiDrawer();
    drawer.hidden = false;
    pageUiState.pageAiOpen = true;
    updatePageAiTriggerState();
    Promise.all([
      pageAiLoadProfiles(),
      pageAiLoadProfileMemory(),
      pageAiLoadSkills()
    ]).then(function() {
      renderPageAiControls();
    }).catch(function() {}).then(function() {
      return pageAiEnsureHermesSession();
    }).then(function() {
      return pageAiRestoreHermesSession();
    }).then(function() {
      renderPageAiControls();
    }).catch(function(error) {
      pageUiState.pageAiMessages.push({
        role: 'assistant',
        content: 'Hermes 当前不可用：' + (error instanceof Error ? error.message : String(error))
      });
      renderPageAiConversation();
      renderPageAiControls();
    });
  }

  function closePageAiDrawer() {
    var drawer = document.querySelector('[data-testid="wolai-page-ai-drawer"]');
    if (drawer instanceof HTMLElement) drawer.hidden = true;
    pageUiState.pageAiOpen = false;
    updatePageAiTriggerState();
  }

  function isPageAiDrawerOpen() {
    var drawer = document.querySelector('[data-testid="wolai-page-ai-drawer"]');
    return drawer instanceof HTMLElement && !drawer.hidden;
  }

  async function streamPageAiResponse(response, onEvent) {
    if (!response.body || typeof response.body.getReader !== 'function') return;
    var reader = response.body.getReader();
    var decoder = new TextDecoder();
    var buffer = '';
    while (true) {
      var chunk = await reader.read();
      if (chunk.done) break;
      buffer += decoder.decode(chunk.value, { stream: true });
      var frames = buffer.split('\n\n');
      buffer = frames.pop() || '';
      frames.forEach(function(frame) {
        var eventName = '';
        var dataLines = [];
        frame.split('\n').forEach(function(line) {
          if (line.startsWith('event:')) eventName = line.slice(6).trim();
          if (line.startsWith('data:')) dataLines.push(line.slice(5).trim());
        });
        var payloadText = dataLines.join('\n');
        if (!eventName && payloadText) {
          try {
            var parsed = JSON.parse(payloadText);
            eventName = parsed && parsed.event ? String(parsed.event) : '';
          } catch (_) {}
        }
        if (eventName) onEvent(eventName, payloadText);
      });
    }
  }

  async function sendPageAiMessage(text) {
    if (pageUiState.pageAiBusy) return;
    var prompt = searchText(text);
    if (!prompt) return;
    pageUiState.pageAiBusy = true;
    var currentSession = null;
    try {
      await pageAiEnsureHermesSession();
      pageAiSetRunStatus('queued');
      var contextSnapshot = currentPageAiContextSnapshot();
      var aggregate = contextSnapshot.aggregate || {};
      var body = contextSnapshot.body || {};
      var subtree = contextSnapshot.subtree || null;
      var outline = subtree && subtree.outline ? subtree.outline : null;
      pageUiState.pageAiMessages.push({ role: 'user', content: prompt });
      currentSession = pageAiCurrentSession();
      if (currentSession) {
        if (currentSession.title === '新会话') {
          currentSession.title = prompt.length > 18 ? prompt.slice(0, 18) + '…' : prompt;
        }
        currentSession.messages = pageUiState.pageAiMessages.slice();
        currentSession.updatedAt = Date.now();
      }
      renderPageAiConversation();
      var response = await fetch('/api/hermes/client/runs', {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({
          workspaceId: resolveWorkspaceId(document.body),
          documentId: currentDocumentId(),
          sessionId: pageUiState.pageAiActiveSessionId,
          profile: pageAiCurrentProfile(),
          message: prompt,
          model: 'hermes-agent',
          pageContext: {
            documentBlocks: body.content || null,
            node: {
              documentId: currentDocumentId(),
              title: aggregate.head && aggregate.head.title ? aggregate.head.title : ''
            },
            subtree: subtree,
            outline: outline,
            pageSubtreeSource: contextSnapshot.pageSubtreeSource || 'none',
            evidence: null,
            pageOptions: currentPageOptions()
          },
          selectedBlockId: null,
          selectedText: null,
          traceId: 'page-ai-run-' + Date.now().toString(36)
        })
      });
      if (!response.ok) {
        var errorPayload = await response.json().catch(function(){ return null; });
        throw new Error(errorPayload && errorPayload.code ? errorPayload.code : 'page_ai_failed_' + response.status);
      }
      var runPayload = await response.json().catch(function(){ return null; });
      var upstream = runPayload && runPayload.upstream ? runPayload.upstream : runPayload;
      var runId = upstream && (upstream.run_id || upstream.runId);
      if (!runId) throw new Error('hermes_run_missing_run_id');
      pageAiSetRunStatus('running', runId);
      var eventResponse = await fetch('/api/hermes/client/events/' + encodeURIComponent(runId), {
        headers: { 'accept': 'text/event-stream' }
      });
      if (!eventResponse.ok) {
        var eventError = await eventResponse.json().catch(function(){ return null; });
        throw new Error(eventError && eventError.code ? eventError.code : 'hermes_events_failed_' + eventResponse.status);
      }
      var assistantText = '';
      await streamPageAiResponse(eventResponse, function(eventName, payloadText) {
        if (eventName === 'assistant_message' || eventName === 'message.delta') {
          try {
            var payload = JSON.parse(payloadText || 'null');
            assistantText += searchText((payload && (payload.text || payload.delta)) || '');
          } catch (_) {
            assistantText += searchText(payloadText);
          }
        }
        if (eventName === 'run.completed') {
          try {
            var completed = JSON.parse(payloadText || 'null');
            if (completed && completed.output) assistantText = searchText(completed.output);
          } catch (_) {}
          pageAiSetRunStatus('completed', runId);
        }
        if (eventName === 'tool.started' || eventName === 'tool.completed' || eventName === 'tool.failed') {
          try {
            var toolEvent = JSON.parse(payloadText || 'null');
            pageUiState.pageAiMessages.push({
              role: 'tool',
              content: String(toolEvent && (toolEvent.name || toolEvent.tool || eventName) || eventName)
            });
          } catch (_) {
            pageUiState.pageAiMessages.push({ role: 'tool', content: eventName });
          }
          renderPageAiConversation();
          pageAiSetRunStatus('tool_calling', runId);
        }
      });
      pageUiState.pageAiMessages.push({
        role: 'assistant',
        content: humanizePageAiResponse(assistantText, prompt)
      });
      currentSession = pageAiCurrentSession();
      if (currentSession) {
        currentSession.messages = pageUiState.pageAiMessages.slice();
        currentSession.updatedAt = Date.now();
      }
    } catch (error) {
      pageAiSetRunStatus('failed');
      pageUiState.pageAiMessages.push({
        role: 'assistant',
        content: 'Hermes 当前请求失败：' + (error instanceof Error ? error.message : String(error))
      });
      currentSession = pageAiCurrentSession();
      if (currentSession) {
        currentSession.messages = pageUiState.pageAiMessages.slice();
        currentSession.updatedAt = Date.now();
      }
    } finally {
      pageUiState.pageAiBusy = false;
      renderPageAiConversation();
      renderPageAiControls();
    }
  }

  function ensurePageSettingsPopover() {
    var existing = document.querySelector('[data-testid="wolai-page-settings-popover"]');
    if (existing instanceof HTMLElement) return existing;
    var popover = document.createElement('div');
    popover.className = 'wolai-page-settings-popover';
    popover.setAttribute('data-testid', 'wolai-page-settings-popover');
    popover.setAttribute('data-mnote-surface', 'page-settings');
    popover.hidden = true;
    popover.innerHTML = '' +
      '<div class="wolai-page-settings-panel" role="dialog" aria-modal="false">' +
        '<div class="wolai-page-settings-tabs" data-testid="wolai-page-settings-tabs" role="tablist" aria-label="页面设置分组">' +
          '<button type="button" class="wolai-page-settings-tab is-active" role="tab" aria-selected="true" data-page-settings-tab="page">页面选项</button>' +
          '<button type="button" class="wolai-page-settings-tab" role="tab" aria-selected="false" data-page-settings-tab="custom">自定义页面</button>' +
          '<button type="button" class="wolai-page-settings-tab" role="tab" aria-selected="false" data-page-settings-tab="global">全局选项</button>' +
        '</div>' +
        '<div class="wolai-page-settings-section" data-page-settings-panel="page">' +
          createPageOptionRow('wideLayout', 'checkbox') +
          createPageOptionRow('smallText', 'checkbox') +
          createPageOptionRow('showToc', 'checkbox') +
          createPageOptionRow('protectEditing', 'checkbox') +
        '</div>' +
        '<div class="wolai-page-settings-section" data-page-settings-panel="custom" hidden>' +
          createPageFontRow() +
          createPageOptionRow('layoutDensity', 'select') +
          createPageOptionRow('collapseBacklinks', 'checkbox') +
          createPageOptionRow('hideChildPages', 'checkbox') +
          createPageOptionRow('showBlockRefCount', 'checkbox') +
        '</div>' +
        '<div class="wolai-page-settings-section" data-page-settings-panel="global" hidden>' +
          createGlobalHeadingNumbersRow() +
        '</div>' +
        '<div class="wolai-page-settings-actions">' +
          '<button type="button" class="wolai-page-settings-action" data-page-settings-action="history">页面历史...</button>' +
          '<button type="button" class="wolai-page-settings-action" data-page-settings-action="share">公开分享页面...</button>' +
          '<button type="button" class="wolai-page-settings-action is-disabled" data-page-settings-action="move" disabled>移动到...</button>' +
          '<button type="button" class="wolai-page-settings-action is-disabled" data-page-settings-action="embed" disabled>嵌入到...</button>' +
          '<button type="button" class="wolai-page-settings-action is-danger is-disabled" data-page-settings-action="delete" disabled>删除页面</button>' +
        '</div>' +
        '<div class="wolai-page-settings-stats" data-testid="wolai-page-settings-stats"></div>' +
      '</div>';
    document.body.appendChild(popover);
    return popover;
  }

  function renderPageSettingsPopover() {
    var popover = ensurePageSettingsPopover();
    var options = currentPageOptions();
    popover.querySelectorAll('[data-page-option-checkbox]').forEach(function(input) {
      var key = input.getAttribute('data-page-option-checkbox');
      input.checked = Boolean(options[key]);
      input.disabled = !pageOptionIsSupported(key);
    });
    popover.querySelectorAll('[data-page-option-select]').forEach(function(select) {
      var key = select.getAttribute('data-page-option-select');
      var value = key === 'layoutDensity' ? String(options.layoutDensity || 'normal') : String(options.pageFont || 'default');
      select.value = value;
    });
    renderGlobalOptions(popover);
    var statsNode = popover.querySelector('[data-testid="wolai-page-settings-stats"]');
    if (statsNode instanceof HTMLElement) {
      var stats = computeLivePageStats();
      statsNode.innerHTML = '' +
        '<span>字数 ' + Number(stats.wordCount || 0) + '</span>' +
        '<span>字符 ' + Number(stats.characterCount || 0) + '</span>' +
        '<span>块数 ' + Number(stats.blockCount || 0) + '</span>' +
        '<span>待办 ' + Number(stats.todoDone || 0) + '/' + Number(stats.todoTotal || 0) + '</span>';
    }
  }

  function setActivePageSettingsTab(tabName) {
    var popover = ensurePageSettingsPopover();
    popover.querySelectorAll('[data-page-settings-tab]').forEach(function(tab) {
      var active = tab.getAttribute('data-page-settings-tab') === tabName;
      tab.classList.toggle('is-active', active);
      tab.setAttribute('aria-selected', active ? 'true' : 'false');
    });
    popover.querySelectorAll('[data-page-settings-panel]').forEach(function(panel) {
      panel.hidden = panel.getAttribute('data-page-settings-panel') !== tabName;
    });
  }

  async function persistPageOptionsPatch(patch) {
    var previous = Object.assign({}, currentPageOptions());
    pageUiState.pageOptions = Object.assign({}, previous, patch);
    var nextOptions = Object.assign({}, pageUiState.pageOptions);
    applyPageOptionsToShell();
    renderPageSettingsPopover();
    try {
      var response = await fetch('/api/documents/options', {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({
          documentId: currentDocumentId(),
          workspaceId: resolveWorkspaceId(document.body),
          ...currentWorkspaceSourcePayload(),
          options: nextOptions,
          commandName: 'page.layout.updateOptions'
        })
      });
      var payload = await response.json().catch(function(){ return null; });
      if (!response.ok || !payload || payload.ok !== true) {
        throw new Error(payload && payload.error && payload.error.message ? payload.error.message : 'page_settings_save_failed_' + response.status);
      }
      document.documentElement.setAttribute('data-mnote-page-options-saved', 'true');
    } catch (error) {
      pageUiState.pageOptions = previous;
      applyPageOptionsToShell();
      renderPageSettingsPopover();
      document.documentElement.setAttribute('data-mnote-page-options-error', error instanceof Error ? error.message : String(error));
    }
  }

  function isPageSettingsOpen() {
    var popover = document.querySelector('[data-testid="wolai-page-settings-popover"]');
    return popover instanceof HTMLElement && !popover.hidden;
  }

  function openPageSettingsPopover() {
    if (!currentDocumentId()) return;
    var popover = ensurePageSettingsPopover();
    renderPageSettingsPopover();
    setActivePageSettingsTab('page');
    popover.hidden = false;
    pageUiState.pageSettingsOpen = true;
    updatePageSettingsTriggerState();
  }

  function closePageSettingsPopover() {
    var popover = document.querySelector('[data-testid="wolai-page-settings-popover"]');
    if (popover instanceof HTMLElement) popover.hidden = true;
    pageUiState.pageSettingsOpen = false;
    updatePageSettingsTriggerState();
  }

  function togglePageSettingsPopover() {
    if (isPageSettingsOpen()) closePageSettingsPopover();
    else openPageSettingsPopover();
  }

  function attachmentQueryParams(href) {
    try {
      return new URL(String(href || ''), window.location.origin).searchParams;
    } catch (_) {
      return new URLSearchParams();
    }
  }

  function isOnlyOfficeAttachmentHref(href) {
    try {
      var url = new URL(String(href || ''), window.location.origin);
      return url.pathname === '/onlyoffice' && (url.searchParams.has('assetId') || url.searchParams.has('fileName'));
    } catch (_) {
      return false;
    }
  }

  function normalizeOnlyOfficeAttachmentHref(href) {
    try {
      var url = new URL(String(href || ''), window.location.origin);
      if (url.pathname !== '/onlyoffice') return String(href || '');
      return buildOnlyOfficeOpenUrl({
        fileUrl: url.searchParams.get('fileUrl') || '',
        fileName: url.searchParams.get('fileName') || '未命名附件',
        fileType: url.searchParams.get('fileType') || inferOnlyOfficeFileType(url.searchParams.get('fileName') || '', ''),
        assetId: url.searchParams.get('assetId') || '',
        documentId: url.searchParams.get('documentId') || currentDocumentId() || '',
        userId: url.searchParams.get('userId') || '',
        mode: url.searchParams.get('mode') || 'edit'
      });
    } catch (_) {
      return String(href || '');
    }
  }

  function isOfficeFileName(fileName) {
    return Boolean(inferOnlyOfficeFileType(fileName, ''));
  }

  function detailFromEditorAttachmentLink(link) {
    var rawHref = link instanceof HTMLAnchorElement ? link.href : '';
    var params = attachmentQueryParams(rawHref);
    var fileName = params.get('fileName') || (link ? link.textContent : '') || '未命名附件';
    var fileType = params.get('fileType') || inferOnlyOfficeFileType(fileName, '');
    var assetId = params.get('assetId') || (link ? link.getAttribute('data-asset-id') : '') || '';
    var fileUrl = params.get('fileUrl') || '';
    var href = rawHref;
    if (!isOnlyOfficeAttachmentHref(rawHref) && fileType) {
      fileUrl = rawHref;
      href = buildOnlyOfficeOpenUrl({
        fileUrl: fileUrl,
        fileName: fileName,
        fileType: fileType,
        assetId: assetId,
        documentId: currentDocumentId() || '',
        userId: '',
        mode: 'edit'
      });
    } else if (isOnlyOfficeAttachmentHref(rawHref)) {
      href = normalizeOnlyOfficeAttachmentHref(rawHref);
    }
    return {
      href: href,
      fileUrl: fileUrl,
      fileName: fileName,
      title: fileName,
      fileType: fileType,
      assetId: assetId,
      documentId: params.get('documentId') || currentDocumentId() || '',
      workspaceId: resolveWorkspaceId(document.body),
      fileSize: (link ? link.getAttribute('data-file-size') : '') || ''
    };
  }

  function enhanceEditorAttachmentLink(link) {
    if (!(link instanceof HTMLAnchorElement)) return;
    var href = link.getAttribute('href') || '';
    var params = attachmentQueryParams(href);
    var fileName = params.get('fileName') || link.textContent || '';
    var className = link.getAttribute('class') || '';
    var shouldEnhance = isOnlyOfficeAttachmentHref(href)
      || className.indexOf('mnote-uploaded-attachment-row') >= 0
      || isOfficeFileName(fileName);
    if (!shouldEnhance) return;
    link.setAttribute('data-mnote-attachment-link', 'true');
    var assetId = params.get('assetId') || link.getAttribute('data-asset-id') || '';
    if (assetId) link.setAttribute('data-asset-id', assetId);
    if (isOnlyOfficeAttachmentHref(href)) {
      link.setAttribute('href', buildOnlyOfficeOpenPath({
        fileUrl: params.get('fileUrl') || '',
        fileName: fileName || '未命名附件',
        fileType: params.get('fileType') || inferOnlyOfficeFileType(fileName, ''),
        assetId: assetId,
        documentId: params.get('documentId') || currentDocumentId() || '',
        userId: params.get('userId') || '',
        mode: params.get('mode') || 'edit'
      }));
    }
    attachmentClassForFileName(fileName).split(/\s+/).forEach(function(name) {
      if (name) link.classList.add(name);
    });
    link.setAttribute('target', '_blank');
    link.setAttribute('rel', 'noopener noreferrer nofollow');
    void hydrateEditorAttachmentMeta(link);
  }

  function enhanceEditorAttachmentLinks() {
    document.querySelectorAll('.editor-surface .ProseMirror a[href]').forEach(enhanceEditorAttachmentLink);
    void healLegacyOfficeAttachmentParagraphs();
  }

  function ensureAttachmentActions() {
    var existing = document.querySelector('[data-testid="mnote-attachment-actions"]');
    if (existing instanceof HTMLElement) return existing;
    var actions = document.createElement('div');
    actions.className = 'mnote-attachment-actions';
    actions.setAttribute('data-testid', 'mnote-attachment-actions');
    actions.hidden = true;
    actions.innerHTML = '' +
      '<button type="button" class="mnote-attachment-action" data-testid="mnote-attachment-action-download" data-attachment-action="download" aria-label="下载附件"><span class="material-symbols-outlined" data-icon="download" aria-hidden="true"></span></button>' +
      '<button type="button" class="mnote-attachment-action" data-testid="mnote-attachment-action-menu" data-attachment-action="menu" aria-label="更多操作"><span class="material-symbols-outlined" data-icon="more_horiz" aria-hidden="true"></span></button>';
    actions.addEventListener('mouseenter', function() {
      if (attachmentActionsHideTimer) window.clearTimeout(attachmentActionsHideTimer);
    });
    actions.addEventListener('mouseleave', scheduleHideAttachmentActions);
    document.body.appendChild(actions);
    return actions;
  }

  function positionAttachmentActions(link) {
    if (!(link instanceof HTMLElement)) return;
    var actions = ensureAttachmentActions();
    var rect = link.getBoundingClientRect();
    actions.hidden = false;
    actions.style.left = Math.min(window.innerWidth - 76, Math.max(8, rect.right + 6)) + 'px';
    actions.style.top = Math.max(8, rect.top + (rect.height - 28) / 2) + 'px';
    activeEditorAttachmentLink = link;
  }

  function scheduleHideAttachmentActions() {
    if (attachmentActionsHideTimer) window.clearTimeout(attachmentActionsHideTimer);
    attachmentActionsHideTimer = window.setTimeout(function() {
      var actions = document.querySelector('[data-testid="mnote-attachment-actions"]');
      if (actions instanceof HTMLElement) actions.hidden = true;
      activeEditorAttachmentLink = null;
    }, 220);
  }

  function openEditorAttachmentDetail(detail) {
    if (!detail || !detail.href) return;
    if (isPdfAttachmentFileName(detail.fileName)) {
      void openPdfEditorAttachment(detail);
      return;
    }
    if (isCodeAttachmentFileName(detail.fileName)) {
      void openCodeEditorAttachment(detail);
      return;
    }
    window.open(detail.href, '_blank', 'noopener,noreferrer');
  }

  async function resolveEditorAttachmentUrl(detail) {
    var assetId = String(detail && detail.assetId || '').trim();
    if (assetId) {
      var response = await fetch('/api/media/sign?assetId=' + encodeURIComponent(assetId), {
        method: 'GET',
        credentials: 'include',
        cache: 'no-store'
      });
      var payload = await response.json().catch(function() { return null; });
      if (!response.ok || !payload) {
        throw new Error(payload && payload.error ? payload.error : '生成签名链接失败');
      }
      var signedUrl = String(payload && payload.signedUrl || '').trim();
      if (!signedUrl) throw new Error('附件链接不可用');
      return {
        url: signedUrl,
        asset: payload.asset && typeof payload.asset === 'object' ? payload.asset : {}
      };
    }
    var url = String(detail && (detail.fileUrl || detail.href) || '').trim();
    if (!url) throw new Error('附件链接不可用');
    return { url: url, asset: {} };
  }

  async function openPdfEditorAttachment(detail) {
    try {
      var resolved = await resolveEditorAttachmentUrl(detail);
      window.open(resolved.url, '_blank', 'noopener,noreferrer');
    } catch (error) {
      window.alert(error && error.message ? error.message : '打开 PDF 失败');
    }
  }

  async function openCodeEditorAttachment(detail) {
    var resolved = null;
    try {
      resolved = await resolveEditorAttachmentUrl(detail);
      var size = Number(resolved.asset && (resolved.asset.file_size || resolved.asset.fileSize) || 0);
      if (Number.isFinite(size) && size > 1024 * 1024) {
        window.open(resolved.url, '_blank', 'noopener,noreferrer');
        return;
      }
      var response = await fetch(resolved.url, {
        method: 'GET',
        credentials: 'include',
        cache: 'no-store'
      });
      if (!response.ok) throw new Error('读取附件内容失败');
      var text = await response.text();
      if (text.length > 1024 * 1024) {
        window.open(resolved.url, '_blank', 'noopener,noreferrer');
        return;
      }
      var editorRoot = document.querySelector('.editor-surface .ProseMirror');
      var editor = editorRoot && editorRoot.editor;
      if (!editor || !editor.chain) {
        window.open(resolved.url, '_blank', 'noopener,noreferrer');
        return;
      }
      var title = String(detail.fileName || resolved.asset.file_name || '附件').trim() || '附件';
      var language = inferCodeAttachmentLanguage(title);
      editor.chain().focus().insertContent([
        {
          type: 'paragraph',
          content: [{ type: 'text', text: title }]
        },
        {
          type: 'codeBlock',
          attrs: { language: language },
          content: text ? [{ type: 'text', text: text.replace(/\r\n?/g, '\n') }] : []
        }
      ]).run();
    } catch (error) {
      console.warn('[mnote attachment] open code attachment failed', error);
      if (resolved && resolved.url) {
        window.open(resolved.url, '_blank', 'noopener,noreferrer');
        return;
      }
      window.alert(error && error.message ? error.message : '打开代码附件失败');
    }
  }

  async function openEditorAttachmentDownload(detail) {
    if (!detail) return;
    if (detail.assetId) {
      try {
        var response = await fetch('/api/media/sign?assetId=' + encodeURIComponent(detail.assetId), {
          method: 'GET',
          credentials: 'include'
        });
        var payload = await response.json().catch(function() { return null; });
        var signedUrl = String(payload && payload.signedUrl || '').trim();
        if (response.ok && signedUrl) {
          window.open(signedUrl, '_blank', 'noopener,noreferrer');
          return;
        }
      } catch (_) {}
    }
    var target = detail.fileUrl || detail.href;
    if (!target) return;
    window.open(target, '_blank', 'noopener,noreferrer');
  }

  function openEditorAttachmentMenu(link, trigger) {
    var detail = detailFromEditorAttachmentLink(link);
    var rect = trigger && trigger.getBoundingClientRect ? trigger.getBoundingClientRect() : link.getBoundingClientRect();
    openTreeContextMenu('attachment', detail, rect.right, rect.bottom + 4, trigger || link);
  }

  function openEditorAttachmentLink(link) {
    enhanceEditorAttachmentLink(link);
    openEditorAttachmentDetail(detailFromEditorAttachmentLink(link));
  }

  enhanceEditorAttachmentLinks();
  var attachmentObserver = new MutationObserver(function() { enhanceEditorAttachmentLinks(); });
  attachmentObserver.observe(document.documentElement, { childList: true, subtree: true });

  document.addEventListener('mouseover', function(event) {
    var link = closestAction(event.target, '.editor-surface .ProseMirror a[data-mnote-attachment-link="true"], .editor-surface .ProseMirror a.mnote-uploaded-attachment-row');
    if (!(link instanceof HTMLAnchorElement)) return;
    if (attachmentActionsHideTimer) window.clearTimeout(attachmentActionsHideTimer);
    enhanceEditorAttachmentLink(link);
    positionAttachmentActions(link);
  });

  document.addEventListener('mouseout', function(event) {
    var link = closestAction(event.target, '.editor-surface .ProseMirror a[data-mnote-attachment-link="true"], .editor-surface .ProseMirror a.mnote-uploaded-attachment-row');
    if (!(link instanceof HTMLAnchorElement)) return;
    var next = event.relatedTarget;
    var actions = document.querySelector('[data-testid="mnote-attachment-actions"]');
    if (next && (link.contains(next) || (actions && actions.contains(next)))) return;
    scheduleHideAttachmentActions();
  });

  document.addEventListener('click', function(e) {
    if (activeTreeContextMenu && activeTreeContextMenu.contains(e.target)) return;
    if (activeTreeContextMenu) closeTreeContextMenu();

    var attachmentAction = closestAction(e.target, '[data-attachment-action]');
    if (attachmentAction) {
      e.preventDefault();
      e.stopPropagation();
      var link = activeEditorAttachmentLink;
      if (!(link instanceof HTMLAnchorElement)) return;
      var attachmentDetail = detailFromEditorAttachmentLink(link);
      var attachmentActionName = attachmentAction.getAttribute('data-attachment-action') || '';
      if (attachmentActionName === 'download') {
        openEditorAttachmentDownload(attachmentDetail);
        return;
      }
      if (attachmentActionName === 'menu') {
        openEditorAttachmentMenu(link, attachmentAction);
        return;
      }
      return;
    }

    var editorAttachmentLink = closestAction(e.target, '.editor-surface .ProseMirror a[data-mnote-attachment-link="true"], .editor-surface .ProseMirror a.mnote-uploaded-attachment-row');
    if (editorAttachmentLink instanceof HTMLAnchorElement) {
      e.preventDefault();
      openEditorAttachmentLink(editorAttachmentLink);
      return;
    }

    var historyClose = closestAction(e.target, '[data-page-history-action="close"]');
    if (historyClose) {
      e.preventDefault();
      closePageHistoryDrawer();
      return;
    }

    var shareClose = closestAction(e.target, '[data-page-share-action="close"]');
    if (shareClose) {
      e.preventDefault();
      closePageShareDialog();
      return;
    }

    var shareCopy = closestAction(e.target, '[data-page-share-action="copy-link"]');
    if (shareCopy) {
      e.preventDefault();
      void copyTreeContextValue(window.location.href, 'page-share-link');
      return;
    }

    var pageHistoryTrigger = closestAction(e.target, '[data-mnote-action="open-page-history"]');
    if (pageHistoryTrigger) {
      e.preventDefault();
      openPageHistoryDrawer();
      return;
    }

    var pageSettingsTrigger = closestAction(e.target, '[data-mnote-action="open-page-settings"]');
    if (pageSettingsTrigger) {
      e.preventDefault();
      togglePageSettingsPopover();
      return;
    }

    var pageSettingsPanel = closestAction(e.target, '.wolai-page-settings-panel');
    if (isPageSettingsOpen() && !pageSettingsPanel) {
      closePageSettingsPopover();
    }

    var pageSettingsTab = closestAction(e.target, '[data-page-settings-tab]');
    if (pageSettingsTab) {
      e.preventDefault();
      setActivePageSettingsTab(pageSettingsTab.getAttribute('data-page-settings-tab') || 'page');
      return;
    }

    var pageSettingsAction = closestAction(e.target, '[data-page-settings-action]');
    if (pageSettingsAction) {
      e.preventDefault();
      var actionName = pageSettingsAction.getAttribute('data-page-settings-action') || '';
      if (actionName === 'history') {
        openPageHistoryDrawer();
        return;
      }
      if (actionName === 'share') {
        openPageShareDialog();
        return;
      }
      return;
    }

    var pageAiTrigger = closestAction(e.target, '[data-mnote-action="open-page-ai"]');
    if (pageAiTrigger) {
      e.preventDefault();
      openPageAiDrawer();
      return;
    }

    var pageAiClose = closestAction(e.target, '[data-page-ai-action="close"]');
    if (pageAiClose) {
      e.preventDefault();
      closePageAiDrawer();
      return;
    }

    var pageAiRotate = closestAction(e.target, '[data-page-ai-action="rotate"]');
    if (pageAiRotate) {
      e.preventDefault();
      pageUiState.pageAiSuggestionIndex += 1;
      renderPageAiSuggestions();
      return;
    }

    var pageAiIntent = closestAction(e.target, '[data-page-ai-intent]');
    if (pageAiIntent) {
      e.preventDefault();
      var intentName = pageAiIntent.getAttribute('data-page-ai-intent') || '';
      if (intentName === 'create-summary') {
        void sendPageAiMessage('请通过 Hermes 调用 mnote.artifact.create_summary，为当前页面创建或更新 AI Summary。');
        return;
      }
      if (intentName === 'create-ai-note') {
        void sendPageAiMessage('请通过 Hermes 调用 mnote.artifact.create_ai_note，基于当前页面创建一篇新的 AI Note。');
        return;
      }
    }

    var pageAiTab = closestAction(e.target, '[data-page-ai-tab]');
    if (pageAiTab) {
      e.preventDefault();
      pageUiState.pageAiPage = pageAiTab.getAttribute('data-page-ai-tab') || 'chat';
      renderPageAiControls();
      renderPageAiConversation();
      return;
    }

    var pageAiProvider = closestAction(e.target, '[data-page-ai-provider]');
    if (pageAiProvider) {
      e.preventDefault();
      pageUiState.pageAiProvider = pageAiProvider.getAttribute('data-page-ai-provider') || 'hermes';
      renderPageAiProviderButtons();
      return;
    }

    var pageAiMemorySave = closestAction(e.target, '[data-page-ai-memory-save]');
    if (pageAiMemorySave) {
      e.preventDefault();
      void pageAiSaveProfileMemory(pageAiMemorySave.getAttribute('data-page-ai-memory-save') || '');
      return;
    }

    var pageAiSkillToggle = closestAction(e.target, '[data-page-ai-skill-toggle]');
    if (pageAiSkillToggle) {
      e.preventDefault();
      var skillName = pageAiSkillToggle.getAttribute('data-page-ai-skill-toggle') || '';
      var nextEnabled = pageAiSkillToggle.getAttribute('aria-pressed') !== 'true';
      void pageAiToggleSkill(skillName, nextEnabled);
      return;
    }

    var pageAiSession = closestAction(e.target, '[data-page-ai-session]');
    if (pageAiSession) {
      e.preventDefault();
      pageAiSetActiveSession(pageAiSession.getAttribute('data-page-ai-session') || '');
      return;
    }

    var pageAiSuggestion = closestAction(e.target, '[data-page-ai-suggestion]');
    if (pageAiSuggestion) {
      e.preventDefault();
      var text = pageAiSuggestion.getAttribute('data-page-ai-suggestion') || '';
      var inputNode = ensurePageAiDrawer().querySelector('[data-page-ai-input]');
      if (inputNode instanceof HTMLTextAreaElement) {
        inputNode.value = text;
        inputNode.focus();
      }
      return;
    }

    var pageAiNewSession = closestAction(e.target, '[data-page-ai-action="new-session"]');
    if (pageAiNewSession) {
      e.preventDefault();
      pageUiState.pageAiPage = 'chat';
      pageAiStartNewSession();
      return;
    }

    var pageAiHistory = closestAction(e.target, '[data-page-ai-action="history"]');
    if (pageAiHistory) {
      e.preventDefault();
      pageAiLoadSessions();
      pageUiState.pageAiPage = pageUiState.pageAiPage === 'history' ? 'chat' : 'history';
      renderPageAiControls();
      renderPageAiConversation();
      return;
    }

    var pageAiSend = closestAction(e.target, '[data-page-ai-action="send"]');
    if (pageAiSend) {
      e.preventDefault();
      var input = ensurePageAiDrawer().querySelector('[data-page-ai-input]');
      if (input instanceof HTMLTextAreaElement) {
        var message = input.value;
        input.value = '';
        void sendPageAiMessage(message);
      }
      return;
    }

    var searchTrigger = closestAction(e.target, '[data-mnote-action="open-search-modal"]');
    if (searchTrigger) {
      e.preventDefault();
      toggleSearchModal();
      return;
    }
    var searchDialog = closestAction(e.target, '.wolai-search-dialog');
    if (isSearchModalOpen() && !searchDialog) {
      closeSearchModal();
      return;
    }

    var sourceMenuTrigger = closestAction(e.target, '[data-mnote-action="open-workspace-source-menu"]');
    if (sourceMenuTrigger) {
      e.preventDefault();
      var existingSourceMenu = document.querySelector('[data-testid="mnote-workspace-source-menu"]');
      if (existingSourceMenu) closeWorkspaceSourceMenu();
      else openWorkspaceSourceMenu(sourceMenuTrigger);
      return;
    }
    var sourceMenu = closestAction(e.target, '[data-testid="mnote-workspace-source-menu"]');
    if (!sourceMenu) closeWorkspaceSourceMenu();

    var tabTrigger = closestAction(e.target, '[data-mnote-sidebar-tree-tab]');
    if (tabTrigger) {
      e.preventDefault();
      switchSidebarTreeTab(tabTrigger);
      return;
    }

    var localFolderTrigger = closestAction(e.target, '[data-mnote-action="open-local-folder"]');
    if (localFolderTrigger) {
      e.preventDefault();
      requestOpenLocalFolder();
      return;
    }

    var createTrigger = closestAction(e.target, '[data-mnote-action="create-page"]');
    if (createTrigger) {
      e.preventDefault();
      void createPage(createTrigger, null);
      return;
    }

    var fileTree = document.getElementById('sidebar-file-tree-root');
    if (fileTree && fileTree.contains(e.target)) {
      var fileBtn = closestAction(e.target, '[data-rust-action]');
      var fileRow = closestAction(e.target, '.tree-row[data-shell-mode="filetree"]');
      if (!fileRow) return;
      var fileAction = fileBtn ? fileBtn.getAttribute('data-rust-action') : 'open';
      var rowId = fileRow.getAttribute('data-row-id') || '';
      var rowKind = fileRow.getAttribute('data-row-kind') || '';
      var documentId = fileRow.getAttribute('data-document-id') || fileRow.getAttribute('data-doc-id') || '';
      var assetId = fileRow.getAttribute('data-asset-id') || '';
      var assetType = '';
      var kindBadge = fileRow.querySelector('.tree-kind-badge');
      if (kindBadge instanceof HTMLElement) {
        assetType = kindBadge.getAttribute('data-kind') || '';
      }
      if (fileAction === 'toggle') {
        e.preventDefault();
        toggleChildren(fileRow, fileBtn);
        return;
      }
      if (fileAction === 'create') {
        e.preventDefault();
        void createPage(fileBtn || fileRow, documentId || fileRow.getAttribute('data-node-id'));
        return;
      }
      if (fileAction === 'menu') {
        e.preventDefault();
        selectSidebarFileTreeRow(fileRow, { ctrlKey: false, metaKey: false, shiftKey: false });
        var point = rowCenter(fileBtn || fileRow);
        dispatchSidebarEvent('tree.filetree.context-menu', { rowId: rowId, rowKind: rowKind, documentId: documentId || null, assetId: assetId || null });
        openFileTreeContextMenu(fileRow, point.x, point.y, fileBtn || fileRow);
        return;
      }
      if (fileAction === 'open' && rowKind === 'folder') {
        e.preventDefault();
        selectSidebarFileTreeRow(fileRow, { ctrlKey: e.ctrlKey, metaKey: e.metaKey, shiftKey: e.shiftKey });
        toggleChildren(fileRow, fileRow.querySelector('[data-rust-action="toggle"]'));
        return;
      }
      e.preventDefault();
      selectSidebarFileTreeRow(fileRow, { ctrlKey: e.ctrlKey, metaKey: e.metaKey, shiftKey: e.shiftKey });
      var objectIdentity = readFileTreeObjectIdentity(fileRow);
      dispatchSidebarEvent('tree.filetree.open', { rowId: rowId, rowKind: rowKind, documentId: documentId || null, assetId: assetId || null, assetType: assetType || null, objectIdentity: objectIdentity, workspaceId: resolveWorkspaceId(fileRow) });
      if (e.shiftKey || e.ctrlKey || e.metaKey) {
        return;
      }
      if ((rowKind === 'document' || rowKind === 'index' || rowKind === 'markdown') && documentId) {
        navigateToDocument(documentId, resolveWorkspaceId(fileRow), { treeView: 'filetree' });
      } else if (assetId) {
        dispatchSidebarEvent('tree.asset.open', { rowId: rowId, rowKind: rowKind, documentId: documentId || null, assetId: assetId, assetType: assetType || null, objectIdentity: objectIdentity, workspaceId: resolveWorkspaceId(fileRow) });
      }
      return;
    }

    var tree = document.getElementById('sidebar-tree-root');
    if (!tree || !tree.contains(e.target)) return;
    var btn = closestAction(e.target, '[data-rust-action]');
    if (!btn) return;
    var nodeId = btn.getAttribute('data-node-id');
    var action = btn.getAttribute('data-rust-action');

    if (action === 'toggle') {
      var row = btn.closest('.tree-row');
      if (!row) return;
      toggleChildren(row, btn);
      e.preventDefault();
    } else if (action === 'open') {
      if (btn.getAttribute('data-page-openable') === 'false') {
        e.preventDefault();
        return;
      }
      var workspaceId = resolveWorkspaceId(btn);
      navigateToDocument(nodeId, workspaceId, { treeView: 'page' });
      e.preventDefault();
    } else if (action === 'create') {
      e.preventDefault();
      void createPage(btn, nodeId);
    } else if (action === 'rename') {
      e.preventDefault();
      var title = window.prompt('重命名页面');
      if (title && title.trim()) {
        void dispatchTreeCommand(btn, {
          action: 'rename',
          workspaceId: resolveWorkspaceId(btn),
          documentId: nodeId,
          title: title.trim()
        }).then(function(){
          updateTitleEverywhere(nodeId, title.trim());
        });
      }
    } else if (action === 'menu') {
      e.preventDefault();
      var menuPoint = rowCenter(btn);
      dispatchSidebarEvent('tree.page.context-menu', { documentId: nodeId, target: { documentId: nodeId } });
      openPageTreeContextMenu(btn.closest('.tree-row'), menuPoint.x, menuPoint.y, btn);
    }
  });

  document.addEventListener('contextmenu', function(event) {
    var fileRow = closestAction(event.target, '.tree-row[data-shell-mode="filetree"]');
    if (fileRow) {
      event.preventDefault();
      var contextRowId = fileRow.getAttribute('data-row-id') || '';
      if (contextRowId && !sidebarFileTreeSelection.selectedRowIds.has(contextRowId)) {
        selectSidebarFileTreeRow(fileRow, { ctrlKey: false, metaKey: false, shiftKey: false });
      }
      openFileTreeContextMenu(fileRow, event.clientX, event.clientY, fileRow);
      return;
    }
    var pageRow = closestAction(event.target, '.tree-row[data-shell-mode="page"]');
    if (pageRow) {
      event.preventDefault();
      openPageTreeContextMenu(pageRow, event.clientX, event.clientY, pageRow);
    }
  });

  document.addEventListener('keydown', function(event) {
    if (event.key === 'Escape' && isPageSettingsOpen()) {
      event.preventDefault();
      closePageSettingsPopover();
      return;
    }
    if ((event.metaKey || event.ctrlKey) && !event.shiftKey && event.key && event.key.toLowerCase() === 'p') {
      event.preventDefault();
      toggleSearchModal();
      return;
    }
    if (event.key === 'Escape') {
      closeSearchModal();
      closeTreeContextMenu();
    }
    if (event.key === 'Enter' && !event.shiftKey) {
      var aiInput = closestAction(event.target, '[data-page-ai-input]');
      if (aiInput instanceof HTMLTextAreaElement) {
        event.preventDefault();
        var text = aiInput.value;
        aiInput.value = '';
        void sendPageAiMessage(text);
      }
    }
  });

  document.addEventListener('input', function(event) {
    var skillSearch = closestAction(event.target, '[data-page-ai-skill-search]');
    if (skillSearch instanceof HTMLInputElement) {
      pageUiState.pageAiSkillQuery = skillSearch.value;
      renderPageAiControls();
      return;
    }
    var memoryEditor = closestAction(event.target, '[data-page-ai-memory-editor]');
    if (memoryEditor instanceof HTMLTextAreaElement) {
      var section = memoryEditor.getAttribute('data-page-ai-memory-editor') || '';
      if (['memory', 'user', 'soul'].indexOf(section) >= 0) {
        pageUiState.pageAiProfileMemoryDrafts[section] = memoryEditor.value;
      }
    }
  });

  document.addEventListener('change', function(event) {
    var pageAiProfileSelect = closestAction(event.target, '[data-page-ai-profile-select]');
    if (pageAiProfileSelect instanceof HTMLSelectElement) {
      void pageAiSwitchProfile(pageAiProfileSelect.value);
      return;
    }
    var pageAiContextSelect = closestAction(event.target, '[data-page-ai-context-scope]');
    if (pageAiContextSelect instanceof HTMLSelectElement) {
      pageAiSetContextScope(pageAiContextSelect.value);
      renderPageAiControls();
      return;
    }
    var globalCheckbox = closestAction(event.target, '[data-global-option-checkbox="showHeadingNumbers"]');
    if (globalCheckbox instanceof HTMLInputElement) {
      writeGlobalShowHeadingNumbers(globalCheckbox.checked);
      applyPageOptionsToShell();
      renderPageSettingsPopover();
      return;
    }
    var checkbox = closestAction(event.target, '[data-page-option-checkbox]');
    if (checkbox instanceof HTMLInputElement) {
      var key = checkbox.getAttribute('data-page-option-checkbox') || '';
      if (!pageOptionIsSupported(key)) return;
      var patch = {};
      patch[key] = checkbox.checked;
      void persistPageOptionsPatch(patch);
      return;
    }
    var select = closestAction(event.target, '[data-page-option-select]');
    if (select instanceof HTMLSelectElement) {
      var selectKey = select.getAttribute('data-page-option-select') || '';
      var next = {};
      if (selectKey === 'layoutDensity') next.layoutDensity = select.value;
      if (selectKey === 'pageFont') next.pageFont = select.value;
      if (Object.keys(next).length) {
        void persistPageOptionsPatch(next);
      }
    }
  });

  function initializePageUiSurfaces() {
    pageUiState.pageOptions = null;
    applyPageOptionsToShell();
    updatePageSettingsTriggerState();
    updatePageAiTriggerState();
    ensureHistorySnapshotsSeeded();
  }

  window.__mnoteRecordPageHistorySnapshot = recordPageHistorySnapshot;
  window.__mnoteApplyPageOptionsToShell = applyPageOptionsToShell;
  setTimeout(initializePageUiSurfaces, 0);

  function readPageDragNodeId(event) {
    var fromTransfer = event.dataTransfer ? event.dataTransfer.getData(PAGE_DRAG_MIME) || event.dataTransfer.getData('text/plain') : '';
    return (fromTransfer || draggingPageNodeId || '').trim();
  }

  function clearPageDropFeedback() {
    if (activePageDropRow instanceof HTMLElement) {
      activePageDropRow.setAttribute('data-drop-feedback', 'false');
    }
    activePageDropRow = null;
  }

  function canDropPage(sourceNodeId, targetRow) {
    if (!sourceNodeId || !(targetRow instanceof HTMLElement)) return false;
    var targetNodeId = targetRow.getAttribute('data-node-id') || '';
    if (!targetNodeId || targetNodeId === sourceNodeId) return false;
    var sourceNode = document.querySelector('#sidebar-tree-root .tree-node[data-node-id="' + cssEscape(sourceNodeId) + '"]');
    return !(sourceNode instanceof HTMLElement && sourceNode.contains(targetRow));
  }

  function pageDropPosition(event, row) {
    var rect = row.getBoundingClientRect();
    var ratio = rect.height > 0 ? (event.clientY - rect.top) / rect.height : 0.5;
    if (ratio < 0.25) return 'before';
    if (ratio > 0.75) return 'after';
    return 'inside';
  }

  function resolvePageMoveTarget(targetRow, position) {
    var targetNodeId = targetRow.getAttribute('data-node-id') || '';
    var parentId = targetRow.getAttribute('data-parent-id') || null;
    if (position === 'inside') {
      var children = targetRow.parentElement ? targetRow.parentElement.querySelectorAll(':scope > .tree-children > .tree-node > .tree-row[data-shell-mode="page"]') : [];
      return { parentId: targetNodeId, sortOrder: children.length };
    }
    var siblings = Array.from(document.querySelectorAll('#sidebar-tree-root .tree-row[data-shell-mode="page"]')).filter(function(row) {
      return (row.getAttribute('data-parent-id') || '') === (parentId || '');
    });
    var index = siblings.indexOf(targetRow);
    return { parentId: parentId, sortOrder: Math.max(0, index + (position === 'after' ? 1 : 0)) };
  }

  document.addEventListener('dragstart', function(event) {
    var pageRow = closestAction(event.target, '.tree-row[data-shell-mode="page"][draggable="true"]');
    if (pageRow) {
      draggingPageNodeId = pageRow.getAttribute('data-node-id') || '';
      if (event.dataTransfer) {
        event.dataTransfer.effectAllowed = 'move';
        event.dataTransfer.setData(PAGE_DRAG_MIME, draggingPageNodeId);
        event.dataTransfer.setData('text/plain', draggingPageNodeId);
      }
      return;
    }
    var fileRow = closestAction(event.target, '.tree-row[data-shell-mode="filetree"][draggable="true"]');
    if (fileRow) {
      draggingFileTreeRowIds = selectedSidebarFileTreeRowIdsForDrag(fileRow);
      if (event.dataTransfer) {
        var payload = JSON.stringify({ type: 'mnote-file-tree-dnd', version: 1, rowIds: draggingFileTreeRowIds });
        event.dataTransfer.effectAllowed = 'copyMove';
        event.dataTransfer.setData(FILETREE_DRAG_MIME, payload);
        event.dataTransfer.setData('application/x-mnote-file-tree', payload);
        event.dataTransfer.setData('text/plain', payload);
      }
    }
  });

  document.addEventListener('dragover', function(event) {
    var pageRow = closestAction(event.target, '.tree-row[data-shell-mode="page"]');
    var sourceNodeId = readPageDragNodeId(event);
    if (pageRow && canDropPage(sourceNodeId, pageRow)) {
      event.preventDefault();
      clearPageDropFeedback();
      pageRow.setAttribute('data-drop-feedback', 'true');
      pageRow.setAttribute('data-drop-position', pageDropPosition(event, pageRow));
      activePageDropRow = pageRow;
      if (event.dataTransfer) event.dataTransfer.dropEffect = 'move';
      return;
    }
    var fileTree = document.getElementById('sidebar-file-tree-root');
    if (fileTree && fileTree.contains(event.target)) {
      var hasFiles = event.dataTransfer && Array.prototype.indexOf.call(event.dataTransfer.types || [], 'Files') >= 0;
      var hasInternal = event.dataTransfer && Array.prototype.indexOf.call(event.dataTransfer.types || [], FILETREE_DRAG_MIME) >= 0;
      if (!hasFiles && !hasInternal && draggingFileTreeRowIds.length === 0) return;
      event.preventDefault();
      if (activeFileTreeDropRow instanceof HTMLElement) activeFileTreeDropRow.setAttribute('data-drop-target', 'false');
      activeFileTreeDropRow = closestAction(event.target, '.tree-row[data-shell-mode="filetree"]') || fileTree;
      if (activeFileTreeDropRow instanceof HTMLElement) activeFileTreeDropRow.setAttribute('data-drop-target', 'true');
      if (event.dataTransfer) event.dataTransfer.dropEffect = hasFiles || event.altKey ? 'copy' : 'move';
    }
  });

  document.addEventListener('drop', function(event) {
    var pageRow = closestAction(event.target, '.tree-row[data-shell-mode="page"]');
    var sourceNodeId = readPageDragNodeId(event);
    if (pageRow && canDropPage(sourceNodeId, pageRow)) {
      event.preventDefault();
      var position = pageDropPosition(event, pageRow);
      var target = resolvePageMoveTarget(pageRow, position);
      clearPageDropFeedback();
      draggingPageNodeId = '';
      void dispatchTreeCommand(pageRow, {
        action: 'move',
        workspaceId: resolveWorkspaceId(pageRow),
        documentId: sourceNodeId,
        parentId: target.parentId,
        sortOrder: target.sortOrder
      });
      return;
    }
    var fileTree = document.getElementById('sidebar-file-tree-root');
    if (fileTree && fileTree.contains(event.target)) {
      var targetRow = closestAction(event.target, '.tree-row[data-shell-mode="filetree"]');
      var files = event.dataTransfer ? Array.from(event.dataTransfer.files || []) : [];
      var raw = event.dataTransfer ? event.dataTransfer.getData(FILETREE_DRAG_MIME) || event.dataTransfer.getData('application/x-mnote-file-tree') || '' : '';
      var rowIds = draggingFileTreeRowIds.slice();
      if (raw) {
        try {
          var parsed = JSON.parse(raw);
          if (Array.isArray(parsed.rowIds)) rowIds = parsed.rowIds;
        } catch (_) {}
      }
      if (!files.length && !rowIds.length) return;
      event.preventDefault();
      var detail = {
        workspaceId: resolveWorkspaceId(targetRow || fileTree),
        targetRowId: targetRow ? targetRow.getAttribute('data-row-id') : null,
        targetRowKind: targetRow ? targetRow.getAttribute('data-row-kind') : 'root',
        documentId: targetRow ? targetRow.getAttribute('data-document-id') || targetRow.getAttribute('data-doc-id') : null,
        assetId: targetRow ? targetRow.getAttribute('data-asset-id') : null
      };
      if (activeFileTreeDropRow instanceof HTMLElement) activeFileTreeDropRow.setAttribute('data-drop-target', 'false');
      activeFileTreeDropRow = null;
      if (files.length) {
        dispatchSidebarEvent('tree.filetree.external-drop', Object.assign({}, detail, { files: files }));
      } else {
        dispatchSidebarEvent('tree.filetree.internal-drop', Object.assign({}, detail, { rowIds: rowIds, copy: event.altKey === true }));
      }
      draggingFileTreeRowIds = [];
    }
  });

  document.addEventListener('dragend', function() {
    draggingPageNodeId = '';
    draggingFileTreeRowIds = [];
    clearPageDropFeedback();
    if (activeFileTreeDropRow instanceof HTMLElement) activeFileTreeDropRow.setAttribute('data-drop-target', 'false');
    activeFileTreeDropRow = null;
  });

  window.addEventListener('tree:title-updated', function(event) {
    var detail = event.detail || {};
    updateTitleEverywhere(detail.documentId, detail.title);
    document.documentElement.setAttribute('data-mnote-title-local-applied', 'true');
  });

  window.addEventListener('tree:local-command', function(event) {
    var detail = event.detail || {};
    var body = detail.body || {};
    var action = String(body.action || '').trim();
    if (action === 'create') {
      applyCreatedDocumentLocally(detail.result || {}, body.parentId || null, body.title || '新页面');
      return;
    }
    if ((action === 'purge' || action === 'delete' || action === 'archive') && body.documentId) {
      if (applyRemoveDocumentDelta({ documentId: body.documentId })) {
        document.documentElement.setAttribute('data-mnote-tree-local-command-applied', 'remove');
      }
    }
  });

  window.addEventListener('tree:snapshot', function(event) {
    var payload = event.detail && event.detail.payload ? event.detail.payload : event.detail;
    if (renderSidebarSnapshot(payload)) {
      document.documentElement.setAttribute('data-mnote-tree-live-applied', 'snapshot');
      return;
    }
    setTreeLiveApplyError('tree_snapshot_missing_projection_payload');
  });

  window.addEventListener('tree:delta', function(event) {
    var payload = event.detail && event.detail.payload ? event.detail.payload : event.detail;
    var data = payload && (payload.data || payload.delta || payload);
    var documentPatch = data && (data.document || data.node);
    if (data && data.op === 'upsert_document' && documentPatch) {
      updateTitleEverywhere(documentPatch.id || documentPatch.documentId, documentPatch.title || '无标题');
    }
    if (data && data.op === 'move_document' && applyMoveDocumentDelta(data)) {
      document.documentElement.setAttribute('data-mnote-tree-live-applied', 'delta');
      return;
    }
    if (data && data.op === 'remove_document' && applyRemoveDocumentDelta(data)) {
      document.documentElement.setAttribute('data-mnote-tree-live-applied', 'delta');
      return;
    }
    var documents = data && (data.upsertDocuments || data.upsert_documents);
    if (Array.isArray(documents)) {
      documents.forEach(function(doc) {
        updateTitleEverywhere(doc.id || doc.documentId, doc.title || '无标题');
      });
    }
    if (renderSidebarSnapshot(payload)) {
      document.documentElement.setAttribute('data-mnote-tree-live-applied', 'delta');
      return;
    }
    document.documentElement.setAttribute('data-mnote-tree-live-applied', 'delta');
    if (deltaNeedsProjectionRefresh(payload)) {
      setTreeLiveApplyError('tree_delta_missing_projection_payload');
    }
  });

  window.addEventListener('tree:resync', function(event) {
    var payload = event.detail && event.detail.payload ? event.detail.payload : event.detail;
    if (renderSidebarSnapshot(payload)) {
      document.documentElement.setAttribute('data-mnote-tree-live-applied', 'resync');
      return;
    }
    document.documentElement.setAttribute('data-mnote-tree-live-applied', 'resync');
    setTreeLiveApplyError('tree_resync_missing_projection_payload');
  });

  var tree = document.getElementById('sidebar-tree-root');
  var activeId = currentDocumentId();
  if (tree && activeId) {
    var activeRow = tree.querySelector('.tree-row[data-node-id="' + cssEscape(activeId) + '"]');
    if (activeRow instanceof HTMLElement) activeRow.setAttribute('data-active', 'true');
  }
  var initiallySelectedFileRows = document.querySelectorAll('#sidebar-file-tree-root .tree-row[data-selected="true"]');
  initiallySelectedFileRows.forEach(function(row) {
    if (!(row instanceof HTMLElement)) return;
    var rowId = row.getAttribute('data-row-id') || '';
    if (!rowId) return;
    sidebarFileTreeSelection.selectedRowIds.add(rowId);
    if (!sidebarFileTreeSelection.anchorRowId) sidebarFileTreeSelection.anchorRowId = rowId;
    sidebarFileTreeSelection.focusedRowId = rowId;
  });
  syncSidebarFileTreeSelection();
  restoreSidebarTreeTab();
})();
"##;

const TREE_LIVE_CONTROLLER_JS: &str = r##"
(function(){
  if (window.__mnoteTreeLiveControllerStarted) return;
  window.__mnoteTreeLiveControllerStarted = true;

  function readBootstrap() {
    var script = document.getElementById('__MNOTE_TREE_LIVE_BOOTSTRAP__');
    var fallback = {
      schema: 'mnote.tree_live_bootstrap.v1',
      transport: 'convex-command-log-sse',
      endpoint: '/api/tree/events',
      rootIds: [],
      initialRevision: null
    };
    if (!script || !script.textContent) return fallback;
    try {
      return Object.assign(fallback, JSON.parse(script.textContent));
    } catch (_) {
      return fallback;
    }
  }

  function resolveWorkspaceId() {
    var withWorkspace = document.querySelector('[data-workspace-id]');
    if (withWorkspace) {
      var value = (withWorkspace.getAttribute('data-workspace-id') || '').trim();
      if (value) return value;
    }
    var params = new URLSearchParams(window.location.search);
    return (params.get('workspaceId') || 'default').trim() || 'default';
  }

  function dispatchTreeEvent(name, detail) {
    window.dispatchEvent(new CustomEvent(name, { detail: detail }));
  }

  function applyStatus(status) {
    document.documentElement.setAttribute('data-mnote-tree-live-status', status);
  }

  function applyTransport(transport) {
    document.documentElement.setAttribute('data-mnote-tree-live-transport', transport || 'convex-command-log-sse');
  }

  function closeActiveSource() {
    var source = window.__mnoteTreeLiveEventSource;
    if (source && typeof source.close === 'function') {
      source.close();
      window.__mnoteTreeLiveEventSource = null;
      applyStatus('closed');
    }
  }

  function start() {
    if (!('EventSource' in window)) {
      applyStatus('unsupported');
      return;
    }
    var bootstrap = readBootstrap();
    var params = new URLSearchParams(window.location.search);
    var sourceKind = (params.get('sourceKind') || '').trim();
    if (sourceKind === 'local_folder') {
      applyTransport('local-folder-static');
      applyStatus('static');
      return;
    }
    applyTransport(bootstrap.transport || 'convex-command-log-sse');
    var workspaceId = bootstrap.workspaceId || resolveWorkspaceId();
    var url = new URL(bootstrap.endpoint || '/api/tree/events', window.location.origin);
    url.searchParams.set('workspaceId', workspaceId);
    if (Array.isArray(bootstrap.rootIds) && bootstrap.rootIds.length === 1) {
      url.searchParams.set('rootNodeId', bootstrap.rootIds[0]);
    }
    var failures = 0;
    var source = new EventSource(url.toString());
    window.__mnoteTreeLiveEventSource = source;
    applyStatus('connecting');

    source.addEventListener('open', function(){
      failures = 0;
      applyStatus('connected');
    });

    source.addEventListener('snapshot', function(event){
      var payload = JSON.parse(event.data || '{}');
      var revision = payload.revision || payload.cursor || event.lastEventId || null;
      document.documentElement.setAttribute('data-mnote-tree-live-revision', String(revision || ''));
      dispatchTreeEvent('tree:snapshot', { payload: payload, revision: revision, bootstrap: bootstrap });
    });

    source.addEventListener('delta', function(event){
      var payload = JSON.parse(event.data || '{}');
      var revision = payload.revision || payload.cursor || event.lastEventId || null;
      document.documentElement.setAttribute('data-mnote-tree-live-revision', String(revision || ''));
      dispatchTreeEvent('tree:delta', { payload: payload, revision: revision, bootstrap: bootstrap });
    });

    source.addEventListener('resync', function(event){
      var payload = JSON.parse(event.data || '{}');
      var revision = payload.revision || payload.cursor || event.lastEventId || null;
      document.documentElement.setAttribute('data-mnote-tree-live-revision', String(revision || ''));
      dispatchTreeEvent('tree:resync', { payload: payload, revision: revision, bootstrap: bootstrap });
    });

    source.onerror = function(){
      failures += 1;
      applyStatus(failures >= 3 ? 'resync-pending' : 'reconnecting');
    };
  }

  window.__mnoteTreeLiveClose = closeActiveSource;
  window.addEventListener('pagehide', closeActiveSource, { once: true });

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', start, { once: true });
  } else {
    start();
  }
})();
"##;

/// MNOTE Wolai 风格页面布局
///
/// 包含左侧栏 + 内容区的双栏布局。
/// 侧栏显示品牌、导航链接和可选的页面树。
///
/// # 用法
///
/// ```ignore
/// view! {
///     <PageLayout current_nav="home" sidebar_tree_html={None}>
///         <section>...</section>
///     </PageLayout>
/// }
/// ```
#[component]
pub fn PageLayout(
    children: Children,
    current_nav: &'static str,
    /// 侧栏页面树 HTML（可选），由路由 handler 渲染
    #[prop(optional)]
    sidebar_tree_html: Option<String>,
    /// 工作区名称（可选），显示在侧栏顶部
    #[prop(optional)]
    workspace_name: Option<String>,
    /// workspace shell 侧栏 sections HTML（可选），由 projection 渲染
    #[prop(optional)]
    workspace_sidebar_html: Option<String>,
    /// 顶栏当前页面标题（可选）
    #[prop(optional)]
    topbar_title: Option<String>,
) -> impl IntoView {
    let sidebar_tree_html = sidebar_tree_html.unwrap_or_default();

    let ws_name = workspace_name
        .map(|value| value.trim().to_string())
        .filter(|value| !value.is_empty())
        .unwrap_or_else(|| "开发用户 的空间".to_string());
    let topbar_title = topbar_title
        .map(|value| value.trim().to_string())
        .filter(|value| !value.is_empty())
        .unwrap_or_else(|| "个人".to_string());
    let sidebar_sections_html = workspace_sidebar_html
        .map(|value| value.trim().to_string())
        .filter(|value| !value.is_empty())
        .unwrap_or_else(|| {
            let dataset = serde_json::json!({
                "workspaces": [{ "id": "default", "name": ws_name.clone() }],
                "documents": []
            });
            let projection = crate::workspace_shell::build_workspace_shell_projection(
                &dataset, "default", None, &ws_name,
            );
            crate::workspace_shell::render_workspace_shell_sidebar_html(
                &projection,
                Some(sidebar_tree_html.as_str()),
                None,
            )
        });
    let tree_live_bootstrap = serde_json::json!({
        "schema": "mnote.tree_live_bootstrap.v1",
        "transport": "convex-command-log-sse",
        "workspaceId": null,
        "rootIds": [],
        "initialRevision": null,
        "endpoint": "/api/tree/events",
        "views": ["page-tree", "file-tree"]
    })
    .to_string();

    view! {
        <div class="mnote-shell wolai-workspace-shell" data-mnote-web-owner="mnote-web" data-mnote-shell="workspace">
            <aside class="mnote-sidebar wolai-sidebar" data-testid="wolai-sidebar">
                <div class="mnote-sidebar-header wolai-sidebar-header" data-testid="wolai-workspace-identity">
                    <a href="/" class="mnote-sidebar-brand wolai-avatar" aria-label="工作区首页">"L"</a>
                    <button
                        type="button"
                        class="mnote-workspace-source-trigger"
                        data-testid="mnote-workspace-source-trigger"
                        data-mnote-action="open-workspace-source-menu"
                        aria-haspopup="menu"
                        aria-expanded="false"
                        title="切换云空间或本地文件夹"
                    >
                        <span class="sidebar-workspace-name">{ws_name.clone()}</span>
                        <span class="wolai-sidebar-chevron" aria-hidden="true">"⌄"</span>
                    </button>
                </div>
                <nav class="mnote-sidebar-nav wolai-quick-actions" aria-label="快捷操作">
                    <a href="/search" class:active={current_nav == "search"} title="搜索" aria-label="搜索" data-mnote-action="open-search-modal"><span class="material-symbols-outlined nav-icon" data-icon="search" aria-hidden="true"></span></a>
                    <a href="/graph" title="关系图" aria-label="关系图"><span class="material-symbols-outlined nav-icon" data-icon="account_tree" aria-hidden="true"></span></a>
                    <a href="/actions" title="快捷动作" aria-label="快捷动作"><span class="material-symbols-outlined nav-icon" data-icon="bolt" aria-hidden="true"></span></a>
                    <a href="/help" title="帮助" aria-label="帮助"><span class="material-symbols-outlined nav-icon" data-icon="help" aria-hidden="true"></span></a>
                    <a href="/files" title="文件" aria-label="文件"><span class="material-symbols-outlined nav-icon" data-icon="inventory_2" aria-hidden="true"></span></a>
                    <button type="button" title="打开本地文件夹" aria-label="打开本地文件夹" data-mnote-action="open-local-folder"><span class="material-symbols-outlined nav-icon" data-icon="folder_open" aria-hidden="true"></span></button>
                    <a href="/more" title="更多" aria-label="更多"><span class="material-symbols-outlined nav-icon" data-icon="more_horiz" aria-hidden="true"></span></a>
                </nav>
                <div class="wolai-sidebar-body" inner_html={sidebar_sections_html}></div>
                <script id="__MNOTE_TREE_LIVE_BOOTSTRAP__" type="application/json" inner_html={tree_live_bootstrap}></script>
                <script inner_html={SIDEBAR_TREE_JS.to_string()}></script>
                <script id="mnote-tree-live-controller" inner_html={TREE_LIVE_CONTROLLER_JS.to_string()}></script>
            </aside>
            <div class="mnote-main">
                <header class="wolai-topbar" data-testid="wolai-topbar">
                    <div class="wolai-topbar-left">
                        <button type="button" class="wolai-icon-button wolai-menu-button" aria-label="切换侧栏"><span class="material-symbols-outlined" data-icon="menu" aria-hidden="true"></span></button>
                        <nav class="wolai-breadcrumb" aria-label="页面路径">
                            <span class="wolai-breadcrumb-root">{ws_name.clone()}</span>
                            <span class="wolai-breadcrumb-separator" aria-hidden="true">"›"</span>
                            <span class="wolai-breadcrumb-current"><span class="material-symbols-outlined wolai-home-icon" data-icon="home" aria-hidden="true"></span><span data-page-title-current="true">{topbar_title}</span></span>
                        </nav>
                    </div>
                    <div class="wolai-topbar-actions" aria-label="页面操作">
                        <span class="wolai-public-pill" data-testid="wolai-public-state">"全网公开"</span>
                        <button type="button" class="wolai-icon-button" title="收藏" aria-label="收藏"><span class="material-symbols-outlined" data-icon="star" aria-hidden="true"></span></button>
                        <button type="button" class="wolai-icon-button" title="演示" aria-label="演示"><span class="material-symbols-outlined" data-icon="slideshow" aria-hidden="true"></span></button>
                        <button type="button" class="wolai-icon-button" title="评论" aria-label="评论"><span class="material-symbols-outlined" data-icon="comment" aria-hidden="true"></span></button>
                        <button type="button" class="wolai-icon-button" title="分享" aria-label="分享"><span class="material-symbols-outlined" data-icon="share" aria-hidden="true"></span></button>
                        <button type="button" class="wolai-icon-button" title="成员" aria-label="成员"><span class="material-symbols-outlined" data-icon="person_add" aria-hidden="true"></span></button>
                        <button type="button" class="wolai-icon-button" title="页面历史" aria-label="历史" data-mnote-action="open-page-history"><span class="material-symbols-outlined" data-icon="history" aria-hidden="true"></span></button>
                        <button type="button" class="wolai-icon-button" title="页面选项和全局选项" aria-label="更多" data-testid="wolai-page-settings-trigger" data-mnote-action="open-page-settings"><span class="material-symbols-outlined" data-icon="more_horiz" aria-hidden="true"></span></button>
                    </div>
                </header>
                <article class="mnote-content">
                    {children()}
                </article>
                <div class="wolai-floating-actions" aria-label="浮动操作">
                    <button type="button" data-testid="wolai-floating-help" class="wolai-floating-button wolai-floating-button--help" aria-label="帮助"><span class="material-symbols-outlined" data-icon="help" aria-hidden="true"></span></button>
                    <button type="button" data-testid="wolai-floating-ai" class="wolai-floating-button wolai-floating-button--ai" aria-label="AI 助手" title="点击 进入空间智能问答" data-mnote-action="open-page-ai"><span class="material-symbols-outlined material-symbols-filled" data-icon="auto_awesome" aria-hidden="true"></span></button>
                </div>
            </div>
        </div>
    }
}

#[cfg(test)]
mod tests {
    use super::{SIDEBAR_TREE_JS, TREE_LIVE_CONTROLLER_JS};

    #[test]
    fn sidebar_tree_runtime_handles_navigation_drag_and_filetree_actions() {
        assert!(SIDEBAR_TREE_JS.contains("mnoteNavigationInFlight"));
        assert!(SIDEBAR_TREE_JS.contains("data-mnote-navigation-pending"));
        assert!(SIDEBAR_TREE_JS.contains("MNOTE_SIDEBAR_TREE_MODE_KEY"));
        assert!(SIDEBAR_TREE_JS.contains("MNOTE_GLOBAL_SHOW_HEADING_NUMBERS_KEY"));
        assert!(SIDEBAR_TREE_JS.contains("MNOTE_LAST_CLOUD_WORKSPACE_KEY"));
        assert!(SIDEBAR_TREE_JS.contains("data-global-option-checkbox=\"showHeadingNumbers\""));
        assert!(SIDEBAR_TREE_JS.contains("restoreSidebarTreeTab"));
        assert!(SIDEBAR_TREE_JS.contains("treeView"));
        assert!(SIDEBAR_TREE_JS.contains("tree:local-command"));
        assert!(SIDEBAR_TREE_JS.contains("applyCreatedDocumentLocally"));
        assert!(SIDEBAR_TREE_JS.contains("data-mnote-tree-local-command-applied', 'create"));
        assert!(SIDEBAR_TREE_JS.contains("data-mnote-tree-local-command-applied', 'remove"));
        assert!(SIDEBAR_TREE_JS.contains("wolai:assets-changed"));
        assert!(SIDEBAR_TREE_JS.contains("applyAssetsChangedToFileTree"));
        assert!(SIDEBAR_TREE_JS.contains("installMindmapAssetFetchObserver"));
        assert!(SIDEBAR_TREE_JS.contains("parseMindmapApiTarget"));
        assert!(SIDEBAR_TREE_JS.contains("data-mnote-assets-local-applied"));
        assert!(SIDEBAR_TREE_JS.contains("application/x-mnote-page-tree-node"));
        assert!(SIDEBAR_TREE_JS.contains("dragstart"));
        assert!(SIDEBAR_TREE_JS.contains("drop"));
        assert!(SIDEBAR_TREE_JS.contains("data-drop-feedback"));
        assert!(SIDEBAR_TREE_JS.contains("action: 'move'"));
        assert!(SIDEBAR_TREE_JS.contains("sidebar-file-tree-root"));
        assert!(SIDEBAR_TREE_JS.contains("tree.filetree.open"));
        assert!(SIDEBAR_TREE_JS.contains("tree.asset.open"));
        assert!(SIDEBAR_TREE_JS.contains("openConvexAssetFromFileTree"));
        assert!(SIDEBAR_TREE_JS.contains("buildMindmapOpenPath"));
        assert!(SIDEBAR_TREE_JS.contains("isMindmapAssetDetail"));
        assert!(!SIDEBAR_TREE_JS.contains("openMindmapAssetInDocumentShell"));
        assert!(SIDEBAR_TREE_JS.contains("data-mnote-last-mindmap-asset-open-mode"));
        assert!(SIDEBAR_TREE_JS.contains("mindmap-object-shell"));
        assert!(SIDEBAR_TREE_JS.contains("window.location.assign(mindmapPath)"));
        assert!(SIDEBAR_TREE_JS.contains("assetType: assetType || null"));
        assert!(SIDEBAR_TREE_JS.contains("data-object-identity"));
        assert!(SIDEBAR_TREE_JS.contains("readFileTreeObjectIdentity"));
        assert!(SIDEBAR_TREE_JS.contains("objectIdentity: objectIdentity"));
        assert!(SIDEBAR_TREE_JS.contains("workspaceId: resolveWorkspaceId(fileRow)"));
        assert!(SIDEBAR_TREE_JS.contains("/api/media/sign?assetId="));
        assert!(SIDEBAR_TREE_JS.contains("fetchCurrentOnlyOfficeUserId"));
        assert!(SIDEBAR_TREE_JS.contains("/api/auth/whoami"));
        assert!(SIDEBAR_TREE_JS.contains("buildOnlyOfficeOpenUrl"));
        assert!(SIDEBAR_TREE_JS.contains("target.searchParams.set('userId'"));
        assert!(SIDEBAR_TREE_JS.contains("window.open(buildOnlyOfficeOpenUrl"));
        assert!(SIDEBAR_TREE_JS.contains("tree.filetree.internal-drop"));
        assert!(SIDEBAR_TREE_JS.contains("tree.filetree.external-drop"));
        assert!(SIDEBAR_TREE_JS.contains("MNOTE_RECENT_LOCAL_ROOTS_KEY"));
        assert!(SIDEBAR_TREE_JS.contains("requestOpenLocalFolder"));
        assert!(SIDEBAR_TREE_JS.contains("sourceKind', 'local_folder"));
        assert!(SIDEBAR_TREE_JS.contains("switchToCloudWorkspace"));
        assert!(SIDEBAR_TREE_JS.contains("mnote-switch-cloud-workspace"));
        assert!(SIDEBAR_TREE_JS.contains("mnote-recent-local-root"));
    }

    #[test]
    fn sidebar_tree_runtime_renders_context_menu_and_scoped_title_updates() {
        assert!(SIDEBAR_TREE_JS.contains("openTreeContextMenu"));
        assert!(SIDEBAR_TREE_JS.contains("mnote-tree-context-menu"));
        assert!(SIDEBAR_TREE_JS.contains("复制访问链接"));
        assert!(SIDEBAR_TREE_JS.contains("删除到垃圾桶"));
        assert!(SIDEBAR_TREE_JS.contains(
            r#".tree-row[data-shell-mode="page"][data-node-id="' + escaped + '"] > .tree-link > .tree-link-title"#
        ));
        assert!(SIDEBAR_TREE_JS.contains(
            r#".tree-row[data-shell-mode="filetree"][data-row-id="' + escapedDocRowId + '"] > .tree-link > .tree-link-title"#
        ));
        assert!(!SIDEBAR_TREE_JS.contains(
            r#".tree-row[data-document-id="' + escaped + '"] > .tree-link > .tree-link-title"#
        ));
        assert!(!SIDEBAR_TREE_JS
            .contains(r#"[data-node-id="' + cssEscape(documentId) + '"] .tree-link-title"#));
        assert!(SIDEBAR_TREE_JS.contains("renderSidebarSnapshot"));
        assert!(SIDEBAR_TREE_JS.contains("kernel_file_tree_projection"));
        assert!(SIDEBAR_TREE_JS.contains("deltaNeedsProjectionRefresh"));
        assert!(SIDEBAR_TREE_JS.contains("upsert_documents"));
        assert!(SIDEBAR_TREE_JS.contains("setTreeLiveApplyError"));
        assert!(!SIDEBAR_TREE_JS.contains("scheduleProjectionRefresh"));
        assert!(!SIDEBAR_TREE_JS.contains("/api/tree/projections/sidebar"));
        assert!(SIDEBAR_TREE_JS.contains("/api/tree/projections/file"));
    }

    #[test]
    fn sidebar_tree_runtime_keeps_pdf_and_code_assets_out_of_onlyoffice() {
        assert!(SIDEBAR_TREE_JS.contains("function inferOnlyOfficeFileType"));
        assert!(SIDEBAR_TREE_JS.contains("isNonOfficeAttachmentName(name, ext)"));
        assert!(!SIDEBAR_TREE_JS.contains("if (ext === 'pdf') return ext;"));
        assert!(!SIDEBAR_TREE_JS.contains("mt.indexOf('pdf') >= 0"));
        assert!(SIDEBAR_TREE_JS.contains("mnote-uploaded-attachment-pdf"));
        assert!(SIDEBAR_TREE_JS.contains("mnote-uploaded-attachment-code"));
        assert!(SIDEBAR_TREE_JS.contains("'toml'"));
        assert!(SIDEBAR_TREE_JS.contains("'json'"));
        assert!(SIDEBAR_TREE_JS.contains("'yaml'"));
        assert!(SIDEBAR_TREE_JS.contains("'md'"));
        assert!(SIDEBAR_TREE_JS.contains("'vue'"));
        assert!(SIDEBAR_TREE_JS.contains("'svelte'"));
        assert!(SIDEBAR_TREE_JS.contains("'proto'"));
        assert!(SIDEBAR_TREE_JS.contains("'dockerfile'"));
        assert!(SIDEBAR_TREE_JS.contains("'.gitignore'"));
    }

    #[test]
    fn sidebar_tree_runtime_opens_pdf_and_code_assets_with_builtin_tools() {
        assert!(SIDEBAR_TREE_JS.contains("function openPdfEditorAttachment"));
        assert!(SIDEBAR_TREE_JS.contains("function openCodeEditorAttachment"));
        assert!(SIDEBAR_TREE_JS.contains("function resolveEditorAttachmentUrl"));
        assert!(SIDEBAR_TREE_JS.contains("type: 'codeBlock'"));
        assert!(SIDEBAR_TREE_JS.contains("attrs: { language: language }"));
        assert!(SIDEBAR_TREE_JS.contains("inferCodeAttachmentLanguage"));
        assert!(SIDEBAR_TREE_JS.contains("if (isPdfAttachmentFileName(detail.fileName))"));
        assert!(SIDEBAR_TREE_JS.contains("if (isCodeAttachmentFileName(detail.fileName))"));
        assert!(SIDEBAR_TREE_JS
            .contains("if (isCodeAttachmentFileName(fileName) && String(asset.document_id"));
        assert!(SIDEBAR_TREE_JS.contains("await openCodeEditorAttachment({"));
    }

    #[test]
    fn tree_live_controller_marks_transport_and_closes_source_on_pagehide() {
        assert!(TREE_LIVE_CONTROLLER_JS.contains("convex-command-log-sse"));
        assert!(TREE_LIVE_CONTROLLER_JS.contains("data-mnote-tree-live-transport"));
        assert!(TREE_LIVE_CONTROLLER_JS.contains("pagehide"));
        assert!(TREE_LIVE_CONTROLLER_JS.contains("__mnoteTreeLiveEventSource"));
        assert!(TREE_LIVE_CONTROLLER_JS.contains("tree:snapshot"));
        assert!(TREE_LIVE_CONTROLLER_JS.contains("tree:delta"));
        assert!(TREE_LIVE_CONTROLLER_JS.contains("tree:resync"));
        assert!(!TREE_LIVE_CONTROLLER_JS.contains("resyncEndpoint"));
        assert!(!TREE_LIVE_CONTROLLER_JS.contains("tree:resync-requested"));
    }
}
