use crate::app::{AppConfig, AppState};
use crate::context::RequestContext;
use crate::document_buffer_store::{self};
use crate::error::WebError;
use crate::page_aggregate::PageAggregate;
use crate::routes::documents::{
    load_document_content_result, load_document_meta_result, DocumentContentQuery,
    DocumentMetaQuery,
};
use crate::routes::gateway::default_workspace_name_for_context;
use crate::routes::local_folder_source::{
    ensure_local_workspace_read_access_with_state, is_local_access_policy_admin_context,
    load_local_folder_file_tree_snapshot, load_local_folder_page_tree_snapshot,
    resolve_local_markdown_page_aggregate,
};
use crate::routes::query_support::execute_runtime_query_against_data;
use crate::routes::snapshot_support::{
    execute_kernel_query, load_projection_snapshot, projection_query, ProjectionSnapshotSpec,
};
use crate::routes::tree::{collect_filetree_render_rows, collect_page_tree_render_rows};
use crate::ssr::pages::document::DocumentPage;
use crate::tree_shell::filetree_renderer::{
    render_initial_filetree_html, FileTreeInitialRenderInput,
};
use crate::tree_shell::page_renderer::{render_initial_page_tree_html, PageTreeInitialRenderInput};
use crate::workspace_shell::{
    apply_active_page, build_workspace_shell_projection, render_workspace_shell_sidebar_html,
    WorkspaceShellProjection,
};
use axum::body::Body;
use axum::extract::{Extension, Path, Query, State};
use axum::http::{header, HeaderMap, HeaderName, HeaderValue, StatusCode};
use axum::response::{Html, IntoResponse, Response};
use axum::Json;
use bridge_runtime::RuntimeQueryEnvelopeWire;
use core_protocol::KernelProjectionKind;
use serde::Deserialize;
use serde_json::json;
use std::fs;
use std::path::{Component, Path as FsPath, PathBuf};

const HEADER_MNOTE_WEB_OWNER: &str = "x-mnote-web-owner";
const HEADER_MNOTE_WEB_SHELL: &str = "x-mnote-web-shell";
const COOKIE_RECENT_PAGE_ID: &str = "mnote_recent_page_id";

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DocumentShellQuery {
    pub workspace_id: Option<String>,
    pub source_kind: Option<String>,
    pub root_uri: Option<String>,
    pub secondary_document_id: Option<String>,
    pub secondary_source_kind: Option<String>,
    pub secondary_root_uri: Option<String>,
}

pub async fn document_page_shell(
    State(state): State<AppState>,
    Extension(context): Extension<RequestContext>,
    Path(document_id): Path<String>,
    Query(query): Query<DocumentShellQuery>,
) -> Result<Response, WebError> {
    let primary_source_kind = normalize_source_kind(query.source_kind.as_deref());
    let primary_root_uri = normalize_optional_query_value(query.root_uri.as_deref());
    let aggregate = build_page_aggregate_snapshot(
        &state,
        &context,
        &document_id,
        query.workspace_id.as_deref(),
        primary_source_kind,
        primary_root_uri,
    )
    .await?;

    // 初始化 BufferStore：local_folder 文档打开时记录 file_version
    if primary_source_kind == Some("local_folder") {
        if let Some(root_uri) = primary_root_uri {
            let relative_path = document_id
                .strip_prefix("local-md:")
                .unwrap_or("")
                .replace("~2F", "/");
            let file_version = aggregate.body.file_version.as_str().map(|s| s.to_string());
            let ws_path = document_buffer_store::build_local_folder_workspace_path(
                &aggregate.identity.workspace_id,
                root_uri,
                &relative_path,
                &document_id,
            );
            state.buffer_store.init_buffer(&ws_path, file_version, None);
        }
    }

    let title = aggregate.head.title.as_str();
    let workspace_id = aggregate.identity.workspace_id.clone();
    let requested_secondary_document_id =
        normalize_optional_owned(query.secondary_document_id.as_deref());
    let secondary_source_kind = normalize_source_kind(
        query
            .secondary_source_kind
            .as_deref()
            .or(primary_source_kind),
    );
    let secondary_root_uri =
        normalize_optional_query_value(query.secondary_root_uri.as_deref().or(primary_root_uri));
    let mut secondary_requested = false;
    let mut secondary_invalid = false;
    let secondary_aggregate =
        if let Some(secondary_document_id) = requested_secondary_document_id.as_deref() {
            secondary_requested = true;
            match build_page_aggregate_snapshot(
                &state,
                &context,
                secondary_document_id,
                query.workspace_id.as_deref(),
                secondary_source_kind,
                secondary_root_uri,
            )
            .await
            {
                Ok(aggregate) => Some(aggregate),
                Err(_) => {
                    secondary_invalid = true;
                    None
                }
            }
        } else {
            None
        };
    let default_workspace_name = default_workspace_name_for_context(&state, &context);
    let mut workspace_projection = load_workspace_shell_projection(
        state.config(),
        &context,
        &workspace_id,
        Some(&document_id),
        &default_workspace_name,
    )
    .await;
    apply_active_page(&mut workspace_projection, Some(&document_id));
    let is_local_folder = query
        .source_kind
        .as_deref()
        .map(str::trim)
        .filter(|value| !value.is_empty())
        == Some("local_folder");
    let (sidebar_tree_html, file_tree_html) = if is_local_folder {
        let root_uri = query.root_uri.as_deref().unwrap_or_default();
        (
            render_local_sidebar_tree_html(root_uri, Some(&document_id)).unwrap_or_default(),
            render_local_file_tree_html(root_uri, Some(&document_id), None).unwrap_or_default(),
        )
    } else {
        (
            load_sidebar_tree_html(state.config(), &context, &workspace_id, Some(&document_id))
                .await
                .unwrap_or_default(),
            load_file_tree_html(
                state.config(),
                &context,
                &workspace_id,
                Some(&document_id),
                None,
            )
            .await
            .unwrap_or_default(),
        )
    };
    let workspace_sidebar_html = render_workspace_shell_sidebar_html(
        &workspace_projection,
        Some(sidebar_tree_html.as_str()),
        Some(file_tree_html.as_str()),
    );
    let workspace_name = workspace_projection.workspace_name.clone();
    let page_subtree_json =
        serde_json::to_string(&aggregate.tree.page_subtree).unwrap_or_else(|_| "null".to_string());
    let page_options_json = serde_json::to_string(&aggregate.layout.page_options)
        .unwrap_or_else(|_| "null".to_string());
    let snapshot_json = serde_json::to_string(&aggregate).unwrap_or_else(|_| "null".to_string());
    let bootstrap_json =
        build_editor_bootstrap_json(&aggregate, &context, primary_source_kind, primary_root_uri);
    let secondary_page_subtree_json = secondary_aggregate.as_ref().map(|aggregate| {
        serde_json::to_string(&aggregate.tree.page_subtree).unwrap_or_else(|_| "null".to_string())
    });
    let secondary_page_options_json = secondary_aggregate.as_ref().map(|aggregate| {
        serde_json::to_string(&aggregate.layout.page_options).unwrap_or_else(|_| "null".to_string())
    });
    let secondary_snapshot_json = secondary_aggregate
        .as_ref()
        .map(|aggregate| serde_json::to_string(aggregate).unwrap_or_else(|_| "null".to_string()));
    let secondary_bootstrap_json = secondary_aggregate.as_ref().map(|aggregate| {
        build_editor_bootstrap_json_with_ids(
            aggregate,
            &context,
            secondary_source_kind,
            secondary_root_uri,
            "__MNOTE_SECONDARY_PAGE_AGGREGATE__",
            "secondary",
        )
    });
    let panes_bootstrap_json = build_document_panes_bootstrap_json(
        &aggregate,
        &context,
        primary_source_kind,
        primary_root_uri,
        secondary_aggregate.as_ref(),
        secondary_source_kind,
        secondary_root_uri,
        secondary_requested,
        secondary_invalid,
    );
    let body_content = crate::ssr::render_view(leptos::view! {
        <DocumentPage
            title={title.to_string()}
            document_id={document_id.clone()}
            workspace_id={workspace_id.clone()}
            sidebar_tree_html={sidebar_tree_html}
            workspace_name={workspace_name}
            workspace_sidebar_html={workspace_sidebar_html}
            page_subtree_json={page_subtree_json}
            page_options_json={page_options_json}
            secondary_title={secondary_aggregate.as_ref().map(|aggregate| aggregate.head.title.clone()).unwrap_or_default()}
            secondary_document_id={secondary_aggregate.as_ref().map(|aggregate| aggregate.identity.document_id.clone()).unwrap_or_default()}
            secondary_workspace_id={secondary_aggregate.as_ref().map(|aggregate| aggregate.identity.workspace_id.clone()).unwrap_or_default()}
            secondary_page_subtree_json={secondary_page_subtree_json.unwrap_or_default()}
            secondary_page_options_json={secondary_page_options_json.unwrap_or_default()}
            primary_hide_title_header={aggregate.layout.page_options.hide_title_header}
            secondary_hide_title_header={secondary_aggregate.as_ref().map(|aggregate| aggregate.layout.page_options.hide_title_header).unwrap_or(secondary_source_kind == Some("local_folder"))}
            show_admin_access_policy={is_local_access_policy_admin_context(&context)}
        />
    });
    let hermes_settings_config_script = render_hermes_settings_config_script();
    let html = format!(
        r#"<!doctype html>
<html lang="zh-CN">
  <head>
    <meta charset="utf-8">
    <title>{}</title>
    <style>{}</style>
  </head>
  <body data-mnote-web-owner="mnote-web" data-mnote-shell="document" data-document-id="{}" data-mnote-source-kind="{}" data-mnote-root-uri="{}" data-secondary-requested="{}" data-secondary-invalid="{}">
    {}
    <script id="__MNOTE_PAGE_AGGREGATE__" type="application/json">{}</script>
    <script id="__MNOTE_EDITOR_BOOTSTRAP__" type="application/json">{}</script>
    <script id="__MNOTE_DOCUMENT_PANES_BOOTSTRAP__" type="application/json">{}</script>
    {}
    {}
    {}
    {}
    {}
    {}
  </body>
</html>"#,
        escape_html(title),
        crate::ssr::MNOTE_CSS,
        escape_html(&document_id),
        escape_html(primary_source_kind.unwrap_or("convex_workspace")),
        escape_html(primary_root_uri.unwrap_or("")),
        secondary_requested,
        secondary_invalid,
        body_content,
        escape_script_json(&snapshot_json),
        escape_script_json(&bootstrap_json),
        escape_script_json(&panes_bootstrap_json),
        hermes_settings_config_script,
        secondary_snapshot_json
            .as_ref()
            .map(|value| format!(r#"<script id="__MNOTE_SECONDARY_PAGE_AGGREGATE__" type="application/json">{}</script>"#, escape_script_json(value)))
            .unwrap_or_default(),
        secondary_bootstrap_json
            .as_ref()
            .map(|value| format!(r#"<script id="__MNOTE_SECONDARY_EDITOR_BOOTSTRAP__" type="application/json">{}</script>"#, escape_script_json(value)))
            .unwrap_or_default(),
        render_document_title_controller_script(),
        render_editor_island_adapter_script(),
        r#"<script type="module" src="/api/mnote-browser-runtime/document-conflict-panel-runtime.js"></script>"#.to_string(),
    );
    let mut response = Html(html).into_response();
    stamp_shell_headers(response.headers_mut(), "document");
    stamp_recent_page_cookie(response.headers_mut(), &document_id);
    Ok(response)
}

pub(crate) fn build_editor_bootstrap_json(
    aggregate: &PageAggregate,
    context: &RequestContext,
    source_kind: Option<&str>,
    root_uri: Option<&str>,
) -> String {
    build_editor_bootstrap_json_with_ids(
        aggregate,
        context,
        source_kind,
        root_uri,
        "__MNOTE_PAGE_AGGREGATE__",
        "primary",
    )
}

fn render_hermes_settings_config_script() -> String {
    let Some(base_url) = [
        "MNOTE_WEB_HERMES_UPSTREAM_URL",
        "MNOTE_HERMES_UPSTREAM_URL",
        "MNOTE_HERMES_API_BASE_URL",
    ]
    .into_iter()
    .find_map(env_or_dotenv)
    .map(|value| value.trim().trim_end_matches('/').to_string())
    .filter(|value| !value.is_empty()) else {
        return String::new();
    };
    let settings_url = format!("{base_url}/hermes/settings");
    let encoded = serde_json::to_string(&settings_url).unwrap_or_else(|_| "\"\"".to_string());
    format!(
        r#"<script>window.__mnoteHermesSettingsUrl = {};</script>"#,
        escape_script_json(&encoded)
    )
}

fn env_or_dotenv(key: &str) -> Option<String> {
    if let Ok(value) = std::env::var(key) {
        let trimmed = value.trim().trim_matches('"').to_string();
        if !trimmed.is_empty() {
            return Some(trimmed);
        }
    }
    if cfg!(test) {
        return None;
    }
    let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("../../..")
        .join(".env.all");
    let content = fs::read_to_string(root).ok()?;
    for line in content.lines() {
        let line = line.trim_end_matches('\r');
        if line.starts_with('#') || line.trim().is_empty() {
            continue;
        }
        let Some((candidate_key, value)) = line.split_once('=') else {
            continue;
        };
        if candidate_key.trim() != key {
            continue;
        }
        let trimmed = value.trim().trim_matches('"').to_string();
        if !trimmed.is_empty() {
            return Some(trimmed);
        }
    }
    None
}

pub(crate) fn build_editor_bootstrap_json_with_ids(
    aggregate: &PageAggregate,
    context: &RequestContext,
    source_kind: Option<&str>,
    root_uri: Option<&str>,
    page_aggregate_script_id: &str,
    pane_role: &str,
) -> String {
    let normalized_source_kind = source_kind
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .unwrap_or("convex_workspace");
    let save_endpoint = if normalized_source_kind == "local_folder" {
        "/api/page-body/write"
    } else {
        "/api/documents/save"
    };
    serde_json::to_string(&json!({
        "schema": "mnote.editor_bootstrap.v1",
        "documentId": aggregate.identity.document_id,
        "workspaceId": aggregate.identity.workspace_id,
        "paneRole": pane_role,
        "sourceKind": normalized_source_kind,
        "rootUri": root_uri
            .map(str::trim)
            .filter(|value| !value.is_empty())
            .unwrap_or(""),
        "pageAggregateScriptId": page_aggregate_script_id,
        "saveEndpoint": save_endpoint,
        "titleEndpoint": "/api/documents/title",
        "editorHostKind": "leptos_tiptap_island",
        "assetMode": "rust-web-leptos-tiptap-spike-island-bundle",
        "requestId": context.trace.request_id,
        "traceId": context.trace.trace_id,
    }))
    .unwrap_or_else(|_| "{}".to_string())
}

pub(crate) fn build_document_panes_bootstrap_json(
    aggregate: &PageAggregate,
    context: &RequestContext,
    source_kind: Option<&str>,
    root_uri: Option<&str>,
    secondary_aggregate: Option<&PageAggregate>,
    secondary_source_kind: Option<&str>,
    secondary_root_uri: Option<&str>,
    secondary_requested: bool,
    secondary_invalid: bool,
) -> String {
    let primary = json!({
        "role": "primary",
        "aggregate": aggregate,
        "bootstrap": serde_json::from_str::<serde_json::Value>(&build_editor_bootstrap_json_with_ids(
            aggregate,
            context,
            source_kind,
            root_uri,
            "__MNOTE_PAGE_AGGREGATE__",
            "primary",
        )).unwrap_or_else(|_| json!({})),
    });
    let secondary = secondary_aggregate.map(|aggregate| {
        json!({
            "role": "secondary",
            "aggregate": aggregate,
            "bootstrap": serde_json::from_str::<serde_json::Value>(&build_editor_bootstrap_json_with_ids(
                aggregate,
                context,
                secondary_source_kind,
                secondary_root_uri,
                "__MNOTE_SECONDARY_PAGE_AGGREGATE__",
                "secondary",
            )).unwrap_or_else(|_| json!({})),
        })
    });
    serde_json::to_string(&json!({
        "schema": "mnote.document_panes_bootstrap.v1",
        "secondaryRequested": secondary_requested,
        "secondaryInvalid": secondary_invalid,
        "panes": match secondary {
            Some(secondary) => vec![primary, secondary],
            None => vec![primary],
        },
    }))
    .unwrap_or_else(|_| "{}".to_string())
}

fn normalize_optional_query_value(value: Option<&str>) -> Option<&str> {
    value.map(str::trim).filter(|value| !value.is_empty())
}

fn normalize_source_kind(value: Option<&str>) -> Option<&str> {
    normalize_optional_query_value(value)
}

fn normalize_optional_owned(value: Option<&str>) -> Option<String> {
    normalize_optional_query_value(value).map(ToOwned::to_owned)
}

pub(crate) fn render_document_title_controller_script() -> &'static str {
    r#"<script>
(() => {
  const CONTRACT = 'mnote.document_title_controller.v1';
  const inputs = Array.from(document.querySelectorAll('[data-page-title-input="true"]')).filter((node) => node instanceof HTMLTextAreaElement);
  if (!inputs.length) return;

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

  const autosize = (input) => {
    input.style.height = 'auto';
    input.style.height = `${Math.max(48, input.scrollHeight)}px`;
  };

  const setStatus = (input, status, message) => {
    input.setAttribute('data-title-save-status', status);
    const shell = input.closest('.document-shell');
    if (shell instanceof HTMLElement) shell.setAttribute('data-title-save-status', status);
    if (message) input.setAttribute('data-title-save-error', message);
    else input.removeAttribute('data-title-save-error');
  };

  const setText = (selector, title) => {
    document.querySelectorAll(selector).forEach((node) => {
      if (node instanceof HTMLElement) node.textContent = title;
    });
  };

  const fileTreePageTitle = (value) => {
    const normalized = String(value || '无标题').trim() || '无标题';
    return normalized.toLowerCase().endsWith('.md') ? normalized : `${normalized}.md`;
  };

  const updateVisibleTitle = (input, title, documentId) => {
    const pane = input.closest('[data-document-pane="true"]');
    const isPrimaryDocument = documentId && document.body?.dataset.documentId === documentId;
    if (isPrimaryDocument) {
      document.title = title;
      const topbarCurrent = document.querySelector('.wolai-breadcrumb-current [data-page-title-current]');
      if (topbarCurrent instanceof HTMLElement) topbarCurrent.textContent = title;
      const pageTabTitle = document.querySelector('[data-mnote-main-tab="page"] .mnote-main-tab-title');
      if (pageTabTitle instanceof HTMLElement) pageTabTitle.textContent = title;
    }
    if (documentId) {
      const escapedId = cssEscape(documentId);
      document.querySelectorAll(`[data-page-title-input="true"][data-document-id="${escapedId}"]`).forEach((node) => {
        if (!(node instanceof HTMLTextAreaElement) || node === input) return;
        node.value = title;
        autosize(node);
        setStatus(node, 'saved');
      });
      document.querySelectorAll(`[data-document-pane="true"][data-pane-document-id="${escapedId}"] [data-page-title-current="true"]`).forEach((node) => {
        if (node instanceof HTMLElement && node !== input) node.textContent = title;
      });
    } else {
      pane?.querySelectorAll('[data-page-title-current]').forEach((node) => {
        if (node instanceof HTMLElement && node !== input) node.textContent = title;
      });
    }
    const current = document.querySelector('.wolai-breadcrumb-current');
    if (current instanceof HTMLElement && isPrimaryDocument) {
      let titleNode = current.querySelector('[data-page-title-current]');
      if (!(titleNode instanceof HTMLElement)) {
        titleNode = document.createElement('span');
        titleNode.setAttribute('data-page-title-current', 'true');
        current.appendChild(titleNode);
      }
      titleNode.textContent = title;
    }
    if (!documentId) return;
    const escapedId = cssEscape(documentId);
    const escapedDocRowId = cssEscape(`doc:${documentId}`);
    setText(`.tree-row[data-shell-mode="page"][data-node-id="${escapedId}"] > .tree-link > .tree-link-title`, title);
    setText(`.tree-row[data-shell-mode="filetree"][data-row-id="${escapedDocRowId}"] > .tree-link > .tree-link-title`, fileTreePageTitle(title));
    setText(`.wolai-page-row[data-node-id="${escapedId}"] > .wolai-row-title`, title);
    setText(`a[href="/documents/${escapedId}"] > .wolai-row-title`, title);
    setText(`a[href^="/documents/${escapedId}?"] > .wolai-row-title`, title);
  };

  inputs.forEach((input) => {
    input.setAttribute('data-title-controller', CONTRACT);
    const endpoint = input.getAttribute('data-title-endpoint') || '/api/documents/title';
    const resolveTitleTarget = (targetInput) => {
      const paneRole = (targetInput.getAttribute('data-pane-role') || 'primary').trim();
      const rawDocumentId = (targetInput.getAttribute('data-document-id') || '').trim();
      const documentId = rawDocumentId || (
        paneRole === 'primary'
          ? (document.body?.dataset.documentId || '').trim()
          : ''
      );
      const query = new URLSearchParams(window.location.search);
      const paneQueryParams = paneRole === 'secondary'
        ? { sourceKindParam: 'secondarySourceKind', rootUriParam: 'secondaryRootUri' }
        : { sourceKindParam: 'sourceKind', rootUriParam: 'rootUri' };
      return {
        documentId,
        workspaceId: (targetInput.getAttribute('data-workspace-id') || query.get('workspaceId') || '').trim(),
        sourceKind: (query.get(paneQueryParams.sourceKindParam) || '').trim(),
        rootUri: (query.get(paneQueryParams.rootUriParam) || '').trim(),
      };
    };
    const readLastSavedTitle = () => input.getAttribute('data-title-last-saved') || '无标题';
    const writeLastSavedTitle = (title) => {
      input.setAttribute('data-title-last-saved', title || '无标题');
    };
    writeLastSavedTitle(input.value.trim() || '无标题');
    let saving = false;

    const saveTitle = async () => {
    const title = input.value.trim() || '无标题';
    const currentTarget = resolveTitleTarget(input);
    autosize(input);
    if (!currentTarget.documentId || saving || title === readLastSavedTitle()) {
      updateVisibleTitle(input, title, currentTarget.documentId);
      setStatus(input, 'saved');
      return;
    }
    saving = true;
    setStatus(input, 'saving');
    try {
      const response = await fetch(endpoint, {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({
          documentId: currentTarget.documentId,
          workspaceId: currentTarget.workspaceId || null,
          sourceKind: currentTarget.sourceKind || undefined,
          rootUri: currentTarget.rootUri || undefined,
          title,
          commandName: 'page.head.updateTitle',
        }),
      });
      const payload = await response.json().catch(() => null);
      if (!response.ok || !payload || payload.ok !== true) {
        throw new Error(payload?.error?.message || payload?.message || `title_failed_${response.status}`);
      }
      const result = payload?.result || {};
      const nextDocumentId = String(result.documentId || result.id || currentTarget.documentId || '').trim();
      const previousDocumentId = currentTarget.documentId;
      const nextTitle = String(result.title || title || '无标题').trim() || '无标题';
      if (nextDocumentId) {
        input.setAttribute('data-document-id', nextDocumentId);
      }
      writeLastSavedTitle(nextTitle);
      updateVisibleTitle(input, nextTitle, nextDocumentId || currentTarget.documentId);
      setStatus(input, 'saved');
      window.dispatchEvent(new CustomEvent('tree:title-updated', {
        detail: {
          documentId: nextDocumentId || currentTarget.documentId,
          previousDocumentId,
          workspaceId: currentTarget.workspaceId || null,
          title: nextTitle,
          payload,
        },
      }));
    } catch (error) {
      setStatus(input, 'error', error instanceof Error ? error.message : String(error));
    } finally {
      saving = false;
    }
  };

    input.addEventListener('input', () => {
      autosize(input);
      setStatus(input, (input.value.trim() || '无标题') === readLastSavedTitle() ? 'saved' : 'dirty');
    });
    input.addEventListener('keydown', (event) => {
      if (event.key === 'Enter' && !event.shiftKey) {
        event.preventDefault();
        input.blur();
      }
    });
    input.addEventListener('blur', () => { void saveTitle(); });
    autosize(input);
    updateVisibleTitle(input, readLastSavedTitle(), resolveTitleTarget(input).documentId);
    setStatus(input, 'saved');
  });
})();
</script>"#
}

pub(crate) fn render_editor_island_adapter_script() -> &'static str {
    r#"<script type="module">
(() => {
  const PANES_BOOTSTRAP_ID = '__MNOTE_DOCUMENT_PANES_BOOTSTRAP__';
  const ROOT_SELECTOR = '[data-testid="mnote-leptos-tiptap-island-editor-root"]';
  const EVENT_PREFIX = 'mnote:leptos-tiptap-spike';
  const CHANGE_EVENT = `${EVENT_PREFIX}:change`;
  const SAVE_EVENT = `${EVENT_PREFIX}:save-request`;
  const READY_EVENT = `${EVENT_PREFIX}:ready`;
  const STATE_EVENT = `${EVENT_PREFIX}:state`;
  const ERROR_EVENT = `${EVENT_PREFIX}:error`;
  const COMMAND_EVENT = `${EVENT_PREFIX}:command`;
  const BRIDGE_PROTOCOL = 'mnote.leptos_tiptap.bridge.v1';

  const parseJsonScript = (id) => {
    const node = document.getElementById(id);
    if (!node) return null;
    try {
      return JSON.parse(node.textContent || 'null');
    } catch (error) {
      console.warn(`mnote rust-web editor bootstrap JSON 解析失败: ${id}`, error);
      return null;
    }
  };

  const panesBootstrap = parseJsonScript(PANES_BOOTSTRAP_ID);
  if (!panesBootstrap || !Array.isArray(panesBootstrap.panes)) return;

  const paneRouteConfig = {
    primary: {
      sourceKindParam: 'sourceKind',
      rootUriParam: 'rootUri',
      secondary: false,
    },
    secondary: {
      documentIdParam: 'secondaryDocumentId',
      sourceKindParam: 'secondarySourceKind',
      rootUriParam: 'secondaryRootUri',
      secondary: true,
    },
  };
  const secondaryQueryParamNames = [
    paneRouteConfig.secondary.documentIdParam,
    paneRouteConfig.secondary.sourceKindParam,
    paneRouteConfig.secondary.rootUriParam,
  ];
  const currentUrl = () => new URL(window.location.href);
  const replaceUrlState = (url) => {
    window.history.replaceState({}, '', url.pathname + url.search + url.hash);
  };
  const pushUrlState = (url) => {
    window.history.pushState({}, '', url.pathname + url.search + url.hash);
  };
  const clearSecondaryParams = () => {
    const url = currentUrl();
    secondaryQueryParamNames.forEach((name) => url.searchParams.delete(name));
    replaceUrlState(url);
  };
  if (panesBootstrap.secondaryInvalid === true) clearSecondaryParams();

  const secondaryUrlForDocument = (documentId, detail = {}) => {
    const url = currentUrl();
    url.searchParams.set(paneRouteConfig.secondary.documentIdParam, documentId);
    const detailSourceKind = typeof detail.sourceKind === 'string' ? detail.sourceKind.trim() : '';
    const detailRootUri = typeof detail.rootUri === 'string' ? detail.rootUri.trim() : '';
    const primarySourceKind = (url.searchParams.get(paneRouteConfig.primary.sourceKindParam) || '').trim();
    const primaryRootUri = (url.searchParams.get(paneRouteConfig.primary.rootUriParam) || '').trim();
    const secondarySourceKind = detailSourceKind || primarySourceKind;
    const secondaryRootUri = detailRootUri || primaryRootUri;
    if (secondarySourceKind) url.searchParams.set(paneRouteConfig.secondary.sourceKindParam, secondarySourceKind);
    else url.searchParams.delete(paneRouteConfig.secondary.sourceKindParam);
    if (secondaryRootUri) url.searchParams.set(paneRouteConfig.secondary.rootUriParam, secondaryRootUri);
    else url.searchParams.delete(paneRouteConfig.secondary.rootUriParam);
    return { url, sourceKind: secondarySourceKind, rootUri: secondaryRootUri };
  };

  const openDocumentInSecondaryPane = (documentId, detail = {}) => {
    const id = typeof documentId === 'string' ? documentId.trim() : '';
    if (!id) return false;
    const target = secondaryUrlForDocument(id, detail);
    if (typeof window.__mnoteDocumentPaneRuntime?.openSecondaryDocument === 'function') {
      void window.__mnoteDocumentPaneRuntime.openSecondaryDocument({
        documentId: id,
        workspaceId: typeof detail.workspaceId === 'string' ? detail.workspaceId.trim() : '',
        sourceKind: target.sourceKind || null,
        rootUri: target.rootUri || null,
        url: target.url,
      });
      return true;
    }
    window.location.assign(target.url.pathname + target.url.search + target.url.hash);
    return true;
  };

  window.addEventListener('tree.page.open-right', (event) => {
    const detail = event?.detail && typeof event.detail === 'object' ? event.detail : {};
    const documentId = typeof detail.documentId === 'string' ? detail.documentId.trim() : '';
    openDocumentInSecondaryPane(documentId, detail);
  });

  window.addEventListener('tree.page.open', (event) => {
    const detail = event?.detail && typeof event.detail === 'object' ? event.detail : {};
    const openTarget = typeof detail.openTarget === 'string' ? detail.openTarget.trim().toLowerCase() : '';
    if (openTarget !== 'side') return;
    const documentId = typeof detail.documentId === 'string' ? detail.documentId.trim() : '';
    openDocumentInSecondaryPane(documentId, detail);
  });

  document.querySelectorAll('[data-mnote-pane-close="secondary"]').forEach((button) => {
    button.addEventListener('click', (event) => {
      event.preventDefault();
      const url = currentUrl();
      secondaryQueryParamNames.forEach((name) => url.searchParams.delete(name));
      if (typeof window.__mnoteDocumentPaneRuntime?.closeSecondaryDocument === 'function') {
        window.__mnoteDocumentPaneRuntime.closeSecondaryDocument({ url });
        return;
      }
      window.location.assign(url.pathname + url.search + url.hash);
    });
  });

  const workspace = document.querySelector('[data-testid="mnote-document-workspace"]');
  const resizer = document.querySelector('[data-document-pane-resizer="true"]');
  const SECONDARY_WIDTH_KEY = 'mnote.document.secondary.width';
  const applyStoredSecondaryWidth = () => {
    if (!(workspace instanceof HTMLElement)) return;
    if (workspace.getAttribute('data-has-secondary-pane') !== 'true') {
      workspace.style.removeProperty('grid-template-columns');
      return;
    }
    try {
      const raw = window.localStorage ? window.localStorage.getItem(SECONDARY_WIDTH_KEY) : '';
      const width = Number(raw || 0);
      if (Number.isFinite(width) && width >= 320) {
        workspace.style.gridTemplateColumns = `minmax(0, 1fr) 6px minmax(320px, ${Math.round(width)}px)`;
      }
    } catch (_) {}
  };
  applyStoredSecondaryWidth();
  if (workspace instanceof HTMLElement && resizer instanceof HTMLElement) {
    resizer.addEventListener('pointerdown', (event) => {
      if (workspace.getAttribute('data-has-secondary-pane') !== 'true') return;
      event.preventDefault();
      const move = (nextEvent) => {
        const rect = workspace.getBoundingClientRect();
        const width = Math.min(Math.max(320, rect.right - nextEvent.clientX), Math.max(420, rect.width - 360));
        workspace.style.gridTemplateColumns = `minmax(0, 1fr) 6px minmax(320px, ${Math.round(width)}px)`;
        try {
          if (window.localStorage) window.localStorage.setItem(SECONDARY_WIDTH_KEY, String(Math.round(width)));
        } catch (_) {}
      };
      const up = () => {
        window.removeEventListener('pointermove', move);
        window.removeEventListener('pointerup', up);
      };
      window.addEventListener('pointermove', move);
      window.addEventListener('pointerup', up);
    });
  }

  const buildPaneRuntime = (paneDescriptor) => {
    const paneRole = typeof paneDescriptor?.role === 'string' ? paneDescriptor.role : 'primary';
    const paneRoot = document.querySelector(`${ROOT_SELECTOR}[data-pane-role="${paneRole}"]`);
    const observability = document.querySelector(`[data-editor-host-observability][data-pane-role="${paneRole}"]`);
    if (!(paneRoot instanceof HTMLElement)) return null;
    if (!paneDescriptor?.aggregate || !paneDescriptor?.bootstrap) return null;
    return {
      paneRole,
      root: paneRoot,
      observability,
      aggregate: paneDescriptor.aggregate,
      bootstrap: paneDescriptor.bootstrap,
    };
  };

  const paneRuntimes = panesBootstrap.panes.map(buildPaneRuntime).filter(Boolean);

  const loadRuntime = async () => {
    if (window.__mnoteLeptosTiptapRuntimePromise) {
      return window.__mnoteLeptosTiptapRuntimePromise;
    }
    window.__mnoteLeptosTiptapRuntimePromise = (async () => {
      const manifestResponse = await fetch('/api/leptos-tiptap-runtime/manifest.json', { cache: 'no-store' });
      if (!manifestResponse.ok) throw new Error(`manifest_failed_${manifestResponse.status}`);
      const manifest = await manifestResponse.json();
      if (!manifest.entryAssetPath) throw new Error('island manifest 缺少 entryAssetPath');
      const entryUrl = `/api/leptos-tiptap-runtime/${manifest.entryAssetPath}`;
      const wasmUrl = manifest.wasmAssetPath ? `/api/leptos-tiptap-runtime/${manifest.wasmAssetPath}` : undefined;
      const runtime = await import(entryUrl);
      if (typeof runtime.default !== 'function' || typeof runtime.mount !== 'function' || typeof runtime.unmount !== 'function') {
        throw new Error('island runtime 导出不完整');
      }
      await runtime.default({ module_or_path: wasmUrl });
      if (typeof runtime.mount_mindmap_shell === 'function' && typeof runtime.unmount_mindmap_shell === 'function') {
        window.__MNOTE_MINDMAP_RUST_SHELL__ = {
          mount: runtime.mount_mindmap_shell,
          unmount: runtime.unmount_mindmap_shell,
        };
      }
      return runtime;
    })();
    return window.__mnoteLeptosTiptapRuntimePromise;
  };

  const flattenText = (value) => {
    if (typeof value === 'string') return value;
    if (Array.isArray(value)) return value.map(flattenText).join('');
    if (value && typeof value === 'object') {
      return `${flattenText(value.text)}${flattenText(value.content)}`;
    }
    return '';
  };

  const firstNonEmptyText = (...values) => {
    for (const value of values) {
      const text = flattenText(value).trim();
      if (text) return text;
    }
    return '';
  };

  // 过渡适配（TODO step-4）：legacy→Tiptap inline marks 转换函数组。
  // AST/block 迁移 complete 后，前端应直接消费 block document 中的
  // tiptap 格式 marks（已由 Rust 侧 local_markdown_parser 输出），
  // 不再需要此 JS 侧样式→marks 适配层。
  const legacyStylesToTiptapMarks = (styles) => {
    if (!styles || typeof styles !== 'object') return [];
    const marks = [];
    if (styles.bold) marks.push({ type: 'bold' });
    if (styles.italic) marks.push({ type: 'italic' });
    if (styles.underline) marks.push({ type: 'underline' });
    if (styles.strike || styles.strikethrough) marks.push({ type: 'strike' });
    if (styles.code || styles.inlineCode) marks.push({ type: 'code' });
    const href = typeof styles.link === 'string' && styles.link.trim()
      ? styles.link.trim()
      : typeof styles.href === 'string' && styles.href.trim()
        ? styles.href.trim()
        : '';
    if (href) marks.push({ type: 'link', attrs: { href } });
    return marks;
  };

  const legacyMarkArrayToTiptapMarks = (inlineMarks) => {
    if (!Array.isArray(inlineMarks)) return [];
    return inlineMarks.flatMap((mark) => {
      if (!mark || typeof mark !== 'object') return [];
      if (mark.type === 'bold' || mark.type === 'italic' || mark.type === 'underline' || mark.type === 'strike' || mark.type === 'code') {
        return [{ type: mark.type }];
      }
      if (mark.type === 'link') {
        const href = typeof mark?.attrs?.href === 'string' ? mark.attrs.href.trim() : '';
        return href ? [{ type: 'link', attrs: { href } }] : [];
      }
      return [];
    });
  };

  const mergeTiptapMarks = (...groups) => {
    const seen = new Set();
    return groups.flat().filter((mark) => {
      const key = `${mark.type}:${JSON.stringify(mark.attrs || {})}`;
      if (seen.has(key)) return false;
      seen.add(key);
      return true;
    });
  };

  const legacyInlineContentToTiptap = (value) => {
    if (typeof value === 'string') return value ? [{ type: 'text', text: value }] : [];
    if (Array.isArray(value)) return value.flatMap(legacyInlineContentToTiptap);
    if (value && typeof value === 'object') {
      const text = typeof value.text === 'string' ? value.text : '';
      if (text) {
        const marks = mergeTiptapMarks(
          legacyStylesToTiptapMarks(value.styles),
          legacyMarkArrayToTiptapMarks(value.marks)
        );
        return [{ type: 'text', text, ...(marks.length ? { marks } : {}) }];
      }
      return legacyInlineContentToTiptap(value.content || value.contentNodes);
    }
    return [];
  };

  const legacyBlockToTiptap = (block, index = 0) => {
    const type = typeof block?.type === 'string' ? block.type : 'paragraph';
    const blockId = typeof block?.id === 'string' && block.id.trim()
      ? block.id.trim()
      : typeof block?.blockId === 'string' && block.blockId.trim()
        ? block.blockId.trim()
        : `block-${index + 1}`;
    const content = legacyInlineContentToTiptap(block?.content ?? block?.contentNodes);
    const textAlign = typeof block?.props?.textAlign === 'string'
      ? block.props.textAlign
      : typeof block?.props?.text_align === 'string'
        ? block.props.text_align
        : undefined;
    const withTextAlign = (attrs = {}) => textAlign ? { ...attrs, textAlign } : attrs;
    const nestedChildren = Array.isArray(block?.children)
      ? block.children.map((child, childIndex) => legacyBlockToTiptap(child, childIndex)).filter(Boolean)
      : [];
    const withListChildren = (itemType, listType, attrs = {}) => ({
      type: listType,
      attrs: { blockId },
      content: [{
        type: itemType,
        attrs: { blockId, ...attrs },
        content: [
          { type: 'paragraph', attrs: { blockId }, content },
          ...nestedChildren,
        ],
      }],
    });
    if (type === 'heading') {
      const level = Number(block?.props?.level || block?.level || 1) || 1;
      const collapsed = typeof block?.props?.collapsed === 'boolean' ? { collapsed: block.props.collapsed } : {};
      return { type: 'heading', attrs: withTextAlign({ blockId, level: Math.max(1, Math.min(6, level)), ...collapsed }), content };
    }
    if (type === 'bulletListItem' || type === 'bullet_list_item') return withListChildren('listItem', 'bulletList');
    if (type === 'numberedListItem' || type === 'numbered_list_item') return withListChildren('listItem', 'orderedList');
    if (type === 'checkListItem' || type === 'advancedTodo' || type === 'todo') {
      return withListChildren('taskItem', 'taskList', { checked: Boolean(block?.props?.checked) });
    }
    if (type === 'quote' || type === 'blockquote') {
      return { type: 'blockquote', attrs: withTextAlign({ blockId }), content: [{ type: 'paragraph', attrs: withTextAlign({ blockId }), content }] };
    }
    if (type === 'codeBlock' || type === 'code_block') {
      return { type: 'codeBlock', attrs: withTextAlign({ blockId, language: block?.props?.language || null }), content };
    }
    if (type === 'divider' || type === 'horizontalRule' || type === 'horizontal_rule') return { type: 'horizontalRule', attrs: { blockId } };
    if (type === 'mindmap') {
      const data = block?.props?.data && typeof block.props.data === 'object' ? block.props.data : null;
      const mindmapId = firstNonEmptyText(
        block?.props?.mindmapId,
        block?.props?.mindmap_id,
        block?.props?.sourcePath,
        block?.props?.source_path,
        block?.mindmapId,
        block?.mindmap_id,
        data?.mindmapId,
        data?.mindmap_id,
        data?.id
      );
      const rootNodeId = firstNonEmptyText(block?.props?.rootNodeId, block?.props?.root_node_id, data?.rootNodeId, data?.root_node_id) || 'root';
      return {
        type: 'paragraph',
        attrs: withTextAlign({
          blockId,
          mnoteBlockType: 'mindmap',
          mindmapId,
          rootNodeId,
        }),
      };
    }
    if (type === 'media') {
      const sourcePath = firstNonEmptyText(block?.props?.sourcePath, block?.props?.url, block?.props?.src);
      const name = firstNonEmptyText(block?.props?.name, block?.props?.fileName, block?.props?.title, sourcePath);
      const mediaContent = name
        ? [{
            type: 'text',
            text: name,
            ...(sourcePath ? { marks: [{ type: 'link', attrs: { href: sourcePath } }] } : {}),
          }]
        : content;
      return { type: 'paragraph', attrs: withTextAlign({ blockId }), content: mediaContent };
    }
    if (type === 'table') {
      const tableSnapshot = block?.props?.tiptapTable;
      if (tableSnapshot && typeof tableSnapshot === 'object' && tableSnapshot.type === 'table') return tableSnapshot;
      return {
        type: 'table',
        content: [{
          type: 'tableRow',
          content: [{
            type: 'tableCell',
            attrs: { colspan: 1, rowspan: 1, colwidth: null },
            content: [{ type: 'paragraph', content }],
          }],
        }],
      };
    }
    if (type === 'toc' || type === 'tocNode' || type === 'toc_node') {
      const tocSnapshot = block?.props?.tiptapTocNode || block?.props?.tiptapToc;
      if (tocSnapshot && typeof tocSnapshot === 'object' && tocSnapshot.type === 'tocNode') return tocSnapshot;
      return {
        type: 'tocNode',
        attrs: {
          topOffset: Number(block?.props?.topOffset || block?.props?.top_offset || 0) || 0,
          maxShowCount: Number(block?.props?.maxShowCount || block?.props?.max_show_count || 20) || 20,
          showTitle: block?.props?.showTitle !== false,
        },
      };
    }
    if (type === 'image') {
      const imageSnapshot = block?.props?.tiptapImage;
      if (imageSnapshot && typeof imageSnapshot === 'object' && imageSnapshot.type === 'image') return imageSnapshot;
      const attrs = {
        src: String(block?.props?.src || block?.src || ''),
        alt: block?.props?.alt || block?.alt || null,
        title: block?.props?.title || block?.title || null,
      };
      return attrs.src ? { type: 'image', attrs: { blockId, ...attrs } } : null;
    }
    return { type: 'paragraph', attrs: withTextAlign({ blockId }), content };
  };

  const textToTiptapDocument = (text) => ({
    type: 'doc',
    content: [{
      type: 'paragraph',
      content: text ? [{ type: 'text', text }] : [],
    }],
  });

  const isTiptapDocument = (content) => (
    content &&
    typeof content === 'object' &&
    !Array.isArray(content) &&
    content.type === 'doc'
  );

  const mindmapDomDescriptors = (root) => {
    if (!(root instanceof HTMLElement)) return [];
    return Array.from(root.querySelectorAll('[data-testid="mnote-mindmap-editor-root"]'))
      .flatMap((node) => {
        if (!(node instanceof HTMLElement)) return [];
        const mindmapId = typeof node.dataset.mnoteMindmapId === 'string' ? node.dataset.mnoteMindmapId.trim() : '';
        if (!mindmapId) return [];
        const rootNodeId = typeof node.dataset.mnoteRootNodeId === 'string' && node.dataset.mnoteRootNodeId.trim()
          ? node.dataset.mnoteRootNodeId.trim()
          : 'root';
        return [{ mindmapId, rootNodeId }];
      });
  };

  const hydrateMindmapAttrsFromDom = (tiptapDocument, root) => {
    if (!isTiptapDocument(tiptapDocument) || !Array.isArray(tiptapDocument.content)) return tiptapDocument;
    const descriptors = mindmapDomDescriptors(root);
    if (!descriptors.length) return tiptapDocument;
    let index = 0;
    for (const node of tiptapDocument.content) {
      if (node?.type !== 'paragraph' || node?.attrs?.mnoteBlockType !== 'mindmap') continue;
      const descriptor = descriptors[index];
      index += 1;
      if (!descriptor) continue;
      node.attrs = node.attrs && typeof node.attrs === 'object' ? node.attrs : {};
      if (typeof node.attrs.mindmapId !== 'string' || !node.attrs.mindmapId.trim()) {
        node.attrs.mindmapId = descriptor.mindmapId;
      }
      if (typeof node.attrs.rootNodeId !== 'string' || !node.attrs.rootNodeId.trim()) {
        node.attrs.rootNodeId = descriptor.rootNodeId;
      }
    }
    return tiptapDocument;
  };

  const toTiptapDocument = (content, fallbackText = '') => {
    if (isTiptapDocument(content)) return content;
    const blocks = Array.isArray(content) ? content : Array.isArray(content?.blocks) ? content.blocks : [];
    const nodes = blocks.map(legacyBlockToTiptap).filter(Boolean);
    if (nodes.length) return { type: 'doc', content: nodes };
    return textToTiptapDocument(fallbackText);
  };

  const decodeLocalIdSegment = (segment) => {
    const encoded = String(segment || '').replace(/~([0-9a-fA-F]{2})/g, '%$1');
    try {
      return decodeURIComponent(encoded);
    } catch (_) {
      return String(segment || '').replace(/~2F/g, '/').replace(/~20/g, ' ');
    }
  };

  const localMarkdownRelativePathFromDocumentId = (documentId) => {
    const raw = String(documentId || '').trim();
    const segment = raw.startsWith('local-md:') ? raw.slice('local-md:'.length) : raw;
    return decodeLocalIdSegment(segment).replace(/^\/+/, '');
  };

  const localMarkdownDocumentIdFromRelativePath = (relativePath) => {
    const normalized = String(relativePath || '').trim().replace(/^\/+/, '');
    if (!normalized) return '';
    return 'local-md:' + normalized
      .split('/')
      .map((part) => part.replace(/ /g, '~20'))
      .join('~2F');
  };

  const localMarkdownDirectoryFromDocumentId = (documentId) => {
    const relativePath = localMarkdownRelativePathFromDocumentId(documentId);
    const slash = relativePath.lastIndexOf('/');
    return slash >= 0 ? relativePath.slice(0, slash) : '';
  };

  const isExternalOrSpecialUrl = (value) => {
    const text = String(value || '').trim();
    return !text
      || text.startsWith('#')
      || text.startsWith('data:')
      || text.startsWith('blob:')
      || text.startsWith('mailto:')
      || text.startsWith('http://')
      || text.startsWith('https://')
      || text.startsWith('/api/');
  };

  const normalizeLocalAssetRelativePath = (value, context) => {
    const text = String(value || '').trim();
    if (!text || isExternalOrSpecialUrl(text)) return text;
    if (text.startsWith('/')) return text.replace(/^\/+/, '');
    const baseDir = localMarkdownDirectoryFromDocumentId(context?.documentId);
    return (baseDir ? `${baseDir}/${text}` : text)
      .split('/')
      .filter((part) => part && part !== '.')
      .join('/');
  };

  const localFileOpenUrlForTiptap = (value, context) => {
    if (!context || context.sourceKind !== 'local_folder' || !context.rootUri) return value;
    const relativePath = normalizeLocalAssetRelativePath(value, context);
    if (!relativePath || isExternalOrSpecialUrl(relativePath)) return value;
    const url = new URL('/api/local-folder/files/open', window.location.origin);
    url.searchParams.set('rootUri', context.rootUri);
    url.searchParams.set('path', relativePath);
    return url.toString();
  };

  const localizeTiptapAssetUrls = (node, context) => {
    if (!node || typeof node !== 'object') return node;
    if (node.type === 'image' && node.attrs && typeof node.attrs.src === 'string') {
      node.attrs = { ...node.attrs, src: localFileOpenUrlForTiptap(node.attrs.src, context) };
    }
    if (Array.isArray(node.marks)) {
      node.marks = node.marks.map((mark) => {
        if (!mark || mark.type !== 'link' || !mark.attrs || typeof mark.attrs.href !== 'string') return mark;
        return { ...mark, attrs: { ...mark.attrs, href: localFileOpenUrlForTiptap(mark.attrs.href, context) } };
      });
    }
    if (Array.isArray(node.content)) {
      node.content = node.content.map((child) => localizeTiptapAssetUrls(child, context));
    }
    return node;
  };

  const pageBodyTiptapDocument = (body, fallbackText = '', context = null) => {
    if (String(body?.projectionSource || body?.projection_source || '') === 'local_markdown.content' && body?.content) {
      return localizeTiptapAssetUrls(toTiptapDocument(body.content, fallbackText), context);
    }
    const blockDocument = body?.blockDocument || body?.block_document || body?.editorDocument || body?.editor_document || null;
    if (blockDocument) return localizeTiptapAssetUrls(toTiptapDocument(blockDocument, fallbackText), context);
    return localizeTiptapAssetUrls(toTiptapDocument(body?.content, fallbackText), context);
  };

  const inlineTextNodes = (node) => {
    if (!node || typeof node !== 'object') return [];
    if (Array.isArray(node.content)) {
      return node.content.flatMap((child) => {
        if (child?.type === 'text') {
          const text = typeof child.text === 'string' ? child.text : '';
          if (!text) return [];
          const styles = {};
          const marks = [];
          for (const mark of Array.isArray(child.marks) ? child.marks : []) {
            if (mark?.type === 'bold') {
              styles.bold = true;
              marks.push('bold');
            }
            if (mark?.type === 'italic') {
              styles.italic = true;
              marks.push('italic');
            }
            if (mark?.type === 'underline') {
              styles.underline = true;
              marks.push('underline');
            }
            if (mark?.type === 'strike') {
              styles.strike = true;
              marks.push('strike');
            }
            if (mark?.type === 'code') {
              styles.code = true;
              marks.push('code');
            }
            if (mark?.type === 'link') {
              const href = typeof mark?.attrs?.href === 'string' ? mark.attrs.href.trim() : '';
              if (href) styles.link = href;
            }
          }
          return [{
            payload: { type: 'text', text, ...(marks.length ? { marks } : {}) },
            attrs: Object.keys(styles).length ? { styles } : {},
            type: 'text',
            text,
            ...(Object.keys(styles).length ? { styles } : {}),
          }];
        }
        if (child?.type === 'hardBreak') return [{ payload: { type: 'hard_break' }, attrs: {}, type: 'text', text: '\n' }];
        return inlineTextNodes(child);
      });
    }
    return [];
  };

  const firstChild = (node) => Array.isArray(node?.content) ? node.content[0] : null;
  const blockIdOf = (node, index) => {
    const raw = typeof node?.attrs?.blockId === 'string' ? node.attrs.blockId.trim() : '';
    return raw || `block-${index + 1}`;
  };
  const mindmapPropsFromAttrs = (attrs, fallbackMindmapId) => {
    const data = attrs?.data && typeof attrs.data === 'object' ? attrs.data : {};
    const mindmapId = firstNonEmptyText(
      attrs?.mindmapId,
      attrs?.mindmap_id,
      attrs?.sourcePath,
      attrs?.source_path,
      data?.mindmapId,
      data?.mindmap_id,
      data?.id,
      fallbackMindmapId
    );
    const rootNodeId = firstNonEmptyText(attrs?.rootNodeId, attrs?.root_node_id, data?.rootNodeId, data?.root_node_id) || 'root';
    const projectionVersion = Number(attrs?.projectionVersion ?? attrs?.projection_version);
    return {
      mindmapId,
      rootNodeId,
      ...(Number.isFinite(projectionVersion) ? { projectionVersion } : {}),
    };
  };

  const tiptapNodeToEditorBlock = (node, index) => {
    const blockId = blockIdOf(node, index);
    if (node?.type === 'paragraph' && node?.attrs?.mnoteBlockType === 'mindmap') {
      const mindmapId = firstNonEmptyText(
        node?.attrs?.mindmapId,
        node?.attrs?.mindmap_id,
        node?.attrs?.sourcePath,
        node?.attrs?.source_path,
        blockId
      );
      return {
        blockId,
        blockType: 'mindmap',
        props: {
          ...mindmapPropsFromAttrs(node?.attrs, blockId),
          sourcePath: mindmapId,
        },
        contentNodes: [],
        childBlockIds: [],
      };
    }
    if (node?.type === 'paragraph') return { blockId, blockType: 'paragraph', props: {}, contentNodes: inlineTextNodes(node), childBlockIds: [] };
    if (node?.type === 'heading') {
      const level = Math.max(1, Math.min(6, Number(node?.attrs?.level || 1) || 1));
      return { blockId, blockType: 'heading', props: { headingLevel: level }, contentNodes: inlineTextNodes(node), childBlockIds: [] };
    }
    if (node?.type === 'bulletList') return { blockId, blockType: 'bullet_list_item', props: {}, contentNodes: inlineTextNodes(firstChild(firstChild(node))), childBlockIds: [] };
    if (node?.type === 'orderedList') return { blockId, blockType: 'numbered_list_item', props: {}, contentNodes: inlineTextNodes(firstChild(firstChild(node))), childBlockIds: [] };
    if (node?.type === 'taskList') {
      const taskItem = firstChild(node);
      return { blockId, blockType: 'todo', props: { checked: Boolean(taskItem?.attrs?.checked) }, contentNodes: inlineTextNodes(firstChild(taskItem)), childBlockIds: [] };
    }
    if (node?.type === 'blockquote') return { blockId, blockType: 'quote', props: {}, contentNodes: inlineTextNodes(firstChild(node)), childBlockIds: [] };
    if (node?.type === 'codeBlock') return { blockId, blockType: 'code_block', props: { language: typeof node?.attrs?.language === 'string' ? node.attrs.language : null }, contentNodes: inlineTextNodes(node), childBlockIds: [] };
    if (node?.type === 'horizontalRule') return { blockId, blockType: 'divider', props: {}, contentNodes: [], childBlockIds: [] };
    if (node?.type === 'image') return { blockId, blockType: 'image', props: { src: node?.attrs?.src || '', alt: node?.attrs?.alt || null, title: node?.attrs?.title || null, tiptapImage: node }, contentNodes: [], childBlockIds: [] };
    if (node?.type === 'tocNode') return { blockId, blockType: 'toc', props: { tiptapTocNode: node }, contentNodes: [], childBlockIds: [] };
    if (node?.type === 'table') return { blockId, blockType: 'table', props: { tiptapTable: node }, contentNodes: inlineTextNodes(node), childBlockIds: [] };
    return null;
  };

  const editorDocumentFromTiptapDocument = (bootstrap, tiptapDocument) => {
    const content = Array.isArray(tiptapDocument?.content) ? tiptapDocument.content : [];
    const blocks = content.map(tiptapNodeToEditorBlock).filter(Boolean);
    return {
      documentId: bootstrap.documentId,
      rootBlockIds: blocks.map((block) => block.blockId),
      blocks,
    };
  };

  const legacyBlocksFromEditorDocument = (editorDocument) => (
    Array.isArray(editorDocument?.blocks) ? editorDocument.blocks : []
  ).map((block) => ({
    id: block.blockId,
    type: block.blockType,
    props: block.blockType === 'heading'
      ? { level: block.props?.headingLevel || 1 }
      : block.blockType === 'todo'
        ? { checked: Boolean(block.props?.checked) }
        : block.blockType === 'code_block'
          ? { language: block.props?.language || null }
        : block.blockType === 'mindmap'
            ? mindmapPropsFromAttrs(block.props || {}, block.blockId)
          : block.blockType === 'image'
            ? { ...(block.props || {}) }
            : block.blockType === 'toc'
              ? { ...(block.props || {}) }
              : block.blockType === 'table'
                ? { ...(block.props || {}) }
                : undefined,
    content: block.blockType === 'mindmap'
      ? ''
      : Array.isArray(block.contentNodes)
      ? block.contentNodes.map((node) => {
        if (!node || typeof node !== 'object') return null;
        const payload = node.payload && typeof node.payload === 'object' ? node.payload : {};
        const text = typeof payload.text === 'string'
          ? payload.text
          : payload.type === 'hard_break'
            ? '\n'
            : typeof node.text === 'string'
              ? node.text
              : '';
        if (!text) return null;
        const attrs = node.attrs && typeof node.attrs === 'object' ? node.attrs : {};
        const styles = attrs.styles && typeof attrs.styles === 'object'
          ? attrs.styles
          : node.styles && typeof node.styles === 'object'
            ? node.styles
            : null;
        const marks = Array.isArray(payload.marks) ? payload.marks : [];
        return {
          type: 'text',
          text,
          ...(styles ? { styles } : {}),
          ...(marks.length ? { marks } : {}),
        };
      }).filter(Boolean)
      : '',
  }));

  const conflictDetectionKeyFromBody = (body) => typeof body?.conflictDetectionKey === 'string'
    ? body.conflictDetectionKey
    : typeof body?.conflict_detection_key === 'string'
      ? body.conflict_detection_key
      : typeof body?.fileVersion === 'string'
        ? body.fileVersion
        : typeof body?.file_version === 'string'
          ? body.file_version
          : null;

  const conflictDetectionKeyBelongsToSession = (session, key) => {
    if (!session || session.sourceKind !== 'local_folder') return true;
    const value = String(key || '').trim();
    if (!value) return false;
    return value.startsWith(`local-md:${session.documentId}:`);
  };

  const revisionFromConflictKey = (value) => {
    const match = String(value || '').match(/:(\d+)$/);
    return match ? Number(match[1]) : null;
  };

  const normalizeBridgeValue = (value) => {
    if (value instanceof Map) {
      const out = {};
      for (const [key, item] of value.entries()) out[key] = normalizeBridgeValue(item);
      return out;
    }
    if (Array.isArray(value)) return value.map(normalizeBridgeValue);
    if (value && typeof value === 'object') {
      return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, normalizeBridgeValue(item)]));
    }
    return value;
  };

  const normalizeEnvelopePayload = (event) => {
    const detail = normalizeBridgeValue(event?.detail);
    if (!detail || typeof detail !== 'object') return null;
    const payload = detail.payload && typeof detail.payload === 'object' ? detail.payload : detail;
    return payload && typeof payload === 'object' ? payload : null;
  };

  const setStatus = (runtime, status, message) => {
    runtime.root.setAttribute('data-runtime-editor-status', status);
    if (message) {
      runtime.root.setAttribute('data-runtime-editor-error', message);
    } else {
      runtime.root.removeAttribute('data-runtime-editor-error');
    }
    if (runtime.observability instanceof HTMLElement) {
      runtime.observability.setAttribute('data-editor-host-status', status);
      runtime.observability.setAttribute('data-editor-host-active', 'leptos_tiptap_island');
    }
  };

  const pageAggregateUrl = (bootstrap) => {
    const url = new URL(`/api/page-aggregate/${encodeURIComponent(bootstrap.documentId)}`, window.location.origin);
    url.searchParams.set('sourceKind', bootstrap.sourceKind || 'local_folder');
    if (bootstrap.workspaceId) url.searchParams.set('workspaceId', bootstrap.workspaceId);
    if (bootstrap.rootUri) url.searchParams.set('rootUri', bootstrap.rootUri);
    return url;
  };
  const pageAggregateUrlFromDescriptor = (descriptor) => {
    const url = new URL(`/api/page-aggregate/${encodeURIComponent(descriptor.documentId)}`, window.location.origin);
    url.searchParams.set('sourceKind', descriptor.sourceKind || 'convex_workspace');
    if (descriptor.workspaceId) url.searchParams.set('workspaceId', descriptor.workspaceId);
    if (descriptor.rootUri) url.searchParams.set('rootUri', descriptor.rootUri);
    return url;
  };
  const buildBootstrapFromAggregate = (aggregate, descriptor, paneRole) => ({
    schema: 'mnote.editor_bootstrap.v1',
    documentId: aggregate?.identity?.documentId || aggregate?.identity?.document_id || descriptor.documentId,
    workspaceId: aggregate?.identity?.workspaceId || aggregate?.identity?.workspace_id || descriptor.workspaceId || '',
    paneRole,
    sourceKind: descriptor.sourceKind || 'convex_workspace',
    rootUri: descriptor.rootUri || '',
    pageAggregateScriptId: paneRole === 'secondary' ? '__MNOTE_SECONDARY_PAGE_AGGREGATE__' : '__MNOTE_PAGE_AGGREGATE__',
    saveEndpoint: (descriptor.sourceKind || 'convex_workspace') === 'local_folder'
      ? '/api/page-body/write'
      : '/api/documents/save',
    titleEndpoint: '/api/documents/title',
    editorHostKind: 'leptos_tiptap_island',
  });
  const syncPageAggregateScript = (session, aggregate) => {
    if (!session || !aggregate) return;
    const scriptId = session.pageAggregateScriptId || '__MNOTE_PAGE_AGGREGATE__';
    const node = document.getElementById(scriptId);
    if (!node) return;
    try {
      node.textContent = JSON.stringify(aggregate);
      node.setAttribute('data-mnote-page-aggregate-synced-at', String(Date.now()));
    } catch (error) {
      console.warn('mnote Page Aggregate script 同步失败', error);
    }
  };

  const documentSessionRegistry = new Map();
  const localFolderEventRegistry = new Map();
  const paneViewRegistry = new Map();
  const mindmapPaneViewRegistry = new Map();
  const resourceTabRegistry = new Map();
  const resourceTabMru = { primary: [], secondary: [] };
  const resourceTabMruMax = 20;
  const resourceTabCloseGuardAttribute = 'data-resource-tab-close-guarded';
  let nextViewId = 1;
  const externalConflictMessage = '本地 Markdown 文件已在外部更新，请刷新或保存前先处理冲突';
  const treeExternalConflictMessage = '当前页面已在其它窗口更新，请刷新或保存前先处理冲突';
  const SESSION_RELEASE_DELAY_MS = 1200;
  const LOCAL_FOLDER_SELF_CHANGE_SUPPRESSIONS_KEY = 'mnote.localFolder.selfChangeSuppressions.v1';

  const ensureLocalFolderSelfChangeSuppressions = () => {
    const now = Date.now();
    const map = window.__mnoteLocalFolderSelfChangeSuppressions instanceof Map
      ? window.__mnoteLocalFolderSelfChangeSuppressions
      : new Map();
    window.__mnoteLocalFolderSelfChangeSuppressions = map;
    try {
      const raw = window.sessionStorage ? window.sessionStorage.getItem(LOCAL_FOLDER_SELF_CHANGE_SUPPRESSIONS_KEY) : '';
      const parsed = raw ? JSON.parse(raw) : null;
      if (parsed && typeof parsed === 'object') {
        Object.entries(parsed).forEach(([documentId, expiresAt]) => {
          const doc = String(documentId || '').trim();
          const expiry = Number(expiresAt || 0);
          if (doc && Number.isFinite(expiry) && expiry > now) {
            map.set(doc, expiry);
          } else if (doc) {
            map.delete(doc);
            delete parsed[documentId];
          }
        });
        if (window.sessionStorage) window.sessionStorage.setItem(LOCAL_FOLDER_SELF_CHANGE_SUPPRESSIONS_KEY, JSON.stringify(parsed));
      }
    } catch (_) {}
    return map;
  };

  const unmountMindmapPane = (paneRole) => {
    const view = mindmapPaneViewRegistry.get(paneRole);
    if (!view) return;
    mindmapPaneViewRegistry.delete(paneRole);
    if (view.mountId != null && view.runtime && typeof view.runtime.unmount === 'function') {
      try {
        view.runtime.unmount(view.mountId);
      } catch (error) {
        console.warn('mnote mindmap pane unmount failed', error);
      }
    }
    if (view.root instanceof HTMLElement) {
      view.root.removeAttribute('data-runtime-mount-id');
      view.root.removeAttribute('data-mnote-object-editor');
      view.root.removeAttribute('data-mnote-object-identity');
      view.root.removeAttribute('data-mnote-mindmap-id');
      view.root.replaceChildren();
    }
  };

  const parseLocalFolderEventPayload = (event) => {
    try {
      return JSON.parse(String(event?.data || '{}'));
    } catch (_) {
      return null;
    }
  };

  const shouldSuppressLocalFolderSelfChange = (documentId, eventKind) => {
    const doc = String(documentId || '').trim();
    if (!doc) return false;
    const kind = String(eventKind || '');
    const suppressibleSelfWrite = kind.includes('Create')
      || kind.includes('Metadata')
      || kind.includes('Modify(Data')
      || kind.includes('Modify(Any')
      || kind.includes('Modify(Name');
    if (!suppressibleSelfWrite) return false;
    const suppressions = ensureLocalFolderSelfChangeSuppressions();
    if (!suppressions || typeof suppressions.get !== 'function') return false;
    const expiresAt = Number(suppressions.get(doc) || 0);
    if (!Number.isFinite(expiresAt) || expiresAt <= 0) return false;
    if (Date.now() > expiresAt) {
      if (typeof suppressions.delete === 'function') suppressions.delete(doc);
      return false;
    }
    return true;
  };

  const markLocalFolderSelfChangeSuppression = (session, ttlMs = 5000) => {
    if (!session || session.sourceKind !== 'local_folder') return;
    const doc = String(session.documentId || '').trim();
    if (!doc) return;
    const suppressions = ensureLocalFolderSelfChangeSuppressions();
    if (!suppressions || typeof suppressions.set !== 'function') return;
    const expiresAt = Date.now() + Math.max(1000, Number(ttlMs) || 5000);
    suppressions.set(doc, expiresAt);
    try {
      if (!window.sessionStorage) return;
      const raw = window.sessionStorage.getItem(LOCAL_FOLDER_SELF_CHANGE_SUPPRESSIONS_KEY);
      const parsed = raw ? JSON.parse(raw) : {};
      const next = parsed && typeof parsed === 'object' ? parsed : {};
      next[doc] = expiresAt;
      window.sessionStorage.setItem(LOCAL_FOLDER_SELF_CHANGE_SUPPRESSIONS_KEY, JSON.stringify(next));
    } catch (_) {}
  };

  const normalizeSessionSourceKind = (bootstrap) => {
    const value = typeof bootstrap?.sourceKind === 'string' ? bootstrap.sourceKind.trim() : '';
    return value || 'convex_workspace';
  };

  const buildDocumentSessionKey = (bootstrap) => {
    const sourceKind = normalizeSessionSourceKind(bootstrap);
    const scope = sourceKind === 'local_folder'
      ? String(bootstrap?.rootUri || '').trim()
      : String(bootstrap?.workspaceId || '').trim();
    return `${sourceKind}:${scope}:${String(bootstrap?.documentId || '').trim()}`;
  };

  const sessionViews = (session) => Array.from(session.views.values());

  const localFolderEventChannelKey = (session) => `${String(session?.rootUri || '').trim()}#${String(session?.documentId || '').trim()}`;

  const detachSessionFromLocalFolderChannel = (session) => {
    const channel = session.localFolderChannel;
    if (!channel) return;
    channel.sessions.delete(session.key);
    if (channel.sessions.size === 0) {
      try {
        channel.eventSource.close();
      } catch (_) {
        // noop
      }
      localFolderEventRegistry.delete(channel.key);
    }
    session.localFolderChannel = null;
  };

  const releaseDocumentSession = (session) => {
    if (session.saveTimer) {
      window.clearTimeout(session.saveTimer);
      session.saveTimer = 0;
    }
    if (session.externalRefreshTimer) {
      window.clearTimeout(session.externalRefreshTimer);
      session.externalRefreshTimer = 0;
    }
    if (session.releaseTimer) {
      window.clearTimeout(session.releaseTimer);
      session.releaseTimer = 0;
    }
    detachSessionFromLocalFolderChannel(session);
    if (documentSessionRegistry.get(session.key) === session) {
      documentSessionRegistry.delete(session.key);
    }
  };

  const scheduleDocumentSessionRelease = (session) => {
    if (session.views.size > 0) return;
    if (session.releaseTimer) window.clearTimeout(session.releaseTimer);
    session.releaseTimer = window.setTimeout(() => {
      session.releaseTimer = 0;
      if (session.views.size === 0) {
        releaseDocumentSession(session);
      }
    }, SESSION_RELEASE_DELAY_MS);
  };

  const cancelDocumentSessionRelease = (session) => {
    if (!session.releaseTimer) return;
    window.clearTimeout(session.releaseTimer);
    session.releaseTimer = 0;
  };

  window.__mnoteDebugDocumentSessions = {
    snapshot: () => ({
      sessionCount: documentSessionRegistry.size,
      sessions: Array.from(documentSessionRegistry.entries()).map(([key, session]) => ({
        key,
        documentId: session.documentId,
        sourceKind: session.sourceKind,
        rootUri: session.rootUri,
        workspaceId: session.workspaceId,
        viewCount: session.views.size,
        status: session.status,
        conflictDetectionKey: session.conflictDetectionKey || '',
        lastExternalConflictDetectionKey: session.lastExternalConflictDetectionKey || '',
        lastExternalConflictEnvelope: session.lastExternalConflictEnvelope || null,
        dirtyState: session.dirty ? 'Dirty' : (session.hasExternalConflict ? 'ExternalModified' : 'Clean'),
      })),
      localFolderChannelCount: localFolderEventRegistry.size,
      localFolderRoots: Array.from(localFolderEventRegistry.keys()),
    }),
  };

  const currentEditorText = (view) => {
    const editor = view.runtimeDescriptor.root.querySelector('.editor-surface .ProseMirror');
    return editor?.textContent || '';
  };

  const findScrollableEditorContainer = (view) => {
    let current = view.runtimeDescriptor.root.querySelector('.editor-surface .ProseMirror');
    while (current instanceof HTMLElement) {
      if (current.scrollHeight > current.clientHeight + 8) {
        return current;
      }
      current = current.parentElement;
    }
    return null;
  };

  const preserveViewScrollPosition = (view, operation) => {
    const scrollable = findScrollableEditorContainer(view);
    const scrollTop = scrollable instanceof HTMLElement ? scrollable.scrollTop : null;
    const viewportTop = window.scrollY;
    const viewportLeft = window.scrollX;
    operation();
    const restore = () => {
      const current = findScrollableEditorContainer(view);
      if (current instanceof HTMLElement && scrollTop != null) {
        current.scrollTop = scrollTop;
      }
      if (Number.isFinite(viewportTop) || Number.isFinite(viewportLeft)) {
        window.scrollTo({
          top: Number.isFinite(viewportTop) ? viewportTop : window.scrollY,
          left: Number.isFinite(viewportLeft) ? viewportLeft : window.scrollX,
          behavior: 'auto',
        });
      }
    };
    window.requestAnimationFrame(() => {
      restore();
      window.setTimeout(restore, 0);
      window.setTimeout(restore, 80);
      window.setTimeout(restore, 180);
      window.setTimeout(restore, 320);
    });
  };

  const dispatchRuntimeCommand = (view, payload, source) => {
    view.runtimeDescriptor.root.dispatchEvent(new CustomEvent(COMMAND_EVENT, {
      bubbles: true,
      detail: {
        protocol: BRIDGE_PROTOCOL,
        runtime: 'mnote-leptos-tiptap-spike',
        version: '1.1.0',
        source: source || 'mnote-web-document-session',
        event: COMMAND_EVENT,
        payload,
      },
    }));
  };

  const clearEmbeddedLocalDraft = (runtimeDescriptor) => {
    if (runtimeDescriptor.bootstrap.sourceKind !== 'local_folder') return;
    try {
      const storage = window.localStorage;
      const base = 'mnote.leptos-tiptap-spike.document';
      const keys = [
        `${base}:${runtimeDescriptor.bootstrap.workspaceId}:${runtimeDescriptor.bootstrap.documentId}`,
        `${base}:${runtimeDescriptor.bootstrap.documentId}`,
      ];
      for (const key of keys) storage.removeItem(key);
    } catch (error) {
      console.warn('mnote local folder 草稿清理失败', error);
    }
  };

  const setSessionStatus = (session, status, message) => {
    session.status = status;
    session.error = message || null;
    sessionViews(session).forEach((view) => {
      setStatus(view.runtimeDescriptor, status, message);
    });
  };

  const dispatchSessionContentToView = (session, view, source) => {
    view.suppressedSerialized = session.currentSerialized;
    view.lastKnownSerialized = session.currentSerialized;
    preserveViewScrollPosition(view, () => {
      dispatchRuntimeCommand(view, {
        command: 'replaceContent',
        documentId: session.documentId,
        workspaceId: session.workspaceId,
        title: session.title,
        content: session.currentTiptapDocument,
        revision: session.revision,
        conflictDetectionKey: session.conflictDetectionKey,
        readOnly: session.readOnly,
        editable: !session.readOnly,
      }, source || 'mnote-web-document-session-sync');
    });
  };

  const dispatchSessionMetaToView = (session, view, source) => {
    dispatchRuntimeCommand(view, {
      command: 'replaceContent',
      documentId: session.documentId,
      workspaceId: session.workspaceId,
      title: session.title,
      revision: session.revision,
      conflictDetectionKey: session.conflictDetectionKey,
      readOnly: session.readOnly,
      editable: !session.readOnly,
    }, source || 'mnote-web-document-session-meta');
  };

  const syncSessionMetaToViews = (session) => {
    sessionViews(session).forEach((view) => {
      if (view.mountId != null) dispatchSessionMetaToView(session, view);
    });
  };

  const broadcastSessionContent = (session, sourceView) => {
    sessionViews(session).forEach((view) => {
      if (sourceView && view.id === sourceView.id) return;
      if (view.mountId == null) return;
      dispatchSessionContentToView(session, view);
    });
  };

  const normalizePlainText = (value) => String(value || '').replace(/\s+/g, ' ').trim();

  const sessionPlainText = (session) => {
    const liveText = sessionViews(session)
      .map((view) => normalizePlainText(currentEditorText(view)))
      .find((text) => text);
    if (liveText) return liveText;
    return normalizePlainText(flattenText(session.currentTiptapDocument));
  };

  const sessionHasRecentExternalSignal = (session) => (
    session.sourceKind === 'local_folder'
      && Number.isFinite(session.lastExternalChangeSignalAt)
      && session.lastExternalChangeSignalAt > 0
      && (Date.now() - session.lastExternalChangeSignalAt) < 1500
  );

  const sessionHasRecentLocalInput = (session) => (
    Number.isFinite(session.lastUserInputAt)
      && session.lastUserInputAt > 0
      && (Date.now() - session.lastUserInputAt) < 1500
  );

  const fetchLatestSessionAggregate = async (session) => {
    const response = await fetch(pageAggregateUrl({
      documentId: session.documentId,
      sourceKind: session.sourceKind,
      workspaceId: session.workspaceId,
      rootUri: session.rootUri,
    }).toString(), {
      cache: 'no-store',
      headers: { accept: 'application/json' },
    });
    if (!response.ok) throw new Error('conflict_latest_fetch_failed_' + response.status);
    const payload = await response.json();
    const nextAggregate = payload?.result;
    if (!nextAggregate || typeof nextAggregate !== 'object') {
      throw new Error('conflict_latest_missing_aggregate');
    }
    return nextAggregate;
  };

  const fetchLatestResourceSnapshot = async (session) => {
    const url = new URL('/api/local-folder/resource/read', window.location.origin);
    url.searchParams.set('rootUri', session.rootUri || '');
    url.searchParams.set('path', session.resourcePath || '');
    const response = await fetch(url.toString(), {
      cache: 'no-store',
      headers: { accept: 'application/json' },
    });
    const payload = await response.json().catch(() => null);
    if (!response.ok || !payload || payload.ok !== true) throw new Error('resource_conflict_latest_fetch_failed_' + response.status);
    const nextResource = payload.result;
    if (!nextResource || typeof nextResource !== 'object') throw new Error('resource_conflict_latest_missing_snapshot');
    return nextResource;
  };

  const aggregatePlainText = (aggregate) => {
    const body = aggregate?.body || {};
    return flattenText(pageBodyTiptapDocument(body)).replace(/\n{3,}/g, '\n\n').trim();
  };

  const resourceSnapshotPlainText = (snapshot) => (
    String(snapshot?.text || flattenText(toTiptapDocument(snapshot?.content, '')) || '')
      .replace(/\n{3,}/g, '\n\n')
      .trim()
  );

  const shouldRetryTransientEmptyLocalAggregate = (session, nextAggregate) => {
    if (!session || session.sourceKind !== 'local_folder') return false;
    if (!sessionHasRecentExternalSignal(session)) return false;
    if (!sessionPlainText(session)) return false;
    return !aggregatePlainText(nextAggregate);
  };

  const delay = (ms) => new Promise((resolve) => window.setTimeout(resolve, ms));

  const conflictEnvelopeFromResponse = (payload) => (
    payload?.error?.details?.conflict
      || payload?.details?.conflict
      || null
  );

  const conflictSourceLabel = (session) => {
    if (!session) return '';
    if (session.lastExternalWriteSource === 'mnote-hermes-tool') {
      const runId = String(session.lastExternalWriteRunId || '').trim();
      return runId ? `agent run ${runId}` : 'agent run';
    }
    return '本地文件变更';
  };

  const conflictMessageFromEnvelope = (session, envelope) => {
    const baseMessage = envelope?.message || externalConflictMessage;
    const sourceLabel = conflictSourceLabel(session);
    return sourceLabel ? `${baseMessage}（来源：${sourceLabel}）` : baseMessage;
  };

  const ensureSessionConflictEnvelope = (session, message) => {
    if (!session) return null;
    if (session.lastExternalConflictEnvelope) return session.lastExternalConflictEnvelope;
    const envelope = {
      code: 'local_markdown_external_change',
      documentId: session.documentId,
      rootUri: session.rootUri || null,
      currentDiskVersion: session.conflictDetectionKey || session.fileVersion || null,
      editorBaseVersion: session.lastExternalConflictDetectionKey || session.conflictDetectionKey || session.fileVersion || null,
      externalActor: session.lastExternalWriteSource || null,
      dirtyState: session.dirty ? 'Dirty' : (session.hasExternalConflict ? 'ExternalModified' : 'Clean'),
      bufferFileVersion: session.fileVersion || session.conflictDetectionKey || null,
      message: message || externalConflictMessage,
      suggestedActions: ['accept_disk', 'keep_editor', 'open_diff', 'merge'],
    };
    session.lastExternalConflictEnvelope = envelope;
    return envelope;
  };

  const clearSessionConflictSurface = (session) => {
    var _rt_ = window.__mnoteDocumentConflictPanelRuntime;
    if (_rt_ && typeof _rt_.clearSessionConflictSurface === 'function') {
      _rt_.clearSessionConflictSurface(session, { sessionViews: sessionViews });
      return;
    }
    sessionViews(session).forEach((view) => {
      const host = view.runtimeDescriptor.root.closest('.document-pane') || view.runtimeDescriptor.root;
      if (!(host instanceof HTMLElement)) return;
      host.querySelectorAll('[data-testid="mnote-editor-conflict-panel"]').forEach((node) => node.remove());
    });
  };

  const applyAggregateSnapshotToSession = (session, nextAggregate, source) => {
    const nextBody = nextAggregate?.body || {};
    const nextPermissions = nextAggregate?.head?.permissions || {};
    const nextConflictKey = conflictDetectionKeyFromBody(nextBody);
    const nextTiptapDocument = pageBodyTiptapDocument(nextBody, '', session);
    const nextSerialized = JSON.stringify(nextTiptapDocument);
    const nextRevision = Number.isInteger(nextBody.revision) ? nextBody.revision : revisionFromConflictKey(nextConflictKey);
    session.latestAggregate = nextAggregate;
    syncPageAggregateScript(session, nextAggregate);
    session.title = nextAggregate?.head?.title || session.title;
    session.currentTiptapDocument = nextTiptapDocument;
    session.currentSerialized = nextSerialized;
    session.lastPersistedSerialized = nextSerialized;
    session.revision = nextRevision;
    session.conflictDetectionKey = nextConflictKey;
    session.lastExternalConflictDetectionKey = nextConflictKey || '';
    session.readOnly = Boolean(nextPermissions.readOnly);
    session.dirty = false;
    session.hasExternalConflict = false;
    session.externalChangePending = false;
    session.lastUserInputAt = 0;
    clearSessionConflictSurface(session);
    sessionViews(session).forEach((view) => {
      if (view.mountId != null) dispatchSessionContentToView(session, view, source || 'mnote-web-conflict-resolved');
    });
    setSessionStatus(session, 'synced-external-change');
  };

  const applyResourceSnapshotToSession = (session, nextResource, source) => {
    const nextConflictKey = String(nextResource?.fileVersion || nextResource?.conflictDetectionKey || '').trim();
    const nextTiptapDocument = toTiptapDocument(nextResource?.content, nextResource?.text || '');
    const nextSerialized = JSON.stringify(nextTiptapDocument);
    session.currentTiptapDocument = nextTiptapDocument;
    session.currentSerialized = nextSerialized;
    session.lastPersistedSerialized = nextSerialized;
    if (nextConflictKey) {
      session.conflictDetectionKey = nextConflictKey;
      session.lastExternalConflictDetectionKey = nextConflictKey;
      session.fileVersion = nextConflictKey;
    }
    session.dirty = false;
    session.hasExternalConflict = false;
    session.externalChangePending = false;
    session.lastUserInputAt = 0;
    clearSessionConflictSurface(session);
    sessionViews(session).forEach((view) => {
      if (view.mountId != null) dispatchSessionContentToView(session, view, source || 'mnote-web-resource-conflict-resolved');
    });
    setSessionStatus(session, 'synced-external-change');
  };

  const openConflictDiffPanel = async (session, panel) => {
    const diffPanel = panel.querySelector('[data-testid="mnote-conflict-diff-panel"]');
    if (!(diffPanel instanceof HTMLElement)) return;
    diffPanel.hidden = false;
    diffPanel.replaceChildren();
    const loading = document.createElement('div');
    loading.className = 'mnote-conflict-diff-status';
    loading.textContent = '正在读取磁盘版本...';
    diffPanel.appendChild(loading);
    try {
      const latest = session.sessionKind === 'resource'
        ? await fetchLatestResourceSnapshot(session)
        : await fetchLatestSessionAggregate(session);
      const latestText = session.sessionKind === 'resource'
        ? resourceSnapshotPlainText(latest)
        : aggregatePlainText(latest);
      diffPanel.replaceChildren();
      var _dpRT_ = window.__mnoteDocumentConflictPanelRuntime;
      if (_dpRT_ && typeof _dpRT_.populateSessionConflictDiffPanel === 'function') {
        _dpRT_.populateSessionConflictDiffPanel(diffPanel, {
          currentText: sessionPlainText(session) || '(当前编辑器为空)',
          diskText: latestText || '(磁盘版本为空)',
          currentMergeText: sessionPlainText(session) || '',
          diskMergeText: latestText || '',
          mergeDefaultText: sessionPlainText(session) || latestText || '',
        }, {
          onUseCurrent: function() {
            return sessionPlainText(session) || '';
          },
          onUseDisk: function() {
            return latestText || '';
          },
          onSaveMerge: function(mergeText) {
            writeMergedConflictResult(session, panel, mergeText).catch((error) => {
              setSessionStatus(session, 'external-change-conflict', error instanceof Error ? error.message : String(error));
              renderSessionConflictSurface(session, error instanceof Error ? error.message : String(error));
            });
          },
        });
      } else {
        const current = document.createElement('pre');
        current.setAttribute('data-testid', 'mnote-conflict-current-text');
        current.textContent = sessionPlainText(session) || '(当前编辑器为空)';
        const disk = document.createElement('pre');
        disk.setAttribute('data-testid', 'mnote-conflict-disk-text');
        disk.textContent = latestText || '(磁盘版本为空)';
        const currentTitle = document.createElement('h3');
        currentTitle.textContent = '当前编辑器版本';
        const diskTitle = document.createElement('h3');
        diskTitle.textContent = '磁盘版本';
        const currentBox = document.createElement('section');
        currentBox.append(currentTitle, current);
        const diskBox = document.createElement('section');
        diskBox.append(diskTitle, disk);
        const mergeTitle = document.createElement('h3');
        mergeTitle.textContent = '合并结果';
        const mergeText = document.createElement('textarea');
        mergeText.setAttribute('data-testid', 'mnote-conflict-merge-text');
        mergeText.value = sessionPlainText(session) || latestText || '';
        const mergeActions = document.createElement('div');
        mergeActions.className = 'mnote-conflict-actions';
        const useCurrent = document.createElement('button');
        useCurrent.type = 'button';
        useCurrent.textContent = '使用当前版本';
        useCurrent.setAttribute('data-testid', 'mnote-conflict-merge-use-current');
        const useDisk = document.createElement('button');
        useDisk.type = 'button';
        useDisk.textContent = '使用磁盘版本';
        useDisk.setAttribute('data-testid', 'mnote-conflict-merge-use-disk');
        const saveMerge = document.createElement('button');
        saveMerge.type = 'button';
        saveMerge.textContent = '写回合并结果';
        saveMerge.setAttribute('data-testid', 'mnote-conflict-merge-save');
        mergeActions.append(useCurrent, useDisk, saveMerge);
        const mergeBox = document.createElement('section');
        mergeBox.className = 'mnote-conflict-merge-box';
        mergeBox.append(mergeTitle, mergeText, mergeActions);
        diffPanel.append(currentBox, diskBox, mergeBox);
        useCurrent.addEventListener('click', () => {
          mergeText.value = sessionPlainText(session) || '';
        });
        useDisk.addEventListener('click', () => {
          mergeText.value = latestText || '';
        });
        saveMerge.addEventListener('click', () => {
          writeMergedConflictResult(session, panel, mergeText.value).catch((error) => {
            setSessionStatus(session, 'external-change-conflict', error instanceof Error ? error.message : String(error));
            renderSessionConflictSurface(session, error instanceof Error ? error.message : String(error));
          });
        });
      }
    } catch (error) {
      loading.textContent = error instanceof Error ? error.message : String(error);
      diffPanel.replaceChildren(loading);
    }
  };

  const acceptDiskVersion = async (session) => {
    setSessionStatus(session, 'conflict-resolving', '正在接受磁盘版本...');
    if (session.sessionKind === 'resource') {
      const latestResource = await fetchLatestResourceSnapshot(session);
      applyResourceSnapshotToSession(session, latestResource, 'mnote-web-resource-conflict-accept-disk');
      return;
    }
    const latest = await fetchLatestSessionAggregate(session);
    applyAggregateSnapshotToSession(session, latest, 'mnote-web-conflict-accept-disk');
  };

  const keepCurrentEditorVersion = async (session) => {
    setSessionStatus(session, 'conflict-resolving', '正在保留当前编辑器版本...');
    const latest = session.sessionKind === 'resource'
      ? await fetchLatestResourceSnapshot(session)
      : await fetchLatestSessionAggregate(session);
    const hydrateView = sessionViews(session).find((item) => item.mountId != null) || sessionViews(session)[0];
    if (hydrateView) {
      const liveText = normalizePlainText(currentEditorText(hydrateView));
      if (liveText) {
        session.currentTiptapDocument = hydrateMindmapAttrsFromDom(
          textToTiptapDocument(liveText),
          hydrateView.runtimeDescriptor.root,
        );
      }
      session.currentSerialized = JSON.stringify(session.currentTiptapDocument);
    }
    const nextKey = session.sessionKind === 'resource'
      ? String(latest?.fileVersion || latest?.conflictDetectionKey || '').trim()
      : conflictDetectionKeyFromBody(latest.body || {});
    if (nextKey) {
      session.conflictDetectionKey = nextKey;
      session.lastExternalConflictDetectionKey = nextKey;
    }
    session.hasExternalConflict = false;
    session.externalChangePending = false;
    session.saving = false;
    session.dirty = true;
    clearSessionConflictSurface(session);
    await persistSession(session);
  };

  const writeMergedConflictResult = async (session, panel, mergedText) => {
    setSessionStatus(session, 'conflict-resolving', '正在写回合并结果...');
    const latest = session.sessionKind === 'resource'
      ? await fetchLatestResourceSnapshot(session)
      : await fetchLatestSessionAggregate(session);
    const nextKey = session.sessionKind === 'resource'
      ? String(latest?.fileVersion || latest?.conflictDetectionKey || '').trim()
      : conflictDetectionKeyFromBody(latest.body || {});
    if (nextKey) {
      session.conflictDetectionKey = nextKey;
      session.lastExternalConflictDetectionKey = nextKey;
    }
    session.currentTiptapDocument = textToTiptapDocument(String(mergedText || ''));
    session.currentSerialized = JSON.stringify(session.currentTiptapDocument);
    session.hasExternalConflict = false;
    session.externalChangePending = false;
    session.saving = false;
    session.dirty = true;
    if (panel && typeof panel.remove === 'function') panel.remove();
    await persistSession(session);
  };

  const runSessionConflictAction = (action, onError) => {
    const runtime = window.__mnoteDocumentConflictPanelRuntime;
    if (runtime && typeof runtime.runSessionConflictAction === 'function') {
      runtime.runSessionConflictAction(action, { onError });
      return;
    }
    try {
      const result = action();
      if (result && typeof result.then === 'function') result.catch(onError);
    } catch (error) {
      onError(error);
    }
  };

  const renderSessionConflictSurface = (session, message) => {
    clearSessionConflictSurface(session);
    sessionViews(session).forEach((view) => {
      const host = view.runtimeDescriptor.root.closest('.document-pane') || view.runtimeDescriptor.root;
      if (!(host instanceof HTMLElement)) return;
      const runtime = window.__mnoteDocumentConflictPanelRuntime;
      const handleConflictActionError = (error) => {
        setSessionStatus(session, 'external-change-conflict', error instanceof Error ? error.message : String(error));
        renderSessionConflictSurface(session, error instanceof Error ? error.message : String(error));
      };
      let panel = null;
      if (runtime && typeof runtime.createSessionConflictPanel === 'function') {
        panel = runtime.createSessionConflictPanel(session, message, {
          externalConflictMessage,
          conflictSourceLabel,
          onAcceptDisk: () => {
            runSessionConflictAction(() => acceptDiskVersion(session), handleConflictActionError);
          },
          onKeepCurrent: () => {
            runSessionConflictAction(() => keepCurrentEditorVersion(session), handleConflictActionError);
          },
          onOpenDiff: (_session, createdPanel) => {
            openConflictDiffPanel(session, createdPanel);
          },
        });
      }
      if (!(panel instanceof HTMLElement)) {
        panel = document.createElement('section');
        panel.className = 'mnote-editor-conflict-panel';
        panel.setAttribute('data-testid', 'mnote-editor-conflict-panel');
        panel.setAttribute('role', 'status');
        panel.setAttribute('aria-live', 'polite');

        const heading = document.createElement('h2');
        heading.textContent = '文件冲突';
        const text = document.createElement('p');
        text.textContent = message || externalConflictMessage;
        const meta = document.createElement('div');
        meta.className = 'mnote-conflict-meta';
        const fileLabel = session.sessionKind === 'resource'
          ? `${session.rootUri || ''}/${session.resourcePath || session.documentId}`
          : (session.rootUri || session.documentId);
        meta.textContent = `文件：${fileLabel} · 来源：${conflictSourceLabel(session) || '本地文件变更'}`;
        const actions = document.createElement('div');
        actions.className = 'mnote-conflict-actions';
        const acceptDisk = document.createElement('button');
        acceptDisk.type = 'button';
        acceptDisk.textContent = '接受磁盘版本';
        acceptDisk.setAttribute('data-testid', 'mnote-conflict-accept-disk');
        const keepCurrent = document.createElement('button');
        keepCurrent.type = 'button';
        keepCurrent.textContent = '保留当前编辑器版本';
        keepCurrent.setAttribute('data-testid', 'mnote-conflict-keep-current');
        const openDiff = document.createElement('button');
        openDiff.type = 'button';
        openDiff.textContent = '打开 diff';
        openDiff.setAttribute('data-testid', 'mnote-conflict-open-diff');
        actions.append(acceptDisk, keepCurrent, openDiff);
        const diffPanel = document.createElement('div');
        diffPanel.className = 'mnote-conflict-diff-panel';
        diffPanel.setAttribute('data-testid', 'mnote-conflict-diff-panel');
        diffPanel.hidden = true;
        panel.append(heading, text, meta, actions, diffPanel);

        acceptDisk.addEventListener('click', () => {
          runSessionConflictAction(() => acceptDiskVersion(session), handleConflictActionError);
        });
        keepCurrent.addEventListener('click', () => {
          runSessionConflictAction(() => keepCurrentEditorVersion(session), handleConflictActionError);
        });
        openDiff.addEventListener('click', () => {
          openConflictDiffPanel(session, panel);
        });
      }

      const header = host.querySelector('.document-shell-header');
      if (header && header.parentNode) {
        header.parentNode.insertBefore(panel, header.nextSibling);
      } else {
        host.prepend(panel);
      }
    });
  };

  const markSessionExternalConflict = (session, message) => {
    session.externalChangePending = false;
    ensureSessionConflictEnvelope(session, message);
    session.hasExternalConflict = true;
    if (session.saveTimer) {
      window.clearTimeout(session.saveTimer);
      session.saveTimer = 0;
    }
    const nextMessage = message || externalConflictMessage;
    setSessionStatus(session, 'external-change-conflict', nextMessage);
    renderSessionConflictSurface(session, nextMessage);
  };

  const queueSessionSave = (session) => {
    if (session.readOnly || session.hasExternalConflict) return;
    if (session.saveTimer) window.clearTimeout(session.saveTimer);
    setSessionStatus(session, 'dirty');
    session.saveTimer = window.setTimeout(() => {
      session.saveTimer = 0;
      void persistSession(session);
    }, 650);
  };

  const persistSession = async (session) => {
    if (session.readOnly || session.saving || session.hasExternalConflict) return;
    const hydrateView = sessionViews(session).find((item) => item.mountId != null) || sessionViews(session)[0];
    if (hydrateView) {
      hydrateMindmapAttrsFromDom(session.currentTiptapDocument, hydrateView.runtimeDescriptor.root);
      session.currentSerialized = JSON.stringify(session.currentTiptapDocument);
    }
    const serialized = session.currentSerialized;
    if (!session.dirty && serialized === session.lastPersistedSerialized) {
      setSessionStatus(session, 'saved');
      return;
    }
    session.saving = true;
    setSessionStatus(session, 'saving');
    try {
      const editorDocument = editorDocumentFromTiptapDocument({ documentId: session.documentId }, session.currentTiptapDocument);
      const content = legacyBlocksFromEditorDocument(editorDocument);
      const saveEndpoint = session.saveEndpoint || '/api/documents/save';
      const savePayload = session.sessionKind === 'resource'
        ? {
          rootUri: session.rootUri,
          path: session.resourcePath,
          expectedFileVersion: session.conflictDetectionKey,
          contentFormat: 'editorBlocks',
          editorSource: 'tiptap-resource-tab',
          editorDocument,
          content,
          tiptapDocument: session.currentTiptapDocument,
          blockCount: editorDocument.blocks.length,
        }
        : {
          documentId: session.documentId,
          workspaceId: session.workspaceId,
          sourceKind: session.sourceKind,
          rootUri: session.rootUri,
          revision: session.revision,
          expectedFileVersion: session.conflictDetectionKey,
          contentFormat: 'editorBlocks',
          editorSource: 'tiptap',
          editorDocument,
          content,
          tiptapDocument: session.currentTiptapDocument,
          blockCount: editorDocument.blocks.length,
        };
      if (session.sourceKind !== 'local_folder' && session.sessionKind !== 'resource') {
        savePayload.conflictDetectionKey = session.conflictDetectionKey;
      }
      markLocalFolderSelfChangeSuppression(session);
      const response = await fetch(saveEndpoint, {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify(savePayload),
      });
      const result = await response.json().catch(() => null);
      if (!response.ok || !result || result.ok !== true) {
        const message = result?.error?.message || result?.message || `save_failed_${response.status}`;
        const conflictEnvelope = conflictEnvelopeFromResponse(result);
        if (response.status === 409 && conflictEnvelope && session.sourceKind === 'local_folder') {
          session.saving = false;
          session.lastExternalConflictEnvelope = conflictEnvelope;
          markSessionExternalConflict(session, conflictMessageFromEnvelope(session, conflictEnvelope));
          return;
        }
        throw new Error(message);
      }
      const saved = result.result || {};
      if (Number.isInteger(saved.revision)) session.revision = saved.revision;
      if (typeof saved.conflict_detection_key === 'string' && saved.conflict_detection_key.trim()) {
        session.conflictDetectionKey = saved.conflict_detection_key.trim();
      }
      if (typeof saved.conflictDetectionKey === 'string' && saved.conflictDetectionKey.trim()) {
        session.conflictDetectionKey = saved.conflictDetectionKey.trim();
      }
      if (typeof saved.fileVersion === 'string' && saved.fileVersion.trim()) {
        session.conflictDetectionKey = saved.fileVersion.trim();
      }
      if (session.conflictDetectionKey) session.lastExternalConflictDetectionKey = session.conflictDetectionKey;
      session.hasExternalConflict = false;
      session.externalChangePending = false;
      session.lastUserInputAt = 0;
      syncSessionMetaToViews(session);
      session.lastPersistedSerialized = serialized;
      session.saving = false;
      if (session.sessionKind !== 'resource' && typeof window.__mnoteRecordPageHistorySnapshot === 'function') {
        const primaryView = sessionViews(session).find((view) => view.runtimeDescriptor.paneRole === 'primary') || sessionViews(session)[0];
        if (primaryView) {
          const plainText = sessionPlainText(session);
          window.__mnoteRecordPageHistorySnapshot('save', {
            wordCount: content.filter((block) => typeof block?.content === 'string' && block.content.trim()).length,
            characterCount: plainText.replace(/\s/g, '').length,
            blockCount: editorDocument.blocks.length,
            todoTotal: editorDocument.blocks.filter((block) => block.blockType === 'todo').length,
            todoDone: editorDocument.blocks.filter((block) => block.blockType === 'todo' && block.props?.checked).length,
          });
        }
      }
      if (session.currentSerialized === serialized) {
        session.dirty = false;
        setSessionStatus(session, 'saved');
      } else {
        session.dirty = true;
        setSessionStatus(session, 'dirty');
        queueSessionSave(session);
      }
      syncResourceSessionTabGuards(session);
    } catch (error) {
      session.saving = false;
      setSessionStatus(session, 'error', error instanceof Error ? error.message : String(error));
      syncResourceSessionTabGuards(session);
    }
  };

  const scheduleSessionExternalRefresh = (session, source) => {
    if (!session || session.views.size === 0) return;
    if (session.externalRefreshTimer) return;
    session.externalRefreshSource = source || session.externalRefreshSource || 'mnote-web-external-change';
    session.externalRefreshTimer = window.setTimeout(() => {
      const refreshSource = session.externalRefreshSource || 'mnote-web-external-change';
      session.externalRefreshSource = '';
      session.externalRefreshTimer = 0;
      void refreshSessionFromExternalChange(session, refreshSource);
    }, 120);
  };

  const refreshSessionFromExternalChange = async (session, source) => {
    if (document.hidden) return;
    if (!session || session.views.size === 0) return;
    if (session.sourceKind === 'local_folder' && !session.rootUri) return;
    try {
      const response = await fetch(pageAggregateUrl({
        documentId: session.documentId,
        sourceKind: session.sourceKind,
        workspaceId: session.workspaceId,
        rootUri: session.rootUri,
      }).toString(), {
        cache: 'no-store',
        headers: { accept: 'application/json' },
      });
      if (!response.ok) {
        if (session.sourceKind === 'local_folder') {
          markSessionExternalConflict(session, externalConflictMessage);
        }
        return;
      }
      const payload = await response.json();
      const conflictEnvelope = conflictEnvelopeFromResponse(payload);
      if (response.status === 409 && conflictEnvelope) {
        session.lastExternalConflictEnvelope = conflictEnvelope;
        markSessionExternalConflict(session, conflictMessageFromEnvelope(session, conflictEnvelope));
        return;
      }
      let nextAggregate = payload?.result;
      if (shouldRetryTransientEmptyLocalAggregate(session, nextAggregate)) {
        await delay(500);
        try {
          const retryAggregate = await fetchLatestSessionAggregate(session);
          if (aggregatePlainText(retryAggregate)) {
            nextAggregate = retryAggregate;
          } else {
            markSessionExternalConflict(session, '检测到外部编辑器正在写入空内容，已暂停自动刷新以保护当前编辑区。');
            return;
          }
        } catch (_retryError) {
          markSessionExternalConflict(session, externalConflictMessage);
          return;
        }
      }
      const nextBody = nextAggregate?.body || {};
      const nextPermissions = nextAggregate?.head?.permissions || {};
      const nextConflictKey = conflictDetectionKeyFromBody(nextBody);
      const nextTiptapDocument = pageBodyTiptapDocument(nextBody, '', session);
      const nextSerialized = JSON.stringify(nextTiptapDocument);
      const contentChanged = nextSerialized !== session.currentSerialized;
      session.externalChangePending = false;
      if (!nextConflictKey || !session.lastExternalConflictDetectionKey) {
        session.lastExternalConflictDetectionKey = nextConflictKey || session.lastExternalConflictDetectionKey;
        if (!contentChanged) return;
      }
      if (nextConflictKey === session.lastExternalConflictDetectionKey && !contentChanged) return;
      if (session.hasExternalConflict || sessionHasRecentLocalInput(session) || session.dirty || session.saveTimer || session.saving) {
        markSessionExternalConflict(session, externalConflictMessage);
        return;
      }
      const nextRevision = Number.isInteger(nextBody.revision) ? nextBody.revision : revisionFromConflictKey(nextConflictKey);
      session.latestAggregate = nextAggregate;
      syncPageAggregateScript(session, nextAggregate);
      session.title = nextAggregate?.head?.title || session.title;
      session.currentTiptapDocument = nextTiptapDocument;
      session.currentSerialized = nextSerialized;
      session.lastPersistedSerialized = session.currentSerialized;
      session.revision = nextRevision;
      session.conflictDetectionKey = nextConflictKey;
      session.lastExternalConflictDetectionKey = nextConflictKey || '';
      session.readOnly = Boolean(nextPermissions.readOnly);
      session.dirty = false;
      session.hasExternalConflict = false;
      session.lastUserInputAt = 0;
      sessionViews(session).forEach((view) => {
        if (view.mountId != null) dispatchSessionContentToView(session, view, source || 'mnote-web-external-change');
      });
      setSessionStatus(session, 'synced-external-change');
    } catch (error) {
      console.warn('mnote 页面外部更新检测失败', error);
    }
  };

  const refreshSessionFromExternalFileChange = async (session) => {
    if (session.sourceKind !== 'local_folder') return;
    await refreshSessionFromExternalChange(session, 'mnote-web-local-folder-watch');
  };

  const ensureLocalFolderEventChannel = (session) => {
    if (session.sourceKind !== 'local_folder' || !session.rootUri || typeof window.EventSource !== 'function') {
      return;
    }
    const channelKey = localFolderEventChannelKey(session);
    let channel = localFolderEventRegistry.get(channelKey);
    if (!channel) {
      const url = new URL('/api/local-folder/events', window.location.origin);
      url.searchParams.set('rootUri', session.rootUri);
      url.searchParams.set('documentId', session.documentId);
      const eventSource = new EventSource(url.toString());
      channel = {
        key: channelKey,
        rootUri: session.rootUri,
        documentId: session.documentId,
        eventSource,
        sessions: new Map(),
      };
      eventSource.addEventListener('change', (event) => {
        const payload = parseLocalFolderEventPayload(event);
        if (!payload) return;
        Array.from(channel.sessions.values()).forEach((targetSession) => {
          if (!targetSession || targetSession.views.size === 0) return;
          const documentId = typeof payload.documentId === 'string' ? payload.documentId.trim() : '';
          if (!documentId) return;
          const eventKind = String(payload.eventKind || '');
          const targetsCurrentDocument = Boolean(documentId && documentId === targetSession.documentId);
          if (documentId && !targetsCurrentDocument) return;
          if (targetsCurrentDocument && shouldSuppressLocalFolderSelfChange(documentId, eventKind)) {
            targetSession.externalChangePending = false;
            targetSession.lastSelfSaveSignalAt = Date.now();
            return;
          }
          if (targetsCurrentDocument && targetSession.saving) {
            targetSession.externalChangePending = false;
            targetSession.lastSelfSaveSignalAt = Date.now();
            return;
          }
          targetSession.lastExternalChangeSignalAt = Date.now();
          targetSession.externalChangePending = true;
          if (targetsCurrentDocument && (targetSession.hasExternalConflict || sessionHasRecentLocalInput(targetSession) || targetSession.dirty || targetSession.saveTimer || targetSession.saving)) {
            markSessionExternalConflict(targetSession, externalConflictMessage);
            return;
          }
          scheduleSessionExternalRefresh(targetSession, 'mnote-web-local-folder-watch');
        });
      });
      eventSource.onerror = () => {
        console.warn('mnote local folder 外部更新事件流中断，将等待浏览器自动重连');
      };
      localFolderEventRegistry.set(channelKey, channel);
    }
    channel.sessions.set(session.key, session);
    session.localFolderChannel = channel;
  };

  const readTreePayloadData = (payload) => (
    payload && typeof payload === 'object'
      ? (payload.data || payload.delta || payload)
      : null
  );

  const readTreePayloadOverview = (payload) => (
    payload && typeof payload === 'object' && payload.overview && typeof payload.overview === 'object'
      ? payload.overview
      : null
  );

  const readTreePayloadCursor = (payload) => {
    const raw = String(payload?.cursor || payload?.revision || '').trim();
    if (!raw) return { id: '', createdAt: '', raw: '' };
    try {
      const parsed = JSON.parse(raw);
      if (parsed && typeof parsed === 'object') {
        return {
          id: String(parsed.id || parsed.commandId || parsed.command_id || '').trim(),
          createdAt: String(parsed.createdAt || parsed.created_at || '').trim(),
          raw,
        };
      }
    } catch (_) {}
    return { id: raw, createdAt: '', raw };
  };

  const treeRecordMatchesPayloadCursor = (record, payload) => {
    if (!record || typeof record !== 'object') return false;
    const cursor = readTreePayloadCursor(payload);
    if (!cursor.id && !cursor.createdAt && !cursor.raw) return false;
    const ids = [
      record.id,
      record._id,
      record.command_log_id,
      record.commandLogId,
      record.domain_event_id,
      record.domainEventId,
      record.command_id,
      record.commandId,
    ].map((value) => String(value || '').trim()).filter(Boolean);
    if (cursor.id && ids.includes(cursor.id)) return true;
    const createdAt = String(record.created_at || record.createdAt || '').trim();
    return Boolean(cursor.createdAt && createdAt && cursor.createdAt === createdAt);
  };

  const treeRecordTargetsDocument = (record, documentId) => {
    if (!record || typeof record !== 'object' || !documentId) return false;
    const targetPageId = String(record.target_page_id || record.targetPageId || '').trim();
    const aggregateId = String(record.aggregate_id || record.aggregateId || '').trim();
    if (targetPageId === documentId || aggregateId === documentId) return true;
    const payload = record.payload && typeof record.payload === 'object' ? record.payload : null;
    if (!payload) return false;
    const streamDelta = payload.streamDelta || payload.stream_delta || null;
    const deltaDocumentId = streamDelta && typeof streamDelta === 'object'
      ? String(streamDelta.documentId || streamDelta.pageId || streamDelta.document_id || streamDelta.page_id || '').trim()
      : '';
    return deltaDocumentId === documentId;
  };

  const collectMindmapIdsFromTreeRecord = (record, documentId, out) => {
    if (!record || typeof record !== 'object' || !documentId) return;
    if (!treeRecordTargetsDocument(record, documentId)) return;
    const targetBlockId = String(record.target_block_id || record.targetBlockId || '').trim();
    if (targetBlockId) out.add(targetBlockId);
    const aggregateType = String(record.aggregate_type || record.aggregateType || '').trim();
    const aggregateId = String(record.aggregate_id || record.aggregateId || '').trim();
    if (aggregateType === 'block' && aggregateId) out.add(aggregateId);
    const payload = record.payload && typeof record.payload === 'object' ? record.payload : null;
    const streamDelta = payload && typeof payload === 'object' ? (payload.streamDelta || payload.stream_delta || null) : null;
    const blockId = streamDelta && typeof streamDelta === 'object'
      ? String(streamDelta.blockId || streamDelta.block_id || '').trim()
      : '';
    if (blockId) out.add(blockId);
  };

  const collectMindmapIdsFromTreePayload = (payload, session) => {
    const ids = new Set();
    if (!payload || typeof payload !== 'object' || !session?.documentId) return [];
    const kind = String(payload.kind || '').trim();
    const data = readTreePayloadData(payload);
    if (kind === 'delta' && data && typeof data === 'object') {
      const documentId = String(data.documentId || data.pageId || data.document_id || data.page_id || '').trim();
      if (!documentId || documentId === session.documentId) {
        const blockId = String(data.blockId || data.block_id || '').trim();
        if (blockId) ids.add(blockId);
        const streamDelta = data.streamDelta || data.stream_delta || null;
        const streamBlockId = streamDelta && typeof streamDelta === 'object'
          ? String(streamDelta.blockId || streamDelta.block_id || '').trim()
          : '';
        if (streamBlockId) ids.add(streamBlockId);
      }
    }
    if (kind === 'resync') {
      const overview = readTreePayloadOverview(payload);
      if (overview) {
        const commandLogs = Array.isArray(overview.command_logs) ? overview.command_logs : Array.isArray(overview.commandLogs) ? overview.commandLogs : [];
        const domainEvents = Array.isArray(overview.domain_events) ? overview.domain_events : Array.isArray(overview.domainEvents) ? overview.domainEvents : [];
        commandLogs
          .filter((record) => treeRecordMatchesPayloadCursor(record, payload))
          .forEach((record) => collectMindmapIdsFromTreeRecord(record, session.documentId, ids));
        domainEvents
          .filter((record) => treeRecordMatchesPayloadCursor(record, payload))
          .forEach((record) => collectMindmapIdsFromTreeRecord(record, session.documentId, ids));
      }
    }
    return Array.from(ids);
  };

  const refreshMindmapRuntimesFromTreePayload = (payload, session) => {
    const registry = window.__MNOTE_LEPTOS_MINDMAP_BRIDGES__ || {};
    collectMindmapIdsFromTreePayload(payload, session).forEach((mindmapId) => {
      const bridge = registry[mindmapId];
      if (bridge && typeof bridge.refreshProjection === 'function') {
        void bridge.refreshProjection('mnote-web-tree-live');
      }
    });
  };

  const treePayloadTargetsDocument = (payload, session) => {
    if (!payload || typeof payload !== 'object' || !session?.documentId) return false;
    if (payload.workspaceId && session.workspaceId && String(payload.workspaceId) !== String(session.workspaceId)) return false;
    const data = readTreePayloadData(payload);
    if (data && typeof data === 'object') {
      const documentId = String(data.documentId || data.pageId || data.document_id || data.page_id || '').trim();
      if (documentId === session.documentId) return true;
      const documents = Array.isArray(data.upsertDocuments) ? data.upsertDocuments : Array.isArray(data.upsert_documents) ? data.upsert_documents : [];
      if (documents.some((item) => String(item?.id || item?.documentId || '').trim() === session.documentId)) return true;
    }
    const overview = readTreePayloadOverview(payload);
    if (!overview) return false;
    const commandLogs = Array.isArray(overview.command_logs) ? overview.command_logs : Array.isArray(overview.commandLogs) ? overview.commandLogs : [];
    const domainEvents = Array.isArray(overview.domain_events) ? overview.domain_events : Array.isArray(overview.domainEvents) ? overview.domainEvents : [];
    return commandLogs.some((record) => treeRecordTargetsDocument(record, session.documentId))
      || domainEvents.some((record) => treeRecordTargetsDocument(record, session.documentId));
  };

  const handleTreeExternalChange = (event) => {
    const payload = event?.detail?.payload || event?.detail || null;
      if (!payload) return;
    Array.from(documentSessionRegistry.values()).forEach((session) => {
      if (session.sourceKind === 'local_folder') return;
      if (!treePayloadTargetsDocument(payload, session)) return;
      refreshMindmapRuntimesFromTreePayload(payload, session);
      session.lastExternalChangeSignalAt = Date.now();
      session.externalChangePending = true;
      if (session.hasExternalConflict || sessionHasRecentLocalInput(session) || session.dirty || session.saveTimer || session.saving) {
        markSessionExternalConflict(session, treeExternalConflictMessage);
        return;
      }
      scheduleSessionExternalRefresh(session, 'mnote-web-tree-live');
    });
  };

  const sessionMatchesDocumentWorkspace = (session, documentId, workspaceId) => {
    if (!session) return false;
    const doc = String(documentId || '').trim();
    const workspace = String(workspaceId || '').trim();
    if (doc && session.documentId !== doc) return false;
    if (workspace && session.workspaceId && session.workspaceId !== workspace) return false;
    return true;
  };

  const refreshDocumentSessionsFromExternalWrite = (detail, source) => {
    const documentId = String(detail?.documentId || '').trim();
    const workspaceId = String(detail?.workspaceId || '').trim();
    let scheduled = 0;
    Array.from(documentSessionRegistry.values()).forEach((session) => {
      if (!sessionMatchesDocumentWorkspace(session, documentId, workspaceId)) return;
      session.lastExternalChangeSignalAt = Date.now();
      session.externalChangePending = true;
      session.lastExternalWriteSource = source || session.lastExternalWriteSource || '';
      session.lastExternalWriteRunId = String(detail?.runId || detail?.traceId || detail?.toolCallId || '').trim();
      session.lastExternalWriteDocumentId = documentId;
      if (session.hasExternalConflict || sessionHasRecentLocalInput(session) || session.dirty || session.saveTimer || session.saving) {
        markSessionExternalConflict(session, conflictMessageFromEnvelope(session, session.lastExternalConflictEnvelope) || treeExternalConflictMessage);
        return;
      }
      scheduled += 1;
      scheduleSessionExternalRefresh(session, source || 'mnote-web-external-write');
    });
    return scheduled;
  };

  window.addEventListener('tree:delta', handleTreeExternalChange);
  window.addEventListener('tree:resync', handleTreeExternalChange);
  window.addEventListener('mnote:page-ai-tool-write-completed', (event) => {
    refreshDocumentSessionsFromExternalWrite(event?.detail || {}, 'mnote-hermes-tool');
  });

  const createDocumentSession = (runtimeDescriptor) => {
    const pageBody = runtimeDescriptor.aggregate.body || {};
    const permissions = runtimeDescriptor.aggregate.head?.permissions || {};
    const conflictDetectionKey = conflictDetectionKeyFromBody(pageBody);
    const pageBodyRevision = Number.isInteger(pageBody.revision) ? pageBody.revision : null;
    const keyRevision = revisionFromConflictKey(conflictDetectionKey);
    const sourceKind = normalizeSessionSourceKind(runtimeDescriptor.bootstrap);
    const tiptapDocument = pageBodyTiptapDocument(pageBody, '', runtimeDescriptor.bootstrap);
    const session = {
      key: buildDocumentSessionKey(runtimeDescriptor.bootstrap),
      documentId: runtimeDescriptor.bootstrap.documentId,
      workspaceId: runtimeDescriptor.bootstrap.workspaceId,
      sourceKind,
      rootUri: runtimeDescriptor.bootstrap.rootUri,
      saveEndpoint: runtimeDescriptor.bootstrap.saveEndpoint || '/api/documents/save',
      pageAggregateScriptId: runtimeDescriptor.bootstrap.pageAggregateScriptId || '__MNOTE_PAGE_AGGREGATE__',
      latestAggregate: runtimeDescriptor.aggregate,
      title: runtimeDescriptor.aggregate.head?.title || '无标题',
      currentTiptapDocument: tiptapDocument,
      currentSerialized: JSON.stringify(tiptapDocument),
      lastPersistedSerialized: JSON.stringify(tiptapDocument),
      revision: pageBodyRevision && pageBodyRevision > 0 ? pageBodyRevision : keyRevision,
      conflictDetectionKey,
      fileVersion: typeof pageBody.fileVersion === 'string' ? pageBody.fileVersion : conflictDetectionKey,
      lastExternalConflictDetectionKey: conflictDetectionKey || '',
      readOnly: Boolean(permissions.readOnly),
      dirty: false,
      saving: false,
      hasExternalConflict: false,
      externalChangePending: false,
      externalRefreshSource: '',
      lastExternalChangeSignalAt: 0,
      lastSelfSaveSignalAt: 0,
      lastUserInputAt: 0,
      status: 'booting',
      error: null,
      saveTimer: 0,
      externalRefreshTimer: 0,
      releaseTimer: 0,
      views: new Map(),
      localFolderChannel: null,
    };
    ensureLocalFolderEventChannel(session);
    return session;
  };

  const getOrCreateDocumentSession = (runtimeDescriptor) => {
    const key = buildDocumentSessionKey(runtimeDescriptor.bootstrap);
    const existing = documentSessionRegistry.get(key);
    if (existing) {
      cancelDocumentSessionRelease(existing);
      return existing;
    }
    const session = createDocumentSession(runtimeDescriptor);
    documentSessionRegistry.set(key, session);
    return session;
  };

  const updatePaneChrome = (runtimeDescriptor) => {
    const pane = runtimeDescriptor.root.closest('[data-document-pane="true"]');
    if (!(pane instanceof HTMLElement)) return;
    const aggregate = runtimeDescriptor.aggregate || {};
    const bootstrap = runtimeDescriptor.bootstrap || {};
    const title = aggregate?.head?.title || '无标题';
    const documentId = bootstrap.documentId || aggregate?.identity?.documentId || '';
    const workspaceId = bootstrap.workspaceId || aggregate?.identity?.workspaceId || '';
    pane.setAttribute('data-pane-document-id', documentId);
    pane.setAttribute('data-pane-workspace-id', workspaceId);
    pane.setAttribute('data-pane-visible', 'true');
    pane.removeAttribute('data-mnote-side-target');
    pane.hidden = false;
    const shell = pane.querySelector('.document-shell');
    if (shell instanceof HTMLElement) {
      shell.setAttribute('data-document-id', documentId);
      shell.setAttribute('data-workspace-id', workspaceId);
      const options = aggregate?.layout?.pageOptions || {};
      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(Boolean(options.showHeadingNumbers)));
    }
    pane.querySelectorAll('[data-page-title-input="true"]').forEach((node) => {
      if (!(node instanceof HTMLTextAreaElement)) return;
      node.value = title;
      node.setAttribute('data-document-id', documentId);
      node.setAttribute('data-workspace-id', workspaceId);
      node.setAttribute('data-title-last-saved', title);
      node.setAttribute('data-title-save-status', 'saved');
      node.style.height = 'auto';
      node.style.height = `${Math.max(48, node.scrollHeight)}px`;
    });
    pane.querySelectorAll('[data-page-title-current="true"]').forEach((node) => {
      if (node instanceof HTMLElement) node.textContent = title;
    });
    const pageTab = document.querySelector(`[data-mnote-main-tab="page"][data-pane-role="${runtimeDescriptor.paneRole}"]`);
    if (pageTab instanceof HTMLElement) {
      pageTab.setAttribute('data-document-id', documentId);
      pageTab.setAttribute('data-workspace-id', workspaceId);
      const pageTabTitle = pageTab.querySelector('.mnote-main-tab-title');
      if (pageTabTitle instanceof HTMLElement) pageTabTitle.textContent = title;
    }
    if (runtimeDescriptor.paneRole === 'primary') {
      document.body.dataset.documentId = documentId;
      document.body.dataset.mnoteShell = 'document';
      delete document.body.dataset.mindmapId;
      document.title = title;
      const topbarCurrent = document.querySelector('.wolai-breadcrumb-current [data-page-title-current]');
      if (topbarCurrent instanceof HTMLElement) topbarCurrent.textContent = title;
      window.dispatchEvent(new CustomEvent('mnote:primary-document-activated', {
        detail: { documentId, workspaceId, title }
      }));
      document.documentElement.removeAttribute('data-mnote-side-target-unsupported');
      document.documentElement.removeAttribute('data-mnote-side-target-asset-id');
      document.querySelectorAll('.tree-row[data-active="true"], .tree-row[data-selected="true"]').forEach((row) => {
        if (row instanceof HTMLElement) {
          row.setAttribute('data-active', 'false');
          row.setAttribute('data-selected', 'false');
        }
      });
      const escapedId = window.CSS && typeof window.CSS.escape === 'function' ? window.CSS.escape(documentId) : String(documentId).replace(/["\\]/g, '\\$&');
      document.querySelectorAll(`.tree-row[data-node-id="${escapedId}"], .tree-row[data-document-id="${escapedId}"], .tree-row[data-doc-id="${escapedId}"]`).forEach((row) => {
        if (!(row instanceof HTMLElement)) return;
        if (row.getAttribute('data-shell-mode') === 'filetree') {
          row.setAttribute('data-selected', String(row.getAttribute('data-row-id') === `doc:${documentId}`));
        } else {
          row.setAttribute('data-active', 'true');
        }
      });
    }
    runtimeDescriptor.root.removeAttribute('data-mnote-object-editor');
    runtimeDescriptor.root.removeAttribute('data-mnote-object-identity');
    runtimeDescriptor.root.removeAttribute('data-mnote-mindmap-id');
    runtimeDescriptor.root.removeAttribute('data-mnote-side-target-unsupported');
    runtimeDescriptor.root.removeAttribute('data-mnote-side-target-asset-id');
  };

  const fetchPageAggregateForPane = async (descriptor) => {
    const response = await fetch(pageAggregateUrlFromDescriptor(descriptor).toString(), {
      cache: 'no-store',
      headers: { accept: 'application/json' },
    });
    if (!response.ok) throw new Error(`page_aggregate_failed_${response.status}`);
    const payload = await response.json();
    if (!payload?.result) throw new Error('page_aggregate_missing_result');
    return payload.result;
  };

  const replacePaneDocument = async (paneRole, descriptor, options = {}) => {
    unmountMindmapPane(paneRole);
    const runtime = await loadRuntime();
    const root = document.querySelector(`${ROOT_SELECTOR}[data-pane-role="${paneRole}"]`);
    const observability = document.querySelector(`[data-editor-host-observability][data-pane-role="${paneRole}"]`);
    if (!(root instanceof HTMLElement)) throw new Error(`pane_root_missing_${paneRole}`);
    const previousView = paneViewRegistry.get(paneRole);
    if (previousView) {
      unmountEditorViewBinding(previousView);
      paneViewRegistry.delete(paneRole);
    }
    const aggregate = options.aggregate || await fetchPageAggregateForPane(descriptor);
    const bootstrap = buildBootstrapFromAggregate(aggregate, descriptor, paneRole);
    const runtimeDescriptor = { paneRole, root, observability, aggregate, bootstrap };
    updatePaneChrome(runtimeDescriptor);
    if (paneRole === 'secondary' && workspace instanceof HTMLElement) {
      workspace.setAttribute('data-has-secondary-pane', 'true');
      setSecondaryEditorHostVisible(true);
      const resizerNode = document.querySelector('[data-document-pane-resizer="true"]');
      if (resizerNode instanceof HTMLElement) resizerNode.hidden = false;
      applyStoredSecondaryWidth();
    }
    const session = getOrCreateDocumentSession(runtimeDescriptor);
    const view = createEditorViewBinding(session, runtime, runtimeDescriptor);
    const mountOptions = {
      documentId: session.documentId,
      workspaceId: session.workspaceId,
      title: session.title,
      content: session.currentTiptapDocument,
      revision: session.revision,
      conflictDetectionKey: session.conflictDetectionKey,
      readOnly: session.readOnly,
      editable: !session.readOnly,
      pageOptions: runtimeDescriptor.aggregate.layout?.pageOptions || {},
    };
    setStatus(runtimeDescriptor, 'loading-assets');
    clearEmbeddedLocalDraft(runtimeDescriptor);
    try {
      const mountId = runtime.mount(runtimeDescriptor.root, mountOptions);
      view.mountId = mountId;
      runtimeDescriptor.root.setAttribute('data-runtime-mount-id', String(mountId));
      runtimeDescriptor.root.setAttribute('data-editor-host-kind', 'leptos_tiptap_island');
      setStatus(runtimeDescriptor, 'mounting-editor');
      paneViewRegistry.set(paneRole, view);
      return view;
    } catch (error) {
      unmountEditorViewBinding(view);
      throw error;
    }
  };

  const descriptorFromCurrentUrl = (paneRole, documentId, explicit = {}) => {
    const url = currentUrl();
    if (paneRole === 'secondary') {
      return {
        documentId,
        workspaceId: explicit.workspaceId || url.searchParams.get('workspaceId') || '',
        sourceKind: explicit.sourceKind || url.searchParams.get('secondarySourceKind') || url.searchParams.get('sourceKind') || 'convex_workspace',
        rootUri: explicit.rootUri || url.searchParams.get('secondaryRootUri') || url.searchParams.get('rootUri') || '',
      };
    }
    return {
      documentId,
      workspaceId: explicit.workspaceId || url.searchParams.get('workspaceId') || '',
      sourceKind: explicit.sourceKind || url.searchParams.get('sourceKind') || 'convex_workspace',
      rootUri: explicit.rootUri || url.searchParams.get('rootUri') || '',
    };
  };

  const updatePrimaryUrl = (descriptor, urlFromCaller) => {
    const url = urlFromCaller instanceof URL
      ? urlFromCaller
      : new URL(`/documents/${encodeURIComponent(descriptor.documentId)}`, window.location.origin);
    if (!url.searchParams.get('workspaceId') && descriptor.workspaceId) url.searchParams.set('workspaceId', descriptor.workspaceId);
    if (descriptor.sourceKind && descriptor.sourceKind !== 'convex_workspace') url.searchParams.set('sourceKind', descriptor.sourceKind);
    if (descriptor.rootUri) url.searchParams.set('rootUri', descriptor.rootUri);
    pushUrlState(url);
  };

  const setSecondaryEditorHostVisible = (visible) => {
    const host = document.querySelector('[data-testid="mnote-secondary-editor-tab-host"]');
    if (host instanceof HTMLElement) host.hidden = !visible;
  };

  const closeSecondaryPane = (url) => {
    const view = paneViewRegistry.get('secondary');
    if (view) {
      unmountEditorViewBinding(view);
      paneViewRegistry.delete('secondary');
    }
    const pane = document.querySelector('[data-document-pane="true"][data-pane-role="secondary"]');
    if (pane instanceof HTMLElement) {
      pane.hidden = true;
      pane.setAttribute('data-pane-visible', 'false');
      pane.removeAttribute('data-mnote-side-target');
      pane.removeAttribute('data-pane-document-id');
      pane.removeAttribute('data-pane-workspace-id');
    }
    const root = document.querySelector(`${ROOT_SELECTOR}[data-pane-role="secondary"]`);
    if (root instanceof HTMLElement) {
      root.replaceChildren();
      root.removeAttribute('data-mnote-side-target-unsupported');
      root.removeAttribute('data-mnote-side-target-asset-id');
    }
    Array.from(resourceTabRegistry.entries()).forEach(([key, entry]) => {
      if (normalizePaneRole(entry?.paneRole) !== 'secondary') return;
      if (entry.view) unmountEditorViewBinding(entry.view, { releaseSession: true });
      if (entry.mindmapRuntime?.runtime && entry.mindmapRuntime?.mountId != null) {
        try {
          entry.mindmapRuntime.runtime.unmount(entry.mindmapRuntime.mountId);
        } catch (error) {
          console.warn('mnote secondary mindmap resource tab unmount failed', error);
        }
      }
      if (entry.tab instanceof HTMLElement) entry.tab.remove();
      if (entry.panel instanceof HTMLElement) entry.panel.remove();
      resourceTabRegistry.delete(key);
      removeFromResourceTabMru('secondary', key);
    });
    if (workspace instanceof HTMLElement) {
      workspace.setAttribute('data-has-secondary-pane', 'false');
      workspace.style.removeProperty('grid-template-columns');
    }
    setSecondaryEditorHostVisible(false);
    const resizerNode = document.querySelector('[data-document-pane-resizer="true"]');
    if (resizerNode instanceof HTMLElement) resizerNode.hidden = true;
    document.documentElement.removeAttribute('data-mnote-side-target-unsupported');
    document.documentElement.removeAttribute('data-mnote-side-target-asset-id');
    replaceUrlState(url);
  };

  const parseJsonScriptFromDocument = (doc, id) => {
    const node = doc?.getElementById?.(id);
    if (!node) return null;
    try {
      return JSON.parse(node.textContent || 'null');
    } catch (error) {
      console.warn(`mnote mindmap shell JSON 解析失败: ${id}`, error);
      return null;
    }
  };

  const fetchMindmapShellBootstrap = async (url) => {
    const response = await fetch(url.toString(), {
      cache: 'no-store',
      credentials: 'include',
      headers: { accept: 'text/html' },
    });
    if (!response.ok) throw new Error(`mindmap_shell_failed_${response.status}`);
    const html = await response.text();
    const parsed = new DOMParser().parseFromString(html, 'text/html');
    const bootstrap = parseJsonScriptFromDocument(parsed, '__MNOTE_MINDMAP_EDITOR_BOOTSTRAP__');
    if (!bootstrap || typeof bootstrap !== 'object') throw new Error('mindmap_shell_missing_bootstrap');
    const contract = parseJsonScriptFromDocument(parsed, '__MNOTE_MINDMAP_SHELL__') || {};
    const title = String(bootstrap.title || parsed.querySelector('title')?.textContent || '思维导图').trim() || '思维导图';
    return { bootstrap, contract, title };
  };

  const setPrimaryMindmapSelection = (documentId, mindmapId) => {
    const cssEscape = window.CSS && typeof window.CSS.escape === 'function' ? window.CSS.escape : (value) => String(value).replace(/["\\]/g, '\\$&');
    const escapedDocId = cssEscape(documentId);
    const escapedMindmapId = cssEscape(mindmapId);
    document.querySelectorAll('.tree-row[data-active="true"], .tree-row[data-selected="true"]').forEach((row) => {
      if (!(row instanceof HTMLElement)) return;
      row.setAttribute('data-active', 'false');
      row.setAttribute('data-selected', 'false');
    });
    document.querySelectorAll(`.tree-row[data-shell-mode="page"][data-node-id="${escapedDocId}"]`).forEach((row) => {
      if (row instanceof HTMLElement) row.setAttribute('data-active', 'true');
    });
    document.querySelectorAll(`.tree-row[data-shell-mode="filetree"][data-asset-id="${escapedMindmapId}"]`).forEach((row) => {
      if (row instanceof HTMLElement) row.setAttribute('data-selected', 'true');
    });
  };

  const updatePrimaryMindmapChrome = ({ documentId, mindmapId, workspaceId, title, root }) => {
    const pane = root.closest('[data-document-pane="true"]');
    if (pane instanceof HTMLElement) {
      pane.setAttribute('data-pane-document-id', `__mindmap_object__:${documentId}:${mindmapId}`);
      pane.setAttribute('data-pane-workspace-id', workspaceId || '');
      pane.setAttribute('data-pane-visible', 'true');
      pane.hidden = false;
    }
    const shell = root.closest('.document-shell');
    if (shell instanceof HTMLElement) {
      shell.setAttribute('data-editor-host', 'mindmap_object');
      shell.setAttribute('data-document-id', documentId);
      shell.setAttribute('data-workspace-id', workspaceId || '');
      shell.setAttribute('data-mindmap-id', mindmapId);
    }
    document.body.dataset.documentId = documentId;
    document.body.dataset.mindmapId = mindmapId;
    document.body.dataset.mnoteShell = 'mindmap';
    document.title = title;
    document.querySelectorAll('[data-page-title-input="true"][data-pane-role="primary"]').forEach((node) => {
      if (!(node instanceof HTMLTextAreaElement)) return;
      node.value = title;
      node.setAttribute('data-document-id', documentId);
      node.setAttribute('data-workspace-id', workspaceId || '');
      node.setAttribute('data-title-last-saved', title);
      node.setAttribute('data-title-save-status', 'saved');
      node.style.height = 'auto';
      node.style.height = `${Math.max(48, node.scrollHeight)}px`;
    });
    document.querySelectorAll('[data-page-title-current="true"]').forEach((node) => {
      if (node instanceof HTMLElement) node.textContent = title;
    });
    const topbarCurrent = document.querySelector('.wolai-breadcrumb-current [data-page-title-current]');
    if (topbarCurrent instanceof HTMLElement) topbarCurrent.textContent = title;
    root.setAttribute('data-mnote-object-editor', 'mindmap');
    root.setAttribute('data-mnote-object-identity', `resource:mindmap:${documentId}:${mindmapId}`);
    root.setAttribute('data-mnote-mindmap-id', mindmapId);
    setPrimaryMindmapSelection(documentId, mindmapId);
  };

  const replacePrimaryPaneMindmap = async ({ documentId, mindmapId, workspaceId, url }) => {
    const targetUrl = url instanceof URL
      ? url
      : new URL(`/mindmap/${encodeURIComponent(documentId)}/${encodeURIComponent(mindmapId)}`, window.location.origin);
    const root = document.querySelector(`${ROOT_SELECTOR}[data-pane-role="primary"]`);
    const observability = document.querySelector('[data-editor-host-observability][data-pane-role="primary"]');
    if (!(root instanceof HTMLElement)) throw new Error('primary_pane_root_missing');
    const runtime = await loadRuntime();
    const { bootstrap, title } = await fetchMindmapShellBootstrap(targetUrl);
    const previousView = paneViewRegistry.get('primary');
    if (previousView) {
      unmountEditorViewBinding(previousView);
      paneViewRegistry.delete('primary');
    }
    unmountMindmapPane('primary');
    if (observability instanceof HTMLElement) {
      observability.setAttribute('data-editor-host-active', 'mindmap_object');
      observability.setAttribute('data-editor-host-status', 'mounting');
    }
    root.replaceChildren();
    root.setAttribute('data-runtime-editor-status', 'booting');
    updatePrimaryMindmapChrome({ documentId, mindmapId, workspaceId, title, root });
    const mountId = runtime.mount(root, bootstrap);
    root.setAttribute('data-runtime-mount-id', String(mountId));
    root.setAttribute('data-editor-host-kind', 'mindmap_object');
    if (observability instanceof HTMLElement) {
      observability.setAttribute('data-editor-host-status', 'mounted');
    }
    mindmapPaneViewRegistry.set('primary', { paneRole: 'primary', root, runtime, mountId });
    pushUrlState(targetUrl);
    return true;
  };

  const handleSessionChange = (session, view, event) => {
    const payload = normalizeEnvelopePayload(event);
    if (!payload) return;
    const pendingExternalChange = session.sourceKind === 'local_folder' && session.externalChangePending;
    const recentExternalChange = sessionHasRecentExternalSignal(session);
    const recentLocalInput = sessionHasRecentLocalInput(session);
    const tiptapDocument = hydrateMindmapAttrsFromDom(toTiptapDocument(
      payload?.tiptapDocument || payload?.editorDocument || payload?.content,
      currentEditorText(view),
    ), view.runtimeDescriptor.root);
    const serialized = JSON.stringify(tiptapDocument);
    if (view.suppressedSerialized && view.suppressedSerialized === serialized) {
      view.suppressedSerialized = null;
      view.lastKnownSerialized = serialized;
      return;
    }
    const previousSerialized = session.currentSerialized;
    session.currentTiptapDocument = tiptapDocument;
    session.currentSerialized = serialized;
    view.lastKnownSerialized = serialized;
    if (typeof payload?.title === 'string' && payload.title.trim()) {
      session.title = payload.title.trim();
    }
    if (payload?.meta && typeof payload.meta === 'object') {
      if (Number.isInteger(payload.meta.revision)) session.revision = payload.meta.revision;
      if (typeof payload.meta.conflictDetectionKey === 'string' && payload.meta.conflictDetectionKey.trim()) {
        const nextMetaConflictKey = payload.meta.conflictDetectionKey.trim();
        if (conflictDetectionKeyBelongsToSession(session, nextMetaConflictKey)) {
          session.conflictDetectionKey = nextMetaConflictKey;
        }
      }
      if (typeof payload.meta.readOnly === 'boolean') {
        session.readOnly = payload.meta.readOnly;
      }
    }
    session.dirty = session.currentSerialized !== session.lastPersistedSerialized;
    syncResourceSessionTabGuards(session);
    if (serialized !== previousSerialized) {
      broadcastSessionContent(session, view);
    }
    if ((pendingExternalChange || recentExternalChange) && (session.dirty || recentLocalInput)) {
      markSessionExternalConflict(session, externalConflictMessage);
      return;
    }
    if (session.hasExternalConflict) {
      setSessionStatus(session, 'external-change-conflict', externalConflictMessage);
      return;
    }
    if (session.dirty) {
      queueSessionSave(session);
    } else {
      setSessionStatus(session, 'saved');
    }
  };

  const unmountEditorViewBinding = (view, options = {}) => {
    if (!view || view.disposed) return;
    view.disposed = true;
    const scheduleRelease = options.scheduleRelease !== false;
    const root = view.runtimeDescriptor.root;
    if (typeof view.disconnectObserver === 'function') {
      view.disconnectObserver();
      view.disconnectObserver = null;
    }
    if (view.onReady) root.removeEventListener(READY_EVENT, view.onReady);
    if (view.onError) root.removeEventListener(ERROR_EVENT, view.onError);
    if (view.onChange) root.removeEventListener(CHANGE_EVENT, view.onChange);
    if (view.onSave) root.removeEventListener(SAVE_EVENT, view.onSave);
    if (view.onState) root.removeEventListener(STATE_EVENT, view.onState);
    if (view.onKeydown) root.removeEventListener('keydown', view.onKeydown, true);
    if (view.onInput) root.removeEventListener('input', view.onInput);
    if (view.disconnectSlashObserver) {
      view.disconnectSlashObserver();
      view.disconnectSlashObserver = null;
    }
    clearSessionConflictSurface(view.session);
    if (view.mountId != null && typeof view.runtime?.unmount === 'function') {
      try {
        view.runtime.unmount(view.mountId);
      } catch (error) {
        console.warn('mnote editor view unmount failed', error);
      }
    }
    view.mountId = null;
    root.removeAttribute('data-runtime-mount-id');
    if (view.session.views.get(view.id) === view) {
      view.session.views.delete(view.id);
    }
    if (scheduleRelease) {
      scheduleDocumentSessionRelease(view.session);
    }
  };

  const observeEditorViewBinding = (view) => {
    if (typeof MutationObserver !== 'function' || !(document.body instanceof HTMLElement)) {
      return;
    }
    const observer = new MutationObserver(() => {
      if (!view.disposed && !view.runtimeDescriptor.root.isConnected) {
        unmountEditorViewBinding(view);
      }
    });
    observer.observe(document.body, { childList: true, subtree: true });
    view.disconnectObserver = () => observer.disconnect();
  };

  const activeEditorRootForSlashMenu = () => {
    const selection = window.getSelection();
    const anchorNode = selection?.anchorNode || null;
    const anchorElement = anchorNode && anchorNode.nodeType === Node.ELEMENT_NODE
      ? anchorNode
      : anchorNode?.parentElement || null;
    const roots = Array.from(document.querySelectorAll(ROOT_SELECTOR)).filter((node) => (
      node instanceof HTMLElement
        && node.offsetParent !== null
        && node.getAttribute('data-mnote-side-target-unsupported') !== 'true'
    ));
    const intendedRoot = window.__mnoteIntendedSlashRoot instanceof HTMLElement
      && roots.includes(window.__mnoteIntendedSlashRoot)
      ? window.__mnoteIntendedSlashRoot
      : null;
    if (intendedRoot) return intendedRoot;
    const focused = document.activeElement instanceof Element
      ? roots.find((root) => root.contains(document.activeElement))
      : null;
    if (focused) return focused;
    if (anchorElement instanceof Element) {
      const activeRoot = roots.find((root) => root.contains(anchorElement));
      if (activeRoot) return activeRoot;
    }
    return roots.find((root) => root.querySelector('.ProseMirror:focus-within')) || roots[0] || null;
  };

  const markIntendedSlashRoot = (entry) => {
    const root = entry?.view?.runtimeDescriptor?.root;
    if (!(root instanceof HTMLElement) || !root.isConnected) return;
    window.__mnoteIntendedSlashRoot = root;
    hideSlashMenusOutsideRoot(root);
    scheduleSlashMenuPosition(root);
  };

  const slashMenuAnchorFromSelection = (root) => {
    const currentBlockFromSelection = () => {
      try {
        const selection = window.getSelection();
        const anchorNode = selection?.anchorNode || null;
        const editor = root.querySelector('.ProseMirror');
        const anchorElement = anchorNode && anchorNode.nodeType === Node.ELEMENT_NODE
          ? anchorNode
          : anchorNode?.parentElement || null;
        if (!(editor instanceof HTMLElement) || !(anchorElement instanceof Element) || !editor.contains(anchorElement)) return null;
        let current = anchorElement;
        while (current && current.parentElement && current.parentElement !== editor) {
          current = current.parentElement;
        }
        return current instanceof HTMLElement && current.parentElement === editor ? current : null;
      } catch (_) {
        return null;
      }
    };
    const fallback = () => {
      const editor = root.querySelector('.ProseMirror');
      if (editor instanceof HTMLElement) {
        const block = currentBlockFromSelection()
          || Array.from(editor.children).find((node) => node instanceof HTMLElement && node.matches(':focus-within, .ProseMirror-selectednode'))
          || Array.from(editor.children).find((node) => node instanceof HTMLElement && node.getBoundingClientRect().height > 0);
        if (block instanceof HTMLElement) {
          const rect = block.getBoundingClientRect();
          return { top: rect.bottom + 8, left: rect.left, anchorTop: rect.top };
        }
      }
      const rect = root.getBoundingClientRect();
      return { top: rect.top + 24, left: rect.left + 24, anchorTop: rect.top + 24 };
    };
    try {
      const selection = window.getSelection();
      if (!selection || selection.rangeCount <= 0) return fallback();
      const anchorNode = selection.anchorNode;
      if (anchorNode && !root.contains(anchorNode.nodeType === Node.ELEMENT_NODE ? anchorNode : anchorNode.parentElement)) {
        return fallback();
      }
      const rect = selection.getRangeAt(0).getBoundingClientRect();
      if (rect && (rect.top > 0 || rect.left > 0 || rect.height > 0)) {
        return { top: rect.bottom + 8, left: rect.left, anchorTop: rect.top };
      }
    } catch (_) {}
    return fallback();
  };

  const setSlashMenuInactive = (menu, inactive) => {
    if (!(menu instanceof HTMLElement)) return;
    if (inactive) {
      if (menu.getAttribute('data-mnote-slash-inactive') !== 'true') {
        menu.setAttribute('data-mnote-slash-inactive', 'true');
      }
      if (menu.style.display !== 'none') menu.style.display = 'none';
      return;
    }
    if (menu.getAttribute('data-mnote-slash-inactive') === 'true') {
      menu.removeAttribute('data-mnote-slash-inactive');
    }
    if (menu.style.display === 'none') menu.style.display = '';
  };

  const setSlashMenuStyle = (menu, property, value) => {
    if (!(menu instanceof HTMLElement)) return;
    if (menu.style[property] !== value) menu.style[property] = value;
  };

  const setSlashMenuAttribute = (menu, name, value) => {
    if (!(menu instanceof HTMLElement)) return;
    if (menu.getAttribute(name) !== value) menu.setAttribute(name, value);
  };

  const hideSlashMenusOutsideRoot = (activeRoot) => {
    document.querySelectorAll(`${ROOT_SELECTOR} [data-testid="mnote-leptos-tiptap-slash-menu"]`).forEach((menu) => {
      const root = menu.closest(ROOT_SELECTOR);
      if (root !== activeRoot) setSlashMenuInactive(menu, true);
    });
  };

  const positionSlashMenuForRoot = (root) => {
    const activeRoot = activeEditorRootForSlashMenu();
    if (!(root instanceof HTMLElement)) root = activeRoot;
    if (!(root instanceof HTMLElement)) return;
    if (activeRoot instanceof HTMLElement && root !== activeRoot) {
      const inactiveMenu = root.querySelector('[data-testid="mnote-leptos-tiptap-slash-menu"]');
      setSlashMenuInactive(inactiveMenu, true);
      hideSlashMenusOutsideRoot(activeRoot);
      return;
    }
    hideSlashMenusOutsideRoot(root);
    const menu = root.querySelector('[data-testid="mnote-leptos-tiptap-slash-menu"]');
    if (!(menu instanceof HTMLElement)) return;
    setSlashMenuInactive(menu, false);
    const anchor = slashMenuAnchorFromSelection(root);
    const gap = 8;
    const menuWidth = Math.min(316, Math.max(160, window.innerWidth - 16));
    const menuHeight = Math.min(430, Math.max(120, window.innerHeight - 16));
    const left = Math.min(Math.max(gap, anchor.left), Math.max(gap, window.innerWidth - menuWidth - gap));
    const belowTop = Math.min(Math.max(gap, anchor.top), Math.max(gap, window.innerHeight - 120));
    let top = belowTop + menuHeight > window.innerHeight - gap
      ? Math.min(Math.max(gap, anchor.anchorTop - menuHeight - gap), Math.max(gap, window.innerHeight - 120))
      : belowTop;
    const mindmap = document.querySelector('[data-testid="mnote-mindmap-editor-root"]');
    if (mindmap instanceof HTMLElement) {
      const mindmapRect = mindmap.getBoundingClientRect();
      const overlapsMindmap = top < mindmapRect.bottom && top + menuHeight > mindmapRect.top;
      if (overlapsMindmap && mindmapRect.bottom > window.innerHeight - 48) {
        top = Math.max(top, Math.max(gap, window.innerHeight - menuHeight - gap));
      } else if (overlapsMindmap && mindmapRect.bottom + menuHeight + gap <= window.innerHeight) {
        top = mindmapRect.bottom + gap;
      }
    }
    setSlashMenuStyle(menu, 'position', 'fixed');
    setSlashMenuStyle(menu, 'zIndex', '130');
    setSlashMenuStyle(menu, 'left', `${Math.round(left)}px`);
    setSlashMenuStyle(menu, 'top', `${Math.round(top)}px`);
    setSlashMenuStyle(menu, 'width', `min(316px, calc(100vw - 16px))`);
    setSlashMenuStyle(menu, 'maxHeight', `min(430px, calc(100vh - 16px))`);
    setSlashMenuAttribute(menu, 'data-mnote-slash-positioned', 'host');
  };

  let pendingSlashMenuPositionFrame = 0;
  let pendingSlashMenuPositionRoot = null;
  const scheduleSlashMenuPosition = (root) => {
    if (root instanceof HTMLElement) pendingSlashMenuPositionRoot = root;
    if (pendingSlashMenuPositionFrame) return;
    pendingSlashMenuPositionFrame = window.requestAnimationFrame(() => {
      const targetRoot = pendingSlashMenuPositionRoot;
      pendingSlashMenuPositionFrame = 0;
      pendingSlashMenuPositionRoot = null;
      positionSlashMenuForRoot(targetRoot);
    });
  };

  const shouldReactToSlashMenuMutation = (records, root) => {
    if (!Array.isArray(records) || !(root instanceof HTMLElement)) return false;
    return records.some((record) => {
      const target = record && record.target instanceof HTMLElement ? record.target : null;
      if (!target || !root.contains(target) && target !== root) return false;
      if (record.type === 'attributes') {
        return record.attributeName !== 'style' && record.attributeName !== 'data-mnote-slash-positioned' && record.attributeName !== 'data-mnote-slash-inactive';
      }
      if (record.type === 'childList') {
        return Array.from(record.addedNodes || []).some((node) => node instanceof HTMLElement && node.closest('[data-testid="mnote-leptos-tiptap-slash-menu"]'))
          || Array.from(record.removedNodes || []).some((node) => node instanceof HTMLElement && node.closest('[data-testid="mnote-leptos-tiptap-slash-menu"]'));
      }
      return true;
    });
  };

  const scheduleGlobalSlashMenuPosition = () => {
    scheduleSlashMenuPosition(activeEditorRootForSlashMenu());
  };

  const installGlobalSlashMenuPositioning = () => {
    if (window.__MNOTE_SLASH_MENU_POSITIONING_INSTALLED__) return;
    window.__MNOTE_SLASH_MENU_POSITIONING_INSTALLED__ = true;
    if (typeof MutationObserver === 'function' && document.body instanceof HTMLElement) {
      const observer = new MutationObserver(() => scheduleGlobalSlashMenuPosition());
      observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class', 'hidden'] });
    }
    document.addEventListener('keydown', (event) => {
      if (event.key === '/') scheduleGlobalSlashMenuPosition();
    }, true);
    document.addEventListener('selectionchange', () => scheduleGlobalSlashMenuPosition(), true);
    window.addEventListener('resize', scheduleGlobalSlashMenuPosition);
    window.addEventListener('scroll', scheduleGlobalSlashMenuPosition, true);
  };

  installGlobalSlashMenuPositioning();

  const observeSlashMenuPosition = (view) => {
    const root = view.runtimeDescriptor.root;
    if (!(root instanceof HTMLElement) || typeof MutationObserver !== 'function') return;
    const observer = new MutationObserver((records) => {
      if (!view.disposed && shouldReactToSlashMenuMutation(records, root)) {
        scheduleSlashMenuPosition(root);
      }
    });
    observer.observe(root, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'hidden', 'class', 'data-mnote-slash-positioned', 'data-mnote-slash-inactive'] });
    view.disconnectSlashObserver = () => observer.disconnect();
  };

  const createEditorViewBinding = (session, runtime, runtimeDescriptor) => {
    cancelDocumentSessionRelease(session);
    const view = {
      id: nextViewId++,
      mountId: null,
      ready: false,
      disposed: false,
      suppressedSerialized: null,
      lastKnownSerialized: session.currentSerialized,
      runtime,
      runtimeDescriptor,
      session,
      disconnectObserver: null,
      onReady: null,
      onError: null,
      onChange: null,
      onSave: null,
      onState: null,
      onKeydown: null,
      onInput: null,
      disconnectSlashObserver: null,
    };

    view.onReady = () => {
      view.ready = true;
      if (view.lastKnownSerialized !== session.currentSerialized) {
        dispatchSessionContentToView(session, view, 'mnote-web-document-session-ready-sync');
      }
      if (session.status === 'dirty' || session.status === 'saving' || session.status === 'saved' || session.status === 'error' || session.status === 'external-change-conflict' || session.status === 'synced-external-change') {
        setStatus(runtimeDescriptor, session.status, session.error);
      } else {
        setStatus(runtimeDescriptor, 'ready');
      }
    };
    view.onError = (event) => {
      const payload = normalizeEnvelopePayload(event);
      setStatus(runtimeDescriptor, 'error', payload?.message || 'runtime_error');
    };
    view.onChange = (event) => {
      scheduleSlashMenuPosition(runtimeDescriptor.root);
      handleSessionChange(session, view, event);
    };
    view.onSave = (event) => {
      handleSessionChange(session, view, event);
    };
    view.onState = () => {
      scheduleSlashMenuPosition(runtimeDescriptor.root);
    };
    view.onKeydown = (event) => {
      if (event.key === '/') scheduleSlashMenuPosition(runtimeDescriptor.root);
    };
    view.onInput = (event) => {
      const target = event.target;
      if (!(target instanceof Element) || !target.closest('.ProseMirror')) return;
      session.lastUserInputAt = Date.now();
      scheduleSlashMenuPosition(runtimeDescriptor.root);
    };

    runtimeDescriptor.root.addEventListener(READY_EVENT, view.onReady);
    runtimeDescriptor.root.addEventListener(ERROR_EVENT, view.onError);
    runtimeDescriptor.root.addEventListener(CHANGE_EVENT, view.onChange);
    runtimeDescriptor.root.addEventListener(SAVE_EVENT, view.onSave);
    runtimeDescriptor.root.addEventListener(STATE_EVENT, view.onState);
    runtimeDescriptor.root.addEventListener('keydown', view.onKeydown, true);
    runtimeDescriptor.root.addEventListener('input', view.onInput);
    session.views.set(view.id, view);
    observeSlashMenuPosition(view);
    observeEditorViewBinding(view);
    return view;
  };

  const normalizePaneRole = (paneRole) => String(paneRole || '').trim() === 'secondary' ? 'secondary' : 'primary';

  const resourceTabRegistryKey = (paneRole, objectIdentity) => `${normalizePaneRole(paneRole)}::${String(objectIdentity || '').trim()}`;

  const resourceTabHostNodes = (paneRole = 'primary') => {
    const role = normalizePaneRole(paneRole);
    return {
      paneRole: role,
      strip: document.querySelector(`[data-mnote-main-tab-strip][data-pane-role="${role}"]`),
      pageTab: document.querySelector(`[data-mnote-main-tab="page"][data-pane-role="${role}"]`),
      pagePanel: document.querySelector(`[data-mnote-page-tab-panel][data-pane-role="${role}"]`),
      host: document.querySelector(`[data-mnote-resource-tab-host][data-pane-role="${role}"]`),
      panelRoot: document.querySelector(`[data-mnote-resource-tab-panel-root][data-pane-role="${role}"]`),
    };
  };

  const resourceTabBadgeKind = (input, kind) => {
    const title = String(input?.title || input?.fileName || input?.path || '').trim().toLowerCase();
    if (kind === 'mindmap') return 'mindmap';
    if (kind === 'office') {
      if (/\.(ppt|pptx|odp)$/i.test(title)) return 'ppt';
      if (/\.(xls|xlsx|ods|csv)$/i.test(title)) return 'sheet';
      return 'word';
    }
    if (kind === 'pdf') return 'pdf';
    if (kind === 'markdown' || kind === 'text' || kind === 'code') return 'code';
    if (kind === 'image') return 'image';
    return 'file';
  };

  const currentWebShellWorkspaceId = () => {
    try {
      return currentUrl().searchParams.get('workspaceId') || '';
    } catch (_) {
      return '';
    }
  };

  const currentWebShellDocumentId = () => {
    const fromBody = document.body?.dataset?.documentId || '';
    if (fromBody) return String(fromBody).trim();
    const pageTab = document.querySelector('[data-mnote-main-tab="page"]');
    if (pageTab instanceof HTMLElement) return String(pageTab.getAttribute('data-document-id') || '').trim();
    return '';
  };

  const normalizeResourceTabKind = (input) => {
    const kind = String(input?.kind || '').trim().toLowerCase();
    const title = String(input?.title || input?.fileName || input?.path || '').trim().toLowerCase();
    if (kind === 'mindmap' || kind === 'office' || kind === 'pdf' || kind === 'image' || kind === 'markdown' || kind === 'text' || kind === 'code') return kind;
    if (/\.(doc|docx|ppt|pptx|xls|xlsx|odt|odp|ods)$/i.test(title)) return 'office';
    if (/\.pdf$/i.test(title)) return 'pdf';
    if (/\.(png|jpg|jpeg|gif|webp|svg)$/i.test(title)) return 'image';
    if (/\.(md|markdown)$/i.test(title)) return 'markdown';
    if (/\.(txt|log)$/i.test(title)) return 'text';
    if (/\.(rs|ts|tsx|js|jsx|json|css|scss|html|xml|py|go|java|kt|swift|c|h|cpp|hpp|sh|bash|zsh|toml|yaml|yml|sql)$/i.test(title)) return 'code';
    return 'file';
  };

  const resolveResourceOpen = (input = {}) => {
    const kind = normalizeResourceTabKind(input);
    const badgeKind = resourceTabBadgeKind(input, kind);
    const rawTarget = String(input?.openTarget || '').trim().toLowerCase();
    const openTarget = rawTarget === 'new-window' || rawTarget === 'side' ? rawTarget : 'active-tab';
    const editable = kind === 'markdown' || kind === 'text' || kind === 'code';
    const defaultOpenMode = kind === 'office' && openTarget === 'active-tab' ? 'active-tab-iframe' : openTarget;
    const viewerUrl = kind === 'office'
      ? String(input?.officeUrl || input?.href || '').trim()
      : String(input?.href || '').trim();
    return { editorKind: kind, badgeKind, defaultOpenMode, editable, viewerUrl, openTarget };
  };

  const touchResourceTabMru = (paneRole, key) => {
    const role = normalizePaneRole(paneRole);
    const id = String(key || '').trim();
    if (!id) return;
    const list = resourceTabMru[role] || (resourceTabMru[role] = []);
    const index = list.indexOf(id);
    if (index >= 0) list.splice(index, 1);
    list.unshift(id);
    if (list.length > resourceTabMruMax) list.length = resourceTabMruMax;
  };

  const setResourceTabLastActive = (key) => {
    const entry = resourceTabRegistry.get(String(key || '').trim());
    if (entry?.session) entry.session.lastActiveAt = Date.now();
  };

  const removeFromResourceTabMru = (paneRole, key) => {
    const list = resourceTabMru[normalizePaneRole(paneRole)] || [];
    const index = list.indexOf(key);
    if (index >= 0) list.splice(index, 1);
  };

  const lastActiveResourceTabKey = (paneRole = 'primary') => {
    const list = resourceTabMru[normalizePaneRole(paneRole)] || [];
    for (const key of list) {
      if (resourceTabRegistry.has(key)) return key;
    }
    return '';
  };

  const resourceTabCloseGuardReason = (session) => {
    if (!session) return '';
    const hasUnsavedChanges = session.dirty
      || Boolean(session.saveTimer)
      || sessionHasRecentLocalInput(session)
      || (session.currentSerialized && session.currentSerialized !== session.lastPersistedSerialized);
    if (hasUnsavedChanges) return 'dirty';
    if (session.saving) return 'saving';
    if (session.hasExternalConflict) return 'hasExternalConflict';
    return '';
  };

  const syncResourceTabCloseGuard = (entry) => {
    if (!entry?.tab || !(entry.tab instanceof HTMLElement)) return;
    const reason = resourceTabCloseGuardReason(entry.session);
    if (reason) {
      entry.tab.setAttribute(resourceTabCloseGuardAttribute, reason);
      entry.tab.classList.add('is-close-guarded');
    } else {
      entry.tab.removeAttribute(resourceTabCloseGuardAttribute);
      entry.tab.classList.remove('is-close-guarded');
    }
  };

  const syncResourceSessionTabGuards = (session) => {
    if (!session || session.sessionKind !== 'resource') return;
    resourceTabRegistry.forEach((entry) => {
      if (entry.session === session) syncResourceTabCloseGuard(entry);
    });
  };

  const openEditorsSnapshotEntry = (entry, key) => ({
    objectIdentity: String(entry?.objectIdentity || key || '').trim(),
    title: String(entry?.title || '资源').trim() || '资源',
    kind: normalizeResourceTabKind(entry),
    badgeKind: entry?.tab instanceof HTMLElement
      ? String(entry.tab.getAttribute('data-mnote-tab-badge-kind') || resourceTabBadgeKind(entry, entry.kind)).trim()
      : resourceTabBadgeKind(entry, entry?.kind),
    active: entry?.tab instanceof HTMLElement
      ? entry.tab.getAttribute('aria-selected') === 'true'
      : false,
    dirtyGuard: resourceTabCloseGuardReason(entry?.session),
    assetId: String(entry?.assetId || entry?.session?.assetId || '').trim(),
    path: String(entry?.path || entry?.session?.resourcePath || '').trim(),
  });

  const buildOpenEditorsSnapshot = () => {
    const pageEntries = ['primary', 'secondary'].map((paneRole) => {
      const nodes = resourceTabHostNodes(paneRole);
      const pageTitle = nodes.pageTab instanceof HTMLElement
        ? String(nodes.pageTab.querySelector('.mnote-main-tab-title')?.textContent || '页面').trim()
        : '页面';
      return {
        objectIdentity: `page:${paneRole}`,
        paneRole,
        documentId: String(nodes.pageTab?.getAttribute?.('data-document-id') || '').trim(),
        workspaceId: String(nodes.pageTab?.getAttribute?.('data-workspace-id') || currentWebShellWorkspaceId() || '').trim(),
        title: pageTitle || '页面',
        kind: 'page',
        badgeKind: 'code',
        active: nodes.pageTab instanceof HTMLElement
          ? nodes.pageTab.getAttribute('aria-selected') === 'true'
          : false,
        dirtyGuard: '',
      };
    }).filter((entry) => entry.paneRole === 'primary' || entry.documentId || document.querySelector('[data-document-pane="true"][data-pane-role="secondary"][data-pane-visible="true"]'));
    const resources = [];
    resourceTabRegistry.forEach((entry, key) => {
      resources.push(openEditorsSnapshotEntry(entry, key));
    });
    const active = [...pageEntries, ...resources].find((entry) => entry.active);
    return {
      schema: 'mnote.open_editors_snapshot.v1',
      generatedAt: Date.now(),
      activeObjectIdentity: active?.objectIdentity || '',
      editors: [...pageEntries, ...resources],
      resourceEditors: resources,
    };
  };

  const syncOpenEditorsSnapshot = () => {
    const snapshot = buildOpenEditorsSnapshot();
    window.__mnoteOpenEditorsSnapshot = snapshot;
    document.documentElement.setAttribute('data-mnote-open-editors-count', String(snapshot.editors.length));
    document.documentElement.setAttribute('data-mnote-active-editor', snapshot.activeObjectIdentity || '');
    window.dispatchEvent(new CustomEvent('mnote:open-editors-snapshot', { detail: snapshot }));
    return snapshot;
  };

  const showResourceTabCloseGuardNotice = (entry, reason) => {
    const nodes = resourceTabHostNodes(entry?.paneRole || 'primary');
    const messages = {
      dirty: '当前资源有未保存的修改，保存完成后再关闭。',
      saving: '当前资源正在保存中，请稍后再关闭。',
      hasExternalConflict: '当前资源存在外部冲突，请先处理冲突。',
    };
    const message = messages[reason] || '当前资源暂时无法关闭。';
    let notice = document.getElementById('mnote-resource-close-guard-notice');
    if (!notice) {
      notice = document.createElement('div');
      notice.id = 'mnote-resource-close-guard-notice';
      notice.className = 'mnote-close-guard-notice';
      notice.setAttribute('role', 'status');
      notice.setAttribute('aria-live', 'polite');
      notice.setAttribute('data-mnote-resource-close-guard', '');
      const parent = nodes.strip?.parentNode;
      if (parent instanceof HTMLElement) {
        const panels = parent.querySelector('.mnote-main-tab-panels');
        if (panels && panels.parentNode === parent) parent.insertBefore(notice, panels);
        else parent.append(notice);
      }
    }
    notice.textContent = `${entry?.title || '资源'}：${message}`;
    notice.setAttribute('data-mnote-resource-close-guard', reason || 'blocked');
    notice.className = `mnote-close-guard-notice is-${reason || 'blocked'}`;
    if (notice._mnoteHideTimer) window.clearTimeout(notice._mnoteHideTimer);
    notice._mnoteHideTimer = window.setTimeout(() => {
      notice.classList.add('is-hiding');
      window.setTimeout(() => {
        if (notice.parentNode) notice.remove();
      }, 260);
    }, 4000);
  };

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

  const syncActiveResourceFileTreeRow = (activeResource) => {
    document.querySelectorAll('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-active="true"]').forEach((row) => {
      if (row instanceof HTMLElement) row.setAttribute('data-active', 'false');
    });
    const entry = activeResource ? resourceTabRegistry.get(activeResource) : null;
    if (!entry) return;
    const assetId = String(entry.assetId || entry.session?.assetId || '').trim();
    const path = String(entry.path || entry.session?.resourcePath || '').trim();
    const identity = String(entry.objectIdentity || activeResource || '').trim();
    if (assetId) {
      const row = document.querySelector(`#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-asset-id="${cssSafe(assetId)}"]`);
      if (row instanceof HTMLElement) {
        row.setAttribute('data-active', 'true');
        return;
      }
    }
    const rows = document.querySelectorAll('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-object-identity]');
    for (const row of rows) {
      if (!(row instanceof HTMLElement)) continue;
      const objectIdentity = row.getAttribute('data-object-identity') || '';
      if ((identity && objectIdentity.includes(identity)) || (path && objectIdentity.includes(path))) {
        row.setAttribute('data-active', 'true');
        return;
      }
    }
  };

  const syncActiveResourceUrlState = (activeResource, paneRole = 'primary') => {
    const role = normalizePaneRole(paneRole);
    if (role !== 'primary') return;
    const url = currentUrl();
    if (activeResource) {
      const entry = resourceTabRegistry.get(activeResource);
      const documentId = String(entry?.ownerDocumentId || entry?.documentId || '').trim();
      if (documentId) url.pathname = `/documents/${encodeURIComponent(documentId)}`;
      url.searchParams.set('resourceTab', activeResource);
    } else {
      const documentId = currentWebShellDocumentId();
      if (documentId) url.pathname = `/documents/${encodeURIComponent(documentId)}`;
      url.searchParams.delete('resourceTab');
    }
    replaceUrlState(url);
  };

  const activateMainEditorTab = (objectIdentity, paneRole = 'primary') => {
    const activeResource = String(objectIdentity || '').trim();
    const activeEntry = activeResource ? resourceTabRegistry.get(activeResource) : null;
    const role = normalizePaneRole(activeEntry?.paneRole || paneRole);
    const nodes = resourceTabHostNodes(role);
    if (activeResource) {
      touchResourceTabMru(role, activeResource);
      setResourceTabLastActive(activeResource);
    }
    if (nodes.pageTab instanceof HTMLElement) {
      const activePage = !activeResource;
      nodes.pageTab.classList.toggle('is-active', activePage);
      nodes.pageTab.setAttribute('aria-selected', activePage ? 'true' : 'false');
      nodes.pageTab.setAttribute('tabindex', activePage ? '0' : '-1');
    }
    if (nodes.pagePanel instanceof HTMLElement) nodes.pagePanel.hidden = Boolean(activeResource);
    if (nodes.host instanceof HTMLElement) nodes.host.hidden = !activeResource;
    resourceTabRegistry.forEach((entry, key) => {
      if (normalizePaneRole(entry.paneRole) !== role) return;
      const active = key === activeResource;
      if (entry.tab instanceof HTMLElement) {
        entry.tab.classList.toggle('is-active', active);
        entry.tab.setAttribute('aria-selected', active ? 'true' : 'false');
        entry.tab.setAttribute('tabindex', active ? '0' : '-1');
      }
      if (entry.panel instanceof HTMLElement) entry.panel.hidden = !active;
      syncResourceTabCloseGuard(entry);
    });
    if (activeEntry) markIntendedSlashRoot(activeEntry);
    if (!activeEntry && window.__mnoteIntendedSlashRoot instanceof HTMLElement) window.__mnoteIntendedSlashRoot = null;
    if (role === 'primary') syncActiveResourceFileTreeRow(activeResource);
    syncActiveResourceUrlState(activeResource, role);
    syncOpenEditorsSnapshot();
  };

  const bindMainEditorTabStrip = (paneRole = 'primary') => {
    const nodes = resourceTabHostNodes(paneRole);
    if (!(nodes.strip instanceof HTMLElement)) return;
    if (nodes.strip.getAttribute('data-mnote-tab-strip-bound') === 'true') return;
    nodes.strip.setAttribute('data-mnote-tab-strip-bound', 'true');
    const collectTabs = () => Array.from(nodes.strip.querySelectorAll('[data-mnote-main-tab]'))
      .filter((tab) => tab instanceof HTMLElement && tab.isConnected);
    const selectedIndex = (tabs) => tabs.findIndex((tab) => tab.getAttribute('aria-selected') === 'true');
    nodes.strip.addEventListener('keydown', (event) => {
      const tabs = collectTabs();
      if (!tabs.length) return;
      const focusedIndex = tabs.findIndex((tab) => tab === document.activeElement);
      const baseIndex = focusedIndex >= 0 ? focusedIndex : Math.max(0, selectedIndex(tabs));
      let targetIndex = -1;
      if (event.key === 'ArrowRight') {
        targetIndex = (baseIndex + 1) % tabs.length;
      } else if (event.key === 'ArrowLeft') {
        targetIndex = (baseIndex - 1 + tabs.length) % tabs.length;
      } else if (event.key === 'Home') {
        targetIndex = 0;
      } else if (event.key === 'End') {
        targetIndex = tabs.length - 1;
      } else if (event.key === 'Enter' || event.key === ' ') {
        event.preventDefault();
        const tab = tabs[baseIndex];
        if (tab instanceof HTMLElement) tab.click();
        return;
      } else {
        return;
      }
      event.preventDefault();
      const target = tabs[targetIndex];
      if (target instanceof HTMLElement) target.focus();
    });
  };

  const bindMainEditorPageTab = (paneRole = 'primary') => {
    const role = normalizePaneRole(paneRole);
    const nodes = resourceTabHostNodes(role);
    if (!(nodes.pageTab instanceof HTMLElement)) return;
    bindMainEditorTabStrip(role);
    if (nodes.pageTab.getAttribute('data-mnote-page-tab-bound') === 'true') return;
    nodes.pageTab.setAttribute('data-mnote-page-tab-bound', 'true');
    nodes.pageTab.addEventListener('click', (event) => {
      const target = event.target;
      if (target instanceof HTMLElement && target.closest('[data-mnote-pane-close="secondary"]')) return;
      event.preventDefault();
      activateMainEditorTab('', role);
    });
    syncOpenEditorsSnapshot();
  };

  const closeResourceTab = (objectIdentity) => {
    const key = String(objectIdentity || '').trim();
    const entry = resourceTabRegistry.get(key);
    if (!entry) return;
    const guardReason = resourceTabCloseGuardReason(entry.session);
    if (guardReason) {
      console.warn(`mnote resource tab 关闭阻止: ${entry.title} (${guardReason})`);
      syncResourceTabCloseGuard(entry);
      showResourceTabCloseGuardNotice(entry, guardReason);
      return;
    }
    removeFromResourceTabMru(entry.paneRole || 'primary', key);
    if (entry.view) unmountEditorViewBinding(entry.view, { releaseSession: true });
    if (entry.mindmapRuntime?.runtime && entry.mindmapRuntime?.mountId != null) {
      try {
        entry.mindmapRuntime.runtime.unmount(entry.mindmapRuntime.mountId);
      } catch (error) {
        console.warn('mnote mindmap resource tab unmount failed', error);
      }
    }
    if (entry.tab instanceof HTMLElement) entry.tab.remove();
    if (entry.panel instanceof HTMLElement) entry.panel.remove();
    resourceTabRegistry.delete(key);
    const role = normalizePaneRole(entry.paneRole);
    const nextKey = lastActiveResourceTabKey(role);
    activateMainEditorTab(nextKey, role);
    const nextTab = nextKey ? resourceTabRegistry.get(nextKey)?.tab : resourceTabHostNodes(role).pageTab;
    if (nextTab instanceof HTMLElement) nextTab.focus();
  };

  const markResourceTabError = (entry) => {
    if (!entry) return;
    entry.kind = 'error';
    if (entry.tab instanceof HTMLElement) {
      entry.tab.setAttribute('data-mnote-tab-kind', 'error');
      entry.tab.setAttribute('data-mnote-tab-badge-kind', 'file');
      entry.tab.classList.add('is-error');
    }
    if (entry.panel instanceof HTMLElement) {
      entry.panel.setAttribute('data-resource-kind', 'error');
      entry.panel.innerHTML = '<div class="mnote-resource-tab-error" data-resource-tab-error="true"><div class="mnote-resource-tab-error-inner"><h1>资源打开失败</h1><p>无法加载此资源，请检查文件路径和访问权限。</p></div></div>';
    }
  };

  const createResourceTabDom = (input) => {
    const paneRole = normalizePaneRole(input.paneRole);
    const nodes = resourceTabHostNodes(paneRole);
    if (!(nodes.strip instanceof HTMLElement) || !(nodes.panelRoot instanceof HTMLElement)) return null;
    const objectIdentity = String(input.objectIdentity || '').trim();
    const registryKey = resourceTabRegistryKey(paneRole, objectIdentity);
    const title = String(input.title || input.fileName || input.path || '资源').trim() || '资源';
    const kind = normalizeResourceTabKind(input);
    const tab = document.createElement('button');
    tab.type = 'button';
    tab.className = 'mnote-main-tab';
    tab.setAttribute('role', 'tab');
    tab.setAttribute('data-mnote-main-tab', registryKey);
    tab.setAttribute('data-mnote-object-identity', objectIdentity);
    tab.setAttribute('data-pane-role', paneRole);
    tab.setAttribute('data-mnote-tab-kind', kind);
    tab.setAttribute('data-mnote-tab-badge-kind', resourceTabBadgeKind(input, kind));
    tab.setAttribute('tabindex', '-1');
    tab.innerHTML = '<span class="mnote-main-tab-badge" aria-hidden="true"></span><span class="mnote-main-tab-title"></span><span class="mnote-main-tab-close" role="button" aria-label="关闭标签页">×</span>';
    const titleNode = tab.querySelector('.mnote-main-tab-title');
    if (titleNode) titleNode.textContent = title;
    tab.addEventListener('click', (event) => {
      const target = event.target;
      if (target instanceof HTMLElement && target.closest('.mnote-main-tab-close')) {
        event.preventDefault();
        event.stopPropagation();
        closeResourceTab(registryKey);
        return;
      }
      activateMainEditorTab(registryKey, paneRole);
    });
    const panel = document.createElement('section');
    panel.className = 'mnote-resource-tab-panel';
    panel.setAttribute('data-mnote-resource-tab-panel', registryKey);
    panel.setAttribute('data-mnote-object-identity', objectIdentity);
    panel.setAttribute('data-pane-role', paneRole);
    panel.setAttribute('data-resource-kind', kind);
    panel.hidden = true;
    nodes.strip.append(tab);
    nodes.panelRoot.append(panel);
    return {
      objectIdentity,
      registryKey,
      paneRole,
      title,
      kind,
      tab,
      panel,
      view: null,
      session: null,
      assetId: String(input.assetId || '').trim(),
      path: String(input.path || '').trim(),
      documentId: String(input.documentId || '').trim(),
      ownerDocumentId: String(input.ownerDocumentId || input.documentId || '').trim(),
    };
  };

  const releaseResourceTabEntryRuntime = (entry) => {
    if (!entry) return;
    if (entry.view) {
      unmountEditorViewBinding(entry.view, { releaseSession: true });
      entry.view = null;
      entry.session = null;
    }
    if (entry.mindmapRuntime?.runtime && entry.mindmapRuntime?.mountId != null) {
      try {
        entry.mindmapRuntime.runtime.unmount(entry.mindmapRuntime.mountId);
      } catch (error) {
        console.warn('mnote resource tab runtime unmount failed', error);
      }
      entry.mindmapRuntime = null;
    }
    if (entry.panel instanceof HTMLElement) entry.panel.replaceChildren();
  };

  const localResourceReadUrl = (rootUri, path) => {
    const url = new URL('/api/local-folder/resource/read', window.location.origin);
    url.searchParams.set('rootUri', rootUri || '');
    url.searchParams.set('path', path || '');
    return url.toString();
  };

  const createResourceSession = (entry, input, readResult) => {
    const resourcePath = String(input.path || '');
    const tiptapDocument = localizeTiptapAssetUrls(
      toTiptapDocument(readResult?.content, readResult?.text || ''),
      {
        sourceKind: 'local_folder',
        rootUri: String(input.rootUri || ''),
        documentId: localMarkdownDocumentIdFromRelativePath(resourcePath),
      }
    );
    const conflictDetectionKey = String(readResult?.fileVersion || readResult?.conflictDetectionKey || '').trim();
    const session = {
      key: `resource:${input.rootUri || ''}:${input.path || entry.objectIdentity}`,
      sessionKind: 'resource',
      documentId: entry.objectIdentity,
      ownerDocumentId: String(input.documentId || currentWebShellDocumentId() || '').trim(),
      workspaceId: String(input.workspaceId || currentWebShellWorkspaceId() || ''),
      sourceKind: 'local_folder',
      rootUri: String(input.rootUri || ''),
      resourcePath,
      saveEndpoint: '/api/local-folder/resource/write',
      pageAggregateScriptId: '',
      latestAggregate: null,
      title: entry.title,
      currentTiptapDocument: tiptapDocument,
      currentSerialized: JSON.stringify(tiptapDocument),
      lastPersistedSerialized: JSON.stringify(tiptapDocument),
      revision: null,
      conflictDetectionKey,
      fileVersion: conflictDetectionKey,
      lastExternalConflictDetectionKey: conflictDetectionKey,
      readOnly: false,
      lastActiveAt: 0,
      dirty: false,
      saving: false,
      hasExternalConflict: false,
      externalChangePending: false,
      externalRefreshSource: '',
      lastExternalChangeSignalAt: 0,
      lastSelfSaveSignalAt: 0,
      lastExternalWriteSource: '',
      lastExternalWriteRunId: '',
      lastUserInputAt: 0,
      saveTimer: 0,
      externalRefreshTimer: 0,
      releaseTimer: 0,
      views: new Map(),
      localFolderChannel: null,
      status: 'ready',
      error: null,
    };
    documentSessionRegistry.set(session.key, session);
    return session;
  };

  const openTiptapResourceTab = async (entry, input) => {
    const runtime = await loadRuntime();
    const paneRole = normalizePaneRole(entry.paneRole);
    entry.panel.innerHTML = `<main class="document-shell mnote-resource-tab-text-shell" data-editor-host="leptos_tiptap_resource" data-mnote-editor-kind="resource" data-pane-role="${paneRole}"><div class="mnote-resource-tab-editor-root" data-testid="mnote-leptos-tiptap-island-editor-root" data-editor-host-kind="leptos_tiptap_resource" data-mnote-editor-kind="resource" data-runtime-editor-status="booting" data-pane-role="${paneRole}"></div><div class="sr-only" data-editor-host-observability="rust-web-resource-tab" data-mnote-editor-kind="resource" data-pane-role="${paneRole}"></div></main>`;
    const root = entry.panel.querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"]');
    const observability = entry.panel.querySelector('[data-editor-host-observability]');
    if (!(root instanceof HTMLElement)) throw new Error('resource_tab_root_missing');
    const response = await fetch(localResourceReadUrl(input.rootUri, input.path), { cache: 'no-store', headers: { accept: 'application/json' } });
    const payload = await response.json().catch(() => null);
    if (!response.ok || !payload || payload.ok !== true) throw new Error(payload?.error?.message || `resource_read_failed_${response.status}`);
    const readResult = payload.result || {};
    const session = createResourceSession(entry, input, readResult);
    const runtimeDescriptor = {
      paneRole,
      root,
      observability,
      aggregate: { layout: { pageOptions: {} } },
      bootstrap: {
        documentId: String(input.documentId || currentWebShellDocumentId() || '').trim(),
        workspaceId: String(input.workspaceId || currentWebShellWorkspaceId() || ''),
        sourceKind: 'local_folder',
        rootUri: String(input.rootUri || ''),
        saveEndpoint: '/api/local-folder/resource/write',
      },
    };
    const view = createEditorViewBinding(session, runtime, runtimeDescriptor);
    const mountId = runtime.mount(root, {
      documentId: session.documentId,
      workspaceId: session.workspaceId,
      title: session.title,
      content: session.currentTiptapDocument,
      revision: session.revision,
      conflictDetectionKey: session.conflictDetectionKey,
      readOnly: false,
      editable: true,
      pageOptions: {},
    });
    view.mountId = mountId;
    root.setAttribute('data-runtime-mount-id', String(mountId));
    root.setAttribute('data-editor-host-kind', 'leptos_tiptap_resource');
    root.setAttribute('data-document-id', session.ownerDocumentId || '');
    root.setAttribute('data-workspace-id', session.workspaceId || '');
    if (entry.panel instanceof HTMLElement) {
      const shell = entry.panel.querySelector('.document-shell');
      if (shell instanceof HTMLElement) {
        shell.setAttribute('data-document-id', session.ownerDocumentId || '');
        shell.setAttribute('data-workspace-id', session.workspaceId || '');
      }
    }
    setStatus(runtimeDescriptor, 'mounting-editor');
    entry.view = view;
    entry.session = session;
    markIntendedSlashRoot(entry);
  };

  const openPassiveResourceTab = (entry, input) => {
    const href = String(input.officeUrl || input.href || '').trim();
    if (entry.kind === 'image') {
      entry.panel.innerHTML = '<img class="mnote-resource-tab-image" alt="">';
      const img = entry.panel.querySelector('img');
      if (img instanceof HTMLImageElement) {
        img.src = href;
        img.alt = entry.title;
      }
      return;
    }
    entry.panel.innerHTML = '<iframe class="mnote-resource-tab-frame" title=""></iframe>';
    const frame = entry.panel.querySelector('iframe');
    if (frame instanceof HTMLIFrameElement) {
      frame.title = entry.title;
      frame.src = href;
    }
  };

  const refreshExistingOfficeResourceTab = (entry, input) => {
    if (!entry || entry.kind !== 'office') return false;
    const nextHref = String(input.officeUrl || input.href || '').trim();
    if (!nextHref) return false;
    const frame = entry.panel?.querySelector?.('iframe.mnote-resource-tab-frame');
    const currentHref = frame instanceof HTMLIFrameElement
      ? String(frame.getAttribute('src') || frame.src || '').trim()
      : '';
    if (currentHref !== nextHref) openPassiveResourceTab(entry, input);
    return true;
  };

  const openMindmapResourceTab = async (entry, input) => {
    const documentId = String(input.documentId || currentWebShellDocumentId() || '').trim();
    const mindmapId = String(input.mindmapId || input.assetId || '').trim();
    if (!documentId || !mindmapId) throw new Error('mindmap_resource_identity_missing');
    const targetUrl = new URL(`/mindmap/${encodeURIComponent(documentId)}/${encodeURIComponent(mindmapId)}`, window.location.origin);
    const runtime = await loadRuntime();
    const { bootstrap, title } = await fetchMindmapShellBootstrap(targetUrl);
    entry.title = String(input.title || title || '思维导图').trim() || '思维导图';
    const titleNode = entry.tab?.querySelector?.('.mnote-main-tab-title');
    if (titleNode) titleNode.textContent = entry.title;
    entry.panel.innerHTML = '<main class="document-shell mnote-resource-tab-mindmap-shell" data-editor-host="mindmap_resource_tab"><div class="mnote-resource-tab-mindmap-root" data-testid="mnote-mindmap-editor-root" data-editor-host-kind="mindmap_resource_tab" data-runtime-editor-status="booting" data-pane-role="resource"></div></main>';
    entry.panel.setAttribute('data-mnote-object-editor', 'mindmap');
    entry.panel.setAttribute('data-mnote-object-identity', entry.objectIdentity);
    entry.panel.setAttribute('data-mnote-mindmap-id', mindmapId);
    const root = entry.panel.querySelector('[data-testid="mnote-mindmap-editor-root"]');
    if (!(root instanceof HTMLElement)) throw new Error('mindmap_resource_root_missing');
    root.setAttribute('data-mnote-object-editor', 'mindmap');
    root.setAttribute('data-mnote-object-identity', entry.objectIdentity);
    root.setAttribute('data-mnote-mindmap-id', mindmapId);
    root.setAttribute('data-document-id', documentId);
    const mountId = runtime.mount(root, bootstrap);
    root.setAttribute('data-runtime-mount-id', String(mountId));
    root.setAttribute('data-editor-host-kind', 'mindmap_resource_tab');
    root.setAttribute('data-runtime-editor-status', 'mounted');
    entry.view = null;
    entry.session = null;
    entry.mindmapRuntime = { runtime, mountId, root };
  };

  const openUnsupportedSideTarget = (input = {}) => {
    const url = currentUrl();
    secondaryQueryParamNames.forEach((name) => url.searchParams.delete(name));
    const previousView = paneViewRegistry.get('secondary');
    if (previousView) {
      unmountEditorViewBinding(previousView);
      paneViewRegistry.delete('secondary');
    }
    unmountMindmapPane('secondary');
    const workspace = document.querySelector('.mnote-document-workspace');
    if (workspace instanceof HTMLElement) {
      workspace.setAttribute('data-has-secondary-pane', 'true');
      setSecondaryEditorHostVisible(true);
      applyStoredSecondaryWidth();
    }
    const resizerNode = document.querySelector('[data-document-pane-resizer="true"]');
    if (resizerNode instanceof HTMLElement) resizerNode.hidden = false;
    const pane = document.querySelector('[data-document-pane="true"][data-pane-role="secondary"]');
    if (pane instanceof HTMLElement) {
      pane.hidden = false;
      pane.setAttribute('data-pane-visible', 'true');
      pane.setAttribute('data-mnote-side-target', 'unsupported-resource');
      pane.removeAttribute('data-pane-document-id');
      pane.removeAttribute('data-pane-workspace-id');
    }
    const root = document.querySelector(`${ROOT_SELECTOR}[data-pane-role="secondary"]`);
    const title = String(input.title || input.fileName || input.assetId || '资源').trim() || '资源';
    if (root instanceof HTMLElement) {
      root.replaceChildren();
      document.querySelectorAll('[data-document-pane="true"][data-pane-role="secondary"] [data-testid="mnote-leptos-tiptap-slash-menu"]').forEach((node) => {
        if (node instanceof HTMLElement) node.remove();
      });
      root.setAttribute('data-editor-host-kind', 'unsupported_resource_side_target');
      root.setAttribute('data-mnote-side-target-unsupported', 'true');
      root.setAttribute('data-mnote-side-target-asset-id', String(input.assetId || ''));
      const placeholder = document.createElement('div');
      placeholder.className = 'mnote-resource-tab-error';
      placeholder.setAttribute('data-mnote-side-target-placeholder', 'true');
      placeholder.innerHTML = '<div class="mnote-resource-tab-error-inner"><h1>暂不支持在侧栏打开此资源</h1><p></p></div>';
      const text = placeholder.querySelector('p');
      if (text) text.textContent = title;
      root.append(placeholder);
    }
    document.documentElement.setAttribute('data-mnote-side-target-unsupported', 'true');
    document.documentElement.setAttribute('data-mnote-side-target-asset-id', String(input.assetId || ''));
    replaceUrlState(url);
    return true;
  };

  const openResourceInActiveTab = async (input = {}) => {
    const paneRole = normalizePaneRole(input.paneRole || input.targetPaneRole || 'primary');
    if (paneRole === 'secondary') {
      if (workspace instanceof HTMLElement) {
        workspace.setAttribute('data-has-secondary-pane', 'true');
        applyStoredSecondaryWidth();
      }
      setSecondaryEditorHostVisible(true);
      const resizerNode = document.querySelector('[data-document-pane-resizer="true"]');
      if (resizerNode instanceof HTMLElement) resizerNode.hidden = false;
      const pane = document.querySelector('[data-document-pane="true"][data-pane-role="secondary"]');
      if (pane instanceof HTMLElement) {
        pane.hidden = false;
        pane.setAttribute('data-pane-visible', 'true');
      }
    }
    bindMainEditorPageTab(paneRole);
    const objectIdentity = String(input.objectIdentity || input.assetId || input.href || '').trim();
    if (!objectIdentity) return false;
    const registryKey = resourceTabRegistryKey(paneRole, objectIdentity);
    if (paneRole === 'secondary') {
      resourceTabRegistry.forEach((entry, key) => {
        if (normalizePaneRole(entry.paneRole) !== paneRole) return;
        if (key === registryKey) return;
        releaseResourceTabEntryRuntime(entry);
        if (entry.tab instanceof HTMLElement) entry.tab.remove();
        if (entry.panel instanceof HTMLElement) entry.panel.remove();
        resourceTabRegistry.delete(key);
        removeFromResourceTabMru(paneRole, key);
      });
    }
    const existing = resourceTabRegistry.get(registryKey);
    if (existing) {
      refreshExistingOfficeResourceTab(existing, input);
      activateMainEditorTab(registryKey, paneRole);
      return true;
    }
    const entry = createResourceTabDom({ ...input, objectIdentity, paneRole });
    if (!entry) return false;
    resourceTabRegistry.set(registryKey, entry);
    activateMainEditorTab(registryKey, paneRole);
    try {
      if (entry.kind === 'mindmap') {
        await openMindmapResourceTab(entry, input);
      } else if (entry.kind === 'markdown' || entry.kind === 'text' || entry.kind === 'code') {
        await openTiptapResourceTab(entry, input);
      } else {
        openPassiveResourceTab(entry, input);
      }
      activateMainEditorTab(registryKey, paneRole);
      return true;
    } catch (error) {
      console.warn('mnote resource tab 打开失败', error);
      markResourceTabError(entry);
      return true;
    }
  };

  const mountPane = async (runtimeDescriptor) => {
    const runtime = await loadRuntime();
    const session = getOrCreateDocumentSession(runtimeDescriptor);
    const view = createEditorViewBinding(session, runtime, runtimeDescriptor);

    const mountOptions = {
      documentId: session.documentId,
      workspaceId: session.workspaceId,
      title: session.title,
      content: session.currentTiptapDocument,
      revision: session.revision,
      conflictDetectionKey: session.conflictDetectionKey,
      readOnly: session.readOnly,
      editable: !session.readOnly,
      pageOptions: runtimeDescriptor.aggregate.layout?.pageOptions || {},
    };

    setStatus(runtimeDescriptor, 'loading-assets');
    clearEmbeddedLocalDraft(runtimeDescriptor);
    try {
      const mountId = runtime.mount(runtimeDescriptor.root, mountOptions);
      view.mountId = mountId;
      runtimeDescriptor.root.setAttribute('data-runtime-mount-id', String(mountId));
      runtimeDescriptor.root.setAttribute('data-editor-host-kind', 'leptos_tiptap_island');
      setStatus(runtimeDescriptor, 'mounting-editor');
      paneViewRegistry.set(runtimeDescriptor.paneRole, view);
    } catch (error) {
      unmountEditorViewBinding(view);
      throw error;
    }
  };

  window.__mnoteDocumentPaneRuntime = {
    openPrimaryDocument: async ({ documentId, workspaceId, sourceKind, rootUri, url } = {}) => {
      const id = typeof documentId === 'string' ? documentId.trim() : '';
      if (!id) return false;
      const descriptor = descriptorFromCurrentUrl('primary', id, { workspaceId, sourceKind, rootUri });
      await replacePaneDocument('primary', descriptor);
      activateMainEditorTab('', 'primary');
      updatePrimaryUrl(descriptor, url instanceof URL ? url : null);
      return true;
    },
    openPrimaryMindmap: async ({ documentId, mindmapId, workspaceId, url } = {}) => {
      const docId = typeof documentId === 'string' ? documentId.trim() : '';
      const mapId = typeof mindmapId === 'string' ? mindmapId.trim() : '';
      if (!docId || !mapId) return false;
      await replacePrimaryPaneMindmap({
        documentId: docId,
        mindmapId: mapId,
        workspaceId: typeof workspaceId === 'string' ? workspaceId.trim() : '',
        url,
      });
      return true;
    },
    openSecondaryDocument: async ({ documentId, workspaceId, sourceKind, rootUri, url } = {}) => {
      const id = typeof documentId === 'string' ? documentId.trim() : '';
      if (!id) return false;
      const descriptor = descriptorFromCurrentUrl('secondary', id, { workspaceId, sourceKind, rootUri });
      await replacePaneDocument('secondary', descriptor);
      activateMainEditorTab('', 'secondary');
      if (url instanceof URL) replaceUrlState(url);
      return true;
    },
    resolveResourceOpen: (input) => resolveResourceOpen(input),
    openResourceInActiveTab: openResourceInActiveTab,
    activatePageTab: ({ paneRole } = {}) => {
      const role = normalizePaneRole(paneRole || 'primary');
      bindMainEditorPageTab(role);
      activateMainEditorTab('', role);
      return true;
    },
    openResourceAsSideTarget: async (input = {}) => {
      return openResourceInActiveTab({ ...input, paneRole: 'secondary', openTarget: 'active-tab' });
    },
    closeSecondaryDocument: ({ url } = {}) => {
      closeSecondaryPane(url instanceof URL ? url : currentUrl());
      return true;
    },
    getOpenEditorsSnapshot: () => buildOpenEditorsSnapshot(),
    refreshDocument: async ({ documentId, workspaceId, source } = {}) => {
      const targets = Array.from(documentSessionRegistry.values()).filter((session) => (
        sessionMatchesDocumentWorkspace(session, documentId, workspaceId)
      ));
      await Promise.all(targets.map((session) => refreshSessionFromExternalChange(
        session,
        source || 'mnote-web-programmatic-refresh',
      )));
      return targets.length;
    },
    refreshPrimaryDocument: async ({ documentId, workspaceId, source } = {}) => {
      const primary = paneViewRegistry.get('primary');
      if (!primary?.session) return 0;
      if (!sessionMatchesDocumentWorkspace(primary.session, documentId, workspaceId)) return 0;
      await refreshSessionFromExternalChange(primary.session, source || 'mnote-web-programmatic-refresh');
      return 1;
    },
  };

  bindMainEditorPageTab('primary');
  bindMainEditorPageTab('secondary');

  window.addEventListener('pagehide', () => {
    Array.from(documentSessionRegistry.values()).forEach((session) => {
      sessionViews(session).forEach((view) => {
        unmountEditorViewBinding(view, { scheduleRelease: false });
      });
      releaseDocumentSession(session);
    });
  }, { once: true });

  if (paneRuntimes.length) {
    Promise.all(paneRuntimes.map((paneRuntime) => mountPane(paneRuntime))).catch((error) => {
      console.error('mnote multi-pane editor mount failed', error);
    });
  }
})();
</script>"#
}

fn runtime_asset_root() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("../../spikes/leptos-tiptap-spike/generated/island")
}

fn resolve_runtime_asset_path(asset_path: &str) -> Option<PathBuf> {
    let asset_path = asset_path.trim();
    if asset_path.is_empty() || asset_path.starts_with('/') || asset_path.contains('\\') {
        return None;
    }
    let mut resolved = runtime_asset_root();
    for component in FsPath::new(asset_path).components() {
        match component {
            Component::Normal(part) => resolved.push(part),
            _ => return None,
        }
    }
    Some(resolved)
}

fn runtime_asset_content_type(asset_path: &str) -> &'static str {
    if asset_path.ends_with(".wasm") {
        "application/wasm"
    } else if asset_path.ends_with(".js") {
        "application/javascript; charset=utf-8"
    } else if asset_path.ends_with(".json") {
        "application/json; charset=utf-8"
    } else {
        "application/octet-stream"
    }
}

pub async fn editor_image_placeholder_asset() -> Response {
    const SVG: &str = r##"<svg xmlns="http://www.w3.org/2000/svg" width="640" height="360" viewBox="0 0 640 360" role="img" aria-label="E24 image placeholder">
  <rect width="640" height="360" rx="18" fill="#f3f4f6"/>
  <rect x="42" y="42" width="556" height="276" rx="14" fill="#ffffff" stroke="#d1d5db" stroke-width="2"/>
  <circle cx="168" cy="132" r="34" fill="#93c5fd"/>
  <path d="M98 278 246 174 340 242 410 192 542 278Z" fill="#86efac"/>
  <path d="M98 278 246 174 340 242 410 192 542 278" fill="none" stroke="#16a34a" stroke-width="8" stroke-linejoin="round"/>
  <text x="320" y="322" text-anchor="middle" font-family="Arial, sans-serif" font-size="22" fill="#374151">E24 Image</text>
</svg>"##;
    Response::builder()
        .status(StatusCode::OK)
        .header(header::CONTENT_TYPE, "image/svg+xml; charset=utf-8")
        .header(header::CACHE_CONTROL, "no-store")
        .header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
        .body(Body::from(SVG))
        .unwrap_or_else(|_| Response::new(Body::empty()))
}

pub async fn resource_open_runtime_asset() -> Response {
    // include_str! 路径相对于当前源文件 (src/routes/web_shell.rs -> ../../browser/)
    const JS: &str = include_str!("../../browser/resource-open-runtime.js");
    Response::builder()
        .status(StatusCode::OK)
        .header(
            header::CONTENT_TYPE,
            "application/javascript; charset=utf-8",
        )
        .header(header::CACHE_CONTROL, "no-store")
        .header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
        .body(Body::from(JS))
        .unwrap_or_else(|_| Response::new(Body::empty()))
}

pub async fn local_upload_runtime_asset() -> Response {
    const JS: &str = include_str!("../../browser/local-upload-runtime.js");
    Response::builder()
        .status(StatusCode::OK)
        .header(
            header::CONTENT_TYPE,
            "application/javascript; charset=utf-8",
        )
        .header(header::CACHE_CONTROL, "no-store")
        .header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
        .body(Body::from(JS))
        .unwrap_or_else(|_| Response::new(Body::empty()))
}

pub async fn filetree_context_menu_runtime_asset() -> Response {
    const JS: &str = include_str!("../../browser/filetree-context-menu-runtime.js");
    Response::builder()
        .status(StatusCode::OK)
        .header(
            header::CONTENT_TYPE,
            "application/javascript; charset=utf-8",
        )
        .header(header::CACHE_CONTROL, "no-store")
        .header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
        .body(Body::from(JS))
        .unwrap_or_else(|_| Response::new(Body::empty()))
}

pub async fn filetree_runtime_asset() -> Response {
    const JS: &str = include_str!("../../browser/filetree-runtime.js");
    Response::builder()
        .status(StatusCode::OK)
        .header(
            header::CONTENT_TYPE,
            "application/javascript; charset=utf-8",
        )
        .header(header::CACHE_CONTROL, "no-store")
        .header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
        .body(Body::from(JS))
        .unwrap_or_else(|_| Response::new(Body::empty()))
}

pub async fn filetree_selection_runtime_asset() -> Response {
    const JS: &str = include_str!("../../browser/filetree-selection-runtime.js");
    Response::builder()
        .status(StatusCode::OK)
        .header(
            header::CONTENT_TYPE,
            "application/javascript; charset=utf-8",
        )
        .header(header::CACHE_CONTROL, "no-store")
        .header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
        .body(Body::from(JS))
        .unwrap_or_else(|_| Response::new(Body::empty()))
}

pub async fn tree_live_controller_runtime_asset() -> Response {
    const JS: &str = include_str!("../../browser/tree-live-controller.js");
    Response::builder()
        .status(StatusCode::OK)
        .header(
            header::CONTENT_TYPE,
            "application/javascript; charset=utf-8",
        )
        .header(header::CACHE_CONTROL, "no-store")
        .header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
        .body(Body::from(JS))
        .unwrap_or_else(|_| Response::new(Body::empty()))
}

pub async fn tree_shell_runtime_asset() -> Response {
    const JS: &str = include_str!("../../browser/tree-shell-runtime.js");
    Response::builder()
        .status(StatusCode::OK)
        .header(
            header::CONTENT_TYPE,
            "application/javascript; charset=utf-8",
        )
        .header(header::CACHE_CONTROL, "no-store")
        .header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
        .body(Body::from(JS))
        .unwrap_or_else(|_| Response::new(Body::empty()))
}

pub async fn document_conflict_panel_runtime_asset() -> Response {
    // include_str! 路径相对于当前源文件 (src/routes/web_shell.rs -> ../../browser/)
    const JS: &str = include_str!("../../browser/document-conflict-panel-runtime.js");
    Response::builder()
        .status(StatusCode::OK)
        .header(
            header::CONTENT_TYPE,
            "application/javascript; charset=utf-8",
        )
        .header(header::CACHE_CONTROL, "no-store")
        .header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
        .body(Body::from(JS))
        .unwrap_or_else(|_| Response::new(Body::empty()))
}

pub async fn leptos_tiptap_manifest() -> Response {
    let manifest = json!({
        "entryAssetPath": "mnote-leptos-tiptap-spike-island.js",
        "wasmAssetPath": "mnote-leptos-tiptap-spike-island_bg.wasm",
        "assetPaths": [
            "mnote-leptos-tiptap-spike-island.js",
            "mnote-leptos-tiptap-spike-island_bg.wasm"
        ],
        "generatedRootPath": "rust/spikes/leptos-tiptap-spike/generated/island"
    });
    let mut response = Json(manifest).into_response();
    stamp_shell_headers(response.headers_mut(), "leptos-tiptap-runtime");
    response
}

pub async fn leptos_tiptap_asset(Path(asset_path): Path<String>) -> Result<Response, WebError> {
    let Some(resolved) = resolve_runtime_asset_path(&asset_path) else {
        return Err(WebError::bad_request_code(
            "runtime_asset_path_invalid",
            "leptos-tiptap runtime asset 路径非法",
        ));
    };
    let bytes = std::fs::read(&resolved).map_err(|_| {
        WebError::new(
            StatusCode::NOT_FOUND,
            "runtime_asset_not_found",
            format!("leptos-tiptap runtime asset 不存在: {asset_path}"),
        )
    })?;
    let response = Response::builder()
        .status(StatusCode::OK)
        .header(
            header::CONTENT_TYPE,
            runtime_asset_content_type(&asset_path),
        )
        .header(header::CACHE_CONTROL, "no-store")
        .header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
        .body(Body::from(bytes))
        .map_err(|error| WebError::internal(format!("runtime asset 响应构造失败: {error}")))?;
    Ok(response)
}

pub async fn page_aggregate(
    State(state): State<AppState>,
    Extension(context): Extension<RequestContext>,
    Path(document_id): Path<String>,
    Query(query): Query<DocumentShellQuery>,
) -> Result<Response, WebError> {
    let aggregate = build_page_aggregate_snapshot(
        &state,
        &context,
        &document_id,
        query.workspace_id.as_deref(),
        query.source_kind.as_deref(),
        query.root_uri.as_deref(),
    )
    .await?;
    let projection_owner = aggregate.source_label();
    let mut response = (
        StatusCode::OK,
        Json(json!({
            "ok": true,
            "owner": "mnote-web",
            "schema": "mnote.page_aggregate.v1",
            "result": aggregate,
            "requestId": context.trace.request_id,
            "traceId": context.trace.trace_id,
        })),
    )
        .into_response();
    stamp_shell_headers(response.headers_mut(), "page-aggregate");
    if let Ok(name) = HeaderName::from_lowercase(b"x-mnote-page-aggregate-owner") {
        if let Ok(value) = HeaderValue::from_str(projection_owner) {
            response.headers_mut().insert(name, value);
        }
    }
    Ok(response)
}

pub(crate) async fn build_page_aggregate_snapshot(
    state: &AppState,
    context: &RequestContext,
    document_id: &str,
    workspace_id: Option<&str>,
    source_kind: Option<&str>,
    root_uri: Option<&str>,
) -> Result<PageAggregate, WebError> {
    if source_kind.map(str::trim).filter(|value| !value.is_empty()) == Some("local_folder") {
        let root_uri = root_uri
            .map(str::trim)
            .filter(|value| !value.is_empty())
            .ok_or_else(|| {
                WebError::bad_request_code("local_folder_root_required", "缺少本地文件夹 rootUri")
            })?;
        ensure_local_workspace_read_access_with_state(state, context, root_uri)
            .map_err(|error| error.with_context(context))?;
        return resolve_local_markdown_page_aggregate(root_uri, document_id);
    }

    let meta = load_document_meta_result(
        state,
        context,
        DocumentMetaQuery {
            document_id: document_id.to_string(),
            workspace_id: workspace_id.map(str::to_string),
        },
    )
    .await?;
    let content = load_document_content_result(
        state,
        context,
        DocumentContentQuery {
            document_id: document_id.to_string(),
            workspace_id: workspace_id.map(str::to_string),
        },
    )
    .await?;

    let projection = execute_runtime_query_against_data(
        context,
        workspace_id,
        RuntimeQueryEnvelopeWire {
            name: "page.aggregate.get".into(),
            payload: json!({
                "documentId": document_id,
                "workspaceId": workspace_id,
            }),
        },
        json!({
            "meta": meta,
            "content": content,
        }),
    )?;

    serde_json::from_value::<PageAggregate>(projection).map_err(|error| {
        WebError::internal(format!("Page Aggregate projection 反序列化失败: {error}"))
    })
}

fn stamp_recent_page_cookie(headers: &mut HeaderMap, document_id: &str) {
    let value = document_id
        .chars()
        .map(|ch| {
            if ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-' | '.') {
                ch
            } else {
                '_'
            }
        })
        .collect::<String>();
    if value.is_empty() {
        return;
    }
    let cookie = format!("{COOKIE_RECENT_PAGE_ID}={value}; Path=/; SameSite=Lax");
    if let Ok(value) = HeaderValue::from_str(&cookie) {
        headers.append(header::SET_COOKIE, value);
    }
}

fn stamp_shell_headers(headers: &mut HeaderMap, shell: &'static str) {
    if let Ok(name) = HeaderName::from_lowercase(HEADER_MNOTE_WEB_OWNER.as_bytes()) {
        headers.insert(name, HeaderValue::from_static("mnote-web"));
    }
    if let Ok(name) = HeaderName::from_lowercase(HEADER_MNOTE_WEB_SHELL.as_bytes()) {
        headers.insert(name, HeaderValue::from_static(shell));
    }
    headers.insert(header::CACHE_CONTROL, HeaderValue::from_static("no-store"));
}

pub(crate) fn escape_html(value: &str) -> String {
    value
        .replace('&', "&amp;")
        .replace('<', "&lt;")
        .replace('>', "&gt;")
        .replace('"', "&quot;")
}

pub(crate) fn escape_script_json(value: &str) -> String {
    value.replace("</script", "<\\/script")
}

pub(crate) async fn load_workspace_shell_projection(
    config: &AppConfig,
    context: &RequestContext,
    workspace_id: &str,
    active_document_id: Option<&str>,
    default_workspace_name: &str,
) -> WorkspaceShellProjection {
    let spec = ProjectionSnapshotSpec {
        workspace_id,
        root_node_id: None,
        depth: Some(99),
        projection: KernelProjectionKind::SidebarTree,
        query: None,
        max_results: None,
    };
    let dataset = match load_projection_snapshot(config, context, &spec).await {
        Ok(snapshot) => snapshot.dataset,
        Err(_) if config.allow_dev_fixtures => {
            let documents = active_document_id
                .map(|document_id| {
                    json!([{
                        "id": document_id,
                        "workspace_id": workspace_id,
                        "title": "个人",
                        "parent_id": null,
                        "sort_order": 0,
                        "is_starred": false
                    }])
                })
                .unwrap_or_else(|| json!([]));
            json!({
                "active_workspace_id": workspace_id,
                "active_page_id": active_document_id,
                "workspaces": [{ "id": workspace_id, "name": default_workspace_name }],
                "documents": documents,
                "dev_fixture": true
            })
        }
        Err(_) => json!({
            "active_workspace_id": workspace_id,
            "active_page_id": active_document_id,
            "workspaces": [{ "id": workspace_id, "name": default_workspace_name }],
            "documents": [],
            "degraded": true,
            "degraded_reason": "projection_unavailable"
        }),
    };

    build_workspace_shell_projection(
        &dataset,
        workspace_id,
        active_document_id,
        default_workspace_name,
    )
}

/// 加载侧栏页面树 HTML（SSR）
///
/// 从 sidebar projection snapshot 中构建页面树 HTML 字符串。
/// 如果加载失败（如 Convex 未配置），返回空字符串，侧栏静默降级为无树状态。
/// 当 allow_dev_fixtures 启用且 Convex 不可用时，使用内建示例数据展示页面树。
pub(crate) async fn load_sidebar_tree_html(
    config: &AppConfig,
    context: &RequestContext,
    workspace_id: &str,
    active_document_id: Option<&str>,
) -> Option<String> {
    let spec = ProjectionSnapshotSpec {
        workspace_id,
        root_node_id: None,
        depth: Some(99),
        projection: KernelProjectionKind::SidebarTree,
        query: None,
        max_results: None,
    };
    let result = match load_projection_snapshot(config, context, &spec).await {
        Ok(snapshot) => Some((snapshot.projection, false)),
        Err(_) if config.allow_dev_fixtures => {
            // Dev 模式降级：如果调用方已经有 active 页面，优先保留这条真实选择链。
            let documents = active_document_id
                .map(|document_id| {
                    serde_json::json!([
                        { "id": document_id, "workspace_id": workspace_id, "title": "个人", "parent_id": null, "sort_order": 0 }
                    ])
                })
                .unwrap_or_else(|| {
                    serde_json::json!([
                        { "id": "dev_welcome", "workspace_id": workspace_id, "title": "欢迎使用 MNOTE", "parent_id": null, "sort_order": 0 },
                        { "id": "dev_guide", "workspace_id": workspace_id, "title": "使用指南", "parent_id": "dev_welcome", "sort_order": 1 },
                        { "id": "dev_api", "workspace_id": workspace_id, "title": "API 文档", "parent_id": "dev_welcome", "sort_order": 2 }
                    ])
                });
            let dev_dataset = serde_json::json!({
                "active_workspace_id": workspace_id,
                "documents": documents,
                "trashed_documents": [],
                "media_assets": [],
                "trashed_media_assets": [],
                "mindmap_assets": [],
                "trashed_mindmap_assets": [],
                "table_assets": [],
                "trashed_table_assets": [],
                "mindmap_docs": [],
                "mindmap_asset_children": {}
            });
            execute_kernel_query(context, workspace_id, projection_query(&spec), dev_dataset)
                .ok()
                .map(|projection| (projection, true))
        }
        Err(_) => None,
    };
    result.map(|(projection, dev_fixture)| {
        let rows = collect_page_tree_render_rows(&projection);
        let html = render_initial_page_tree_html(&PageTreeInitialRenderInput {
            rows,
            active_node_id: active_document_id.map(ToOwned::to_owned),
            focused_node_id: None,
        });
        mark_dev_fixture_html(html, dev_fixture, "sidebar-tree")
    })
}

/// 加载文件树 HTML（SSR）
///
/// 文件树与页面树共用同一份 sidebar dataset，再由 Rust kernel 输出 file_tree projection。
pub(crate) async fn load_file_tree_html(
    config: &AppConfig,
    context: &RequestContext,
    workspace_id: &str,
    active_document_id: Option<&str>,
    active_row_id: Option<&str>,
) -> Option<String> {
    let spec = ProjectionSnapshotSpec {
        workspace_id,
        root_node_id: None,
        depth: Some(99),
        projection: KernelProjectionKind::FileTree,
        query: None,
        max_results: None,
    };
    let result = match load_projection_snapshot(config, context, &spec).await {
        Ok(snapshot) => Some((snapshot.projection, false)),
        Err(_) if config.allow_dev_fixtures => {
            let documents = active_document_id
                .map(|document_id| {
                    serde_json::json!([
                        { "id": document_id, "workspace_id": workspace_id, "title": "个人", "parent_id": null, "sort_order": 0 }
                    ])
                })
                .unwrap_or_else(|| {
                    serde_json::json!([
                        { "id": "dev_welcome", "workspace_id": workspace_id, "title": "欢迎使用 MNOTE", "parent_id": null, "sort_order": 0 },
                        { "id": "dev_guide", "workspace_id": workspace_id, "title": "使用指南", "parent_id": "dev_welcome", "sort_order": 1 }
                    ])
                });
            let dev_dataset = serde_json::json!({
                "active_workspace_id": workspace_id,
                "documents": documents,
                "trashed_documents": [],
                "media_assets": [],
                "trashed_media_assets": [],
                "mindmap_assets": [],
                "trashed_mindmap_assets": [],
                "table_assets": [],
                "trashed_table_assets": [],
                "mindmap_docs": [],
                "mindmap_asset_children": {}
            });
            execute_kernel_query(context, workspace_id, projection_query(&spec), dev_dataset)
                .ok()
                .map(|projection| (projection, true))
        }
        Err(_) => None,
    };
    result.map(|(projection, dev_fixture)| {
        let rows = collect_filetree_render_rows(&projection, active_document_id, active_row_id);
        let html = render_initial_filetree_html(&FileTreeInitialRenderInput { rows });
        mark_dev_fixture_html(html, dev_fixture, "file-tree")
    })
}

fn mark_dev_fixture_html(html: String, dev_fixture: bool, kind: &'static str) -> String {
    if !dev_fixture {
        return html;
    }
    format!(
        r#"<span hidden data-mnote-dev-fixture="true" data-mnote-dev-fixture-kind="{kind}"></span>{html}"#
    )
}

pub(crate) fn render_local_sidebar_tree_html(
    root_uri: &str,
    active_document_id: Option<&str>,
) -> Result<String, WebError> {
    let snapshot = load_local_folder_page_tree_snapshot(root_uri)?;
    let rows = collect_page_tree_render_rows(&snapshot.projection);
    Ok(render_initial_page_tree_html(&PageTreeInitialRenderInput {
        rows,
        active_node_id: active_document_id.map(ToOwned::to_owned),
        focused_node_id: None,
    }))
}

pub(crate) fn render_local_file_tree_html(
    root_uri: &str,
    active_document_id: Option<&str>,
    active_row_id: Option<&str>,
) -> Result<String, WebError> {
    let snapshot = load_local_folder_file_tree_snapshot(root_uri)?;
    let rows =
        collect_filetree_render_rows(&snapshot.projection, active_document_id, active_row_id);
    Ok(render_initial_filetree_html(&FileTreeInitialRenderInput {
        rows,
    }))
}

#[cfg(test)]
mod tests {
    use crate::app::{build_app, AppConfig, AppState};
    use crate::context::RequestContext;
    use axum::body::{to_bytes, Body};
    use axum::http::{header, HeaderMap, HeaderValue, Method, Request, StatusCode, Uri};
    use control_plane::{DirectoryGrantInput, UpsertUserInput};
    use serde_json::Value;
    use tower::util::ServiceExt;

    fn app() -> axum::Router {
        build_app(AppState::new(AppConfig {
            service_name: "mnote-web".into(),
            service_version: "0.1.0".into(),
            bind_addr: "127.0.0.1:0".into(),
            public_bind_addr: "127.0.0.1:3000".into(),
            legacy_next_base_url: Some("http://127.0.0.1:3100".into()),
            enable_legacy_next_compat: true,
            enable_debug_shell_routes: false,
            enable_editor_actor: true,
            hermes_base_path: "/api/hermes".into(),
            compat_next_base_path: "/api/compat/next".into(),
            convex_url: None,
            convex_admin_key: None,
            allow_dev_fixtures: true,
            query_fixtures_json: Some(
                r#"{
                  "documents:getMeta": {
                    "id": "doc_1",
                    "workspace_id": "ws_demo",
                    "title": "服务端页面",
                    "updated_at": "2026-04-18T09:30:00Z",
                    "can_edit": true,
                    "word_count": 42,
                    "character_count": 128,
                    "block_count": 3
                  },
                  "documents:getContent": {
                    "content": [{"id": "block_1", "type": "paragraph", "content": []}],
                    "editorDocument": {
                      "documentId": "doc_1",
                      "rootBlockIds": ["editor_1"],
                      "blocks": [{
                        "blockId": "editor_1",
                        "blockType": "paragraph",
                        "contentNodes": [{
                          "payload": {"type": "text", "text": "来自 editorDocument 的正文"},
                          "attrs": {}
                        }],
                        "childBlockIds": []
                      }]
                    },
                    "revision": 7,
                    "conflict_detection_key": "doc_1:7",
                    "pageSubtree": {"rootNodeId": "doc_1", "outline": []}
                  }
                }"#
                .into(),
            ),
            mutation_fixtures_json: None,
            dev_user_id: "dev-user".into(),
            dev_user_name: "开发用户".into(),
            dev_user_email: "dev@mnote.local".into(),
        }))
        .layer(axum::middleware::from_fn(inject_test_actor))
    }

    async fn inject_test_actor(
        mut request: axum::extract::Request,
        next: axum::middleware::Next,
    ) -> axum::response::Response {
        request
            .headers_mut()
            .entry("x-mnote-actor-id")
            .or_insert(HeaderValue::from_static("user_test"));
        request
            .headers_mut()
            .entry("x-mnote-actor-type")
            .or_insert(HeaderValue::from_static("user"));
        next.run(request).await
    }

    fn init_local_workspace(root: &std::path::Path, actor_id: &str) {
        crate::routes::local_folder_source::initialize_local_workspace_for_actor(
            actor_id,
            &format!("file://{}", root.display()),
        )
        .expect("init local workspace");
    }

    fn request_context(actor_id: &str, actor_type: &str) -> RequestContext {
        let mut headers = HeaderMap::new();
        headers.insert("x-mnote-actor-id", actor_id.parse().unwrap());
        headers.insert("x-mnote-actor-type", actor_type.parse().unwrap());
        RequestContext::from_http_parts(
            &Method::GET,
            &"/api/page-aggregate/local-md:test.md".parse().expect("uri"),
            &headers,
        )
    }

    fn grant_local_workspace_read_access(
        state: &AppState,
        actor_id: &str,
        root_uri: &str,
        root: &std::path::Path,
    ) {
        state
            .control_plane()
            .upsert_user(UpsertUserInput {
                id: Some(actor_id.into()),
                email: Some(format!("{actor_id}@example.com")),
                username: actor_id.into(),
                display_name: actor_id.into(),
                role: None,
                password_hash: None,
            })
            .expect("upsert sqlite reader");
        state
            .control_plane()
            .grant_directory_access(DirectoryGrantInput {
                user_id: actor_id.into(),
                workspace_id: None,
                root_uri: root_uri.into(),
                root_path: root
                    .canonicalize()
                    .expect("canonical root")
                    .display()
                    .to_string(),
                permission: "read".into(),
                recursive: true,
                capabilities: vec![],
                source: "unit-test".into(),
                created_by: None,
            })
            .expect("grant sqlite read");
    }

    fn app_with_unreachable_convex_without_fixture() -> axum::Router {
        build_app(AppState::new(AppConfig {
            service_name: "mnote-web".into(),
            service_version: "0.1.0".into(),
            bind_addr: "127.0.0.1:0".into(),
            public_bind_addr: "127.0.0.1:3000".into(),
            legacy_next_base_url: None,
            enable_legacy_next_compat: false,
            enable_debug_shell_routes: false,
            enable_editor_actor: true,
            hermes_base_path: "/api/hermes".into(),
            compat_next_base_path: "/api/compat/next".into(),
            convex_url: Some("http://127.0.0.1:9".into()),
            convex_admin_key: Some("test-admin-key".into()),
            allow_dev_fixtures: false,
            query_fixtures_json: None,
            mutation_fixtures_json: None,
            dev_user_id: "dev-user".into(),
            dev_user_name: "开发用户".into(),
            dev_user_email: "dev@mnote.local".into(),
        }))
    }

    #[tokio::test]
    async fn document_shell_returns_page_aggregate_snapshot() {
        let response = app()
            .oneshot(
                Request::builder()
                    .uri("/documents/doc_1?workspaceId=ws_demo")
                    .body(Body::empty())
                    .expect("request"),
            )
            .await
            .expect("response");

        assert_eq!(response.status(), StatusCode::OK);
        assert_eq!(
            response
                .headers()
                .get("x-mnote-web-owner")
                .and_then(|value| value.to_str().ok()),
            Some("mnote-web")
        );
        assert_eq!(
            response
                .headers()
                .get("x-mnote-web-shell")
                .and_then(|value| value.to_str().ok()),
            Some("document")
        );
        assert!(response
            .headers()
            .get_all("set-cookie")
            .iter()
            .any(|value| value
                .to_str()
                .unwrap_or_default()
                .contains("mnote_recent_page_id=doc_1")));
        let body = to_bytes(response.into_body(), usize::MAX)
            .await
            .expect("body");
        let html = String::from_utf8(body.to_vec()).expect("html");
        assert!(html.contains("data-testid=\"wolai-sidebar\""));
        assert!(html.contains("data-mnote-dev-fixture-kind=\"workspace-shell\""));
        assert!(html.contains("data-mnote-dev-fixture-kind=\"sidebar-tree\""));
        assert!(html.contains("data-mnote-dev-fixture-kind=\"file-tree\""));
        assert!(html.contains("data-testid=\"wolai-topbar\""));
        assert!(html.contains("data-testid=\"wolai-floating-ai\""));
        assert!(html.contains("星标置顶"));
        assert!(html.contains("我的页面"));
        assert!(html.contains("垃圾箱"));
        assert!(html.contains("模板中心"));
        assert!(html.contains("data-page-aggregate-snapshot=\"mnote.page_aggregate.v1\""));
        assert!(html.contains("data-testid=\"mnote-page-subtree\""));
        assert!(html.contains("data-page-tree-source=\"page_aggregate.tree.pageSubtree\""));
        assert!(html.contains("data-editor-host=\"leptos_tiptap_island\""));
        assert!(html.contains("aria-label=\"页面标题\""));
        assert!(html.contains("data-page-title-input=\"true\""));
        assert!(html.contains("data-title-endpoint=\"/api/documents/title\""));
        assert!(html.contains("mnote.document_title_controller.v1"));
        assert!(html.contains("const currentTarget = resolveTitleTarget(input);"));
        assert!(html.contains("documentId: currentTarget.documentId"));
        assert!(html.contains(
            r#".tree-row[data-shell-mode="page"][data-node-id="' + escaped + '"] > .tree-link > .tree-link-title"#
        ));
        assert!(html.contains("const fileTreePageTitle = (value) => {"));
        assert!(html.contains(
            "return normalized.toLowerCase().endsWith('.md') ? normalized : `${normalized}.md`;"
        ));
        assert!(html.contains(
            r#".tree-row[data-shell-mode="filetree"][data-row-id="${escapedDocRowId}"] > .tree-link > .tree-link-title`, fileTreePageTitle(title)"#
        ));
        assert!(html.contains("row.getAttribute('data-row-id') === `doc:${documentId}`"));
        assert!(!html.contains("row.getAttribute('data-row-id') === `index:${documentId}`"));
        assert!(!html.contains("[data-node-id=\"${escapedId}\"] .tree-link-title"));
        assert!(html.contains("data-testid=\"wolai-page-settings-trigger\""));
        assert!(html.contains("data-mnote-action=\"open-page-settings\""));
        assert!(html.contains("data-mnote-action=\"open-page-ai\""));
        assert!(html.contains("\"titleEndpoint\":\"/api/documents/title\""));
        assert!(html.contains("data-testid=\"mnote-document-workspace\""));
        assert!(html.contains("data-document-pane=\"true\""));
        assert!(html.contains("data-pane-role=\"primary\""));
        assert!(html.contains("data-document-pane-resizer=\"true\""));
        assert!(html.contains("data-mnote-main-tab-strip"));
        assert!(html.contains("class=\"mnote-main-tab-badge\""));
        assert!(!html.contains(">description</span><span class=\"mnote-main-tab-title\""));
        assert!(html.contains("[data-mnote-main-tab=\"page\"] .mnote-main-tab-title"));
        assert!(html.contains("pageTab.setAttribute('data-document-id', documentId);"));
        assert!(html.contains("data-mnote-tab-badge-kind"));
        assert!(html.contains("resourceTabBadgeKind(input, kind)"));
        assert!(html.contains("mnote.open_editors_snapshot.v1"));
        assert!(html.contains("getOpenEditorsSnapshot"));
        assert!(html.contains("bindMainEditorTabStrip"));
        assert!(html.contains("data-mnote-tab-strip-bound"));
        assert!(html.contains("currentWebShellDocumentId"));
        assert!(html.contains(
            "const documentId = String(entry?.ownerDocumentId || entry?.documentId || '').trim();"
        ));
        assert!(html.contains(
            "if (documentId) url.pathname = `/documents/${encodeURIComponent(documentId)}`;"
        ));
        assert!(!html
            .contains("nodes.pageTab?.getAttribute?.('data-document-id') || currentDocumentId()"));
        assert!(html.contains("runtime.default({ module_or_path: wasmUrl })"));
        assert!(html.contains("positionSlashMenuForRoot"));
        assert!(html.contains("menu.style.position = 'fixed';"));
        assert!(html.contains("installGlobalSlashMenuPositioning();"));
        assert!(html.contains("data-mnote-slash-positioned', 'host'"));
        assert!(html.contains(
            "const menu = root.querySelector('[data-testid=\"mnote-leptos-tiptap-slash-menu\"]');"
        ));
        assert!(html.contains("const setSlashMenuInactive = (menu, inactive) =>"));
        assert!(html.contains("const hideSlashMenusOutsideRoot = (activeRoot) =>"));
        assert!(html.contains("data-mnote-slash-inactive"));
        assert!(html.contains("if (activeRoot instanceof HTMLElement && root !== activeRoot)"));
        assert!(!html.contains(
            "|| document.querySelector('[data-testid=\"mnote-leptos-tiptap-slash-menu\"]')"
        ));
        assert!(html.contains("data-mnote-side-target-unsupported') !== 'true'"));
        assert!(
            html.contains("runtimeDescriptor.root.addEventListener(STATE_EVENT, view.onState);")
        );
        assert!(html.contains("data-mnote-resource-tab-host"));
        assert!(html.contains("openPrimaryMindmap"));
        assert!(html.contains("openResourceInActiveTab"));
        assert!(html.contains("/api/local-folder/resource/read"));
        assert!(html.contains("/api/local-folder/resource/write"));
        assert!(html.contains("replacePrimaryPaneMindmap"));
        assert!(html.contains("__MNOTE_MINDMAP_EDITOR_BOOTSTRAP__"));
        assert!(html.contains("data-mnote-object-identity"));
        assert!(html.contains("__MNOTE_DOCUMENT_PANES_BOOTSTRAP__"));
        assert!(html.contains("__MNOTE_TREE_LIVE_BOOTSTRAP__"));
        assert!(html.contains("mnote.tree_live_bootstrap.v1"));
        assert!(html.contains("/api/tree/events"));
        assert!(html.contains("data-mnote-tree-live-transport"));
        assert!(html.contains("syncPageAggregateScript(session, nextAggregate);"));
        assert!(html.contains(
            "const pageBodyTiptapDocument = (body, fallbackText = '', context = null) => {"
        ));
        assert!(html.contains("body?.blockDocument || body?.block_document"));
        assert!(html.contains("localizeTiptapAssetUrls(toTiptapDocument(body.content"));
        assert!(html
            .contains("const nextTiptapDocument = pageBodyTiptapDocument(nextBody, '', session);"));
        assert!(html.contains(
            "const tiptapDocument = pageBodyTiptapDocument(pageBody, '', runtimeDescriptor.bootstrap);"
        ));
        assert!(html.contains("mnote.localFolder.selfChangeSuppressions.v1"));
        assert!(html.contains("ensureLocalFolderSelfChangeSuppressions()"));
        assert!(html.contains("markLocalFolderSelfChangeSuppression(session);"));
        assert!(html.contains("const suppressibleSelfWrite = kind.includes('Create')"));
        assert!(html.contains("|| kind.includes('Modify(Data')"));
        assert!(html.contains("|| kind.includes('Modify(Any')"));
        assert!(!html.contains("mnote-web-document-shell"));

        // Resource open resolver contract
        assert!(html.contains("normalizeResourceTabKind"));
        assert!(html.contains("data-resource-tab-error"));
        assert!(html.contains("resourceTabCloseGuardReason"));
        assert!(html.contains("closeResourceTab"));
        assert!(html.contains("resourceTabRegistry.delete(key)"));
        assert!(html.contains("data-testid=\"mnote-secondary-editor-tab-host\""));
        assert!(html.contains("data-testid=\"mnote-secondary-resource-tab-host\""));
        assert!(html.contains("resourceTabRegistryKey(paneRole, objectIdentity)"));
        assert!(html.contains("openResourceInActiveTab({ ...input, paneRole: 'secondary'"));
        assert!(html.contains("data-mnote-editor-kind=\"resource\""));
        assert!(html.contains("const paneRole = normalizePaneRole(entry.paneRole);"));
        assert!(html.contains("paneRole,"));
        assert!(html.contains("markIntendedSlashRoot(entry);"));
        assert!(html.contains("if (activeEntry) markIntendedSlashRoot(activeEntry);"));
        assert!(html.contains("const nextKey = lastActiveResourceTabKey(role);"));
        assert!(html.contains("if (nextTab instanceof HTMLElement) nextTab.focus();"));
        assert!(html.contains("resourceTabBadgeKind(input, kind)"));
        assert!(html.contains("const refreshExistingOfficeResourceTab = (entry, input) =>"));
        assert!(html.contains("if (!entry || entry.kind !== 'office') return false;"));
        assert!(
            html.contains("if (currentHref !== nextHref) openPassiveResourceTab(entry, input);")
        );
        assert!(html.contains("refreshExistingOfficeResourceTab(existing, input);"));
    }

    #[tokio::test]
    async fn page_aggregate_endpoint_returns_snapshot_contract() {
        let response = app()
            .oneshot(
                Request::builder()
                    .uri("/api/page-aggregate/doc_1?workspaceId=ws_demo")
                    .body(Body::empty())
                    .expect("request"),
            )
            .await
            .expect("response");

        assert_eq!(response.status(), StatusCode::OK);
        assert_eq!(
            response
                .headers()
                .get("x-mnote-page-aggregate-owner")
                .and_then(|value| value.to_str().ok()),
            Some("compat-join")
        );
        assert_eq!(
            response
                .headers()
                .get(header::CACHE_CONTROL)
                .and_then(|value| value.to_str().ok()),
            Some("no-store")
        );
        let body = to_bytes(response.into_body(), usize::MAX)
            .await
            .expect("body");
        let payload: Value = serde_json::from_slice(&body).expect("json");
        assert_eq!(payload["owner"], "mnote-web");
        assert_eq!(payload["schema"], "mnote.page_aggregate.v1");
        assert_eq!(payload["result"]["source"], "CompatMetaContentJoin");
        assert_eq!(payload["result"]["projectionVersion"], 1);
        assert_eq!(payload["result"]["identity"]["documentId"], "doc_1");
        assert_eq!(payload["result"]["body"]["revision"], 7);
        assert_eq!(
            payload["result"]["body"]["projectionSource"],
            "editorDocument"
        );
        assert_eq!(
            payload["result"]["body"]["blockDocument"]["rootBlockIds"][0],
            "editor_1"
        );
        assert_eq!(
            payload["result"]["body"]["blockDocument"]["blocks"][0]["text"],
            "来自 editorDocument 的正文"
        );
    }

    #[tokio::test]
    async fn page_aggregate_endpoint_errors_without_convex_or_fixture() {
        let response = app_with_unreachable_convex_without_fixture()
            .oneshot(
                Request::builder()
                    .uri("/api/page-aggregate/doc_1?workspaceId=ws_demo")
                    .body(Body::empty())
                    .expect("request"),
            )
            .await
            .expect("response");

        if response.status() != StatusCode::SERVICE_UNAVAILABLE {
            let status = response.status();
            let body = to_bytes(response.into_body(), usize::MAX)
                .await
                .expect("body");
            panic!(
                "expected SERVICE_UNAVAILABLE, got {status}: {}",
                String::from_utf8_lossy(&body)
            );
        }
        assert_eq!(
            response
                .headers()
                .get("x-error-code")
                .and_then(|value| value.to_str().ok()),
            Some("convex_unavailable")
        );
        assert_eq!(
            response
                .headers()
                .get("x-error-phase")
                .and_then(|value| value.to_str().ok()),
            Some("query_send")
        );
        assert_eq!(
            response
                .headers()
                .get("x-upstream-service")
                .and_then(|value| value.to_str().ok()),
            Some("convex")
        );
        let body = to_bytes(response.into_body(), usize::MAX)
            .await
            .expect("body");
        let payload: Value = serde_json::from_slice(&body).expect("json");
        assert_eq!(payload["ok"], false);
        assert_eq!(payload["code"], "convex_unavailable");
        assert!(payload.get("schema").is_none());
        assert!(payload.get("result").is_none());
    }

    #[tokio::test]
    async fn document_shell_errors_without_convex_or_fixture() {
        let response = app_with_unreachable_convex_without_fixture()
            .oneshot(
                Request::builder()
                    .uri("/documents/doc_1?workspaceId=ws_demo")
                    .body(Body::empty())
                    .expect("request"),
            )
            .await
            .expect("response");

        if response.status() != StatusCode::SERVICE_UNAVAILABLE {
            let status = response.status();
            let body = to_bytes(response.into_body(), usize::MAX)
                .await
                .expect("body");
            panic!(
                "expected SERVICE_UNAVAILABLE, got {status}: {}",
                String::from_utf8_lossy(&body)
            );
        }
        assert_eq!(
            response
                .headers()
                .get("x-error-code")
                .and_then(|value| value.to_str().ok()),
            Some("convex_unavailable")
        );
        let body = to_bytes(response.into_body(), usize::MAX)
            .await
            .expect("body");
        let text = String::from_utf8(body.to_vec()).expect("utf8");
        let payload: Value = serde_json::from_str(&text).expect("json");
        assert_eq!(payload["ok"], false);
        assert_eq!(payload["code"], "convex_unavailable");
        assert!(!text.contains("mnote.page_aggregate.v1"));
        assert!(!text.contains("data-mnote-dev-fixture"));
        assert!(!text.contains("data-page-aggregate-snapshot"));
    }

    #[tokio::test]
    async fn page_aggregate_endpoint_returns_local_markdown_readonly_snapshot() {
        let root =
            std::env::temp_dir().join(format!("mnote-local-page-aggregate-{}", std::process::id()));
        let _ = std::fs::remove_dir_all(&root);
        std::fs::create_dir_all(root.join("Local Aggregate")).expect("create local page bundle");
        std::fs::write(
            root.join("Local Aggregate").join("Local Aggregate.md"),
            "# Local Heading\n正文内容\n",
        )
        .expect("write local md");

        let root_uri = format!("file://{}", root.display());
        init_local_workspace(&root, "user_test");
        let response = app()
            .oneshot(
                Request::builder()
                    .uri(format!(
                        "/api/page-aggregate/local-md:Local~20Aggregate~2FLocal~20Aggregate.md?sourceKind=local_folder&rootUri={root_uri}"
                    ))
                    .header("x-mnote-actor-id", "user_test")
                    .header("x-mnote-actor-type", "user")
                    .body(Body::empty())
                    .expect("request"),
            )
            .await
            .expect("response");

        let _ = std::fs::remove_dir_all(&root);

        assert_eq!(response.status(), StatusCode::OK);
        assert_eq!(
            response
                .headers()
                .get(header::CACHE_CONTROL)
                .and_then(|value| value.to_str().ok()),
            Some("no-store")
        );
        let body = to_bytes(response.into_body(), usize::MAX)
            .await
            .expect("body");
        let payload: Value = serde_json::from_slice(&body).expect("json");
        assert_eq!(payload["schema"], "mnote.page_aggregate.v1");
        assert_eq!(
            payload["result"]["identity"]["documentId"],
            "local-md:Local~20Aggregate~2FLocal~20Aggregate.md"
        );
        // 本地 Markdown 标题来自文件名；正文 H1 只作为正文内容。
        assert_eq!(payload["result"]["head"]["title"], "Local Aggregate");
        assert_eq!(payload["result"]["head"]["permissions"]["readOnly"], false);
        assert_eq!(payload["result"]["body"]["revision"], 0);
        assert!(payload["result"]["body"]["content"]
            .to_string()
            .contains("Local Heading"));
    }

    #[tokio::test]
    async fn page_aggregate_endpoint_allows_sqlite_granted_local_folder_read_access() {
        let root = std::env::temp_dir().join(format!(
            "mnote-local-page-aggregate-grant-{}",
            std::process::id()
        ));
        let _ = std::fs::remove_dir_all(&root);
        std::fs::create_dir_all(root.join("Grant Root")).expect("create local page bundle");
        std::fs::write(
            root.join("Grant Root").join("Grant Root.md"),
            "# Grant Heading\n授权正文\n",
        )
        .expect("write local md");

        let root_uri = format!("file://{}", root.display());
        let state = AppState::new(AppConfig {
            service_name: "mnote-web".into(),
            service_version: "0.1.0".into(),
            bind_addr: "127.0.0.1:0".into(),
            public_bind_addr: "127.0.0.1:3000".into(),
            legacy_next_base_url: Some("http://127.0.0.1:3100".into()),
            enable_legacy_next_compat: true,
            enable_debug_shell_routes: false,
            enable_editor_actor: true,
            hermes_base_path: "/api/hermes".into(),
            compat_next_base_path: "/api/compat/next".into(),
            convex_url: None,
            convex_admin_key: None,
            allow_dev_fixtures: true,
            query_fixtures_json: None,
            mutation_fixtures_json: None,
            dev_user_id: "dev-user".into(),
            dev_user_name: "开发用户".into(),
            dev_user_email: "dev@mnote.local".into(),
        });
        init_local_workspace(&root, "owner_user");
        grant_local_workspace_read_access(&state, "user_test", &root_uri, &root);

        let context = request_context("user_test", "user");
        let aggregate = super::build_page_aggregate_snapshot(
            &state,
            &context,
            "local-md:Grant~20Root~2FGrant~20Root.md",
            None,
            Some("local_folder"),
            Some(&root_uri),
        )
        .await
        .expect("sqlite read grant can open local page aggregate");

        let _ = std::fs::remove_dir_all(&root);

        assert_eq!(
            aggregate.identity.document_id,
            "local-md:Grant~20Root~2FGrant~20Root.md"
        );
        assert_eq!(aggregate.head.title, "Grant Heading");
    }

    #[tokio::test]
    async fn document_shell_renders_local_markdown_with_same_sidebar_surfaces() {
        let root =
            std::env::temp_dir().join(format!("mnote-local-document-shell-{}", std::process::id()));
        let _ = std::fs::remove_dir_all(&root);
        std::fs::create_dir_all(root.join("Local Shell")).expect("create local page bundle");
        std::fs::create_dir_all(root.join("docs").join("Child Page"))
            .expect("create local child bundle");
        std::fs::write(
            root.join("Local Shell").join("Local Shell.md"),
            "# Local Shell\n正文\n",
        )
        .expect("write root md");
        std::fs::write(
            root.join("docs").join("Child Page").join("Child Page.md"),
            "# Child Page\n",
        )
        .expect("write child md");
        std::fs::write(root.join("asset.png"), b"png").expect("write asset");

        let root_uri = format!("file://{}", root.display());
        init_local_workspace(&root, "user_test");
        let response = app()
            .oneshot(
                Request::builder()
                    .uri(format!(
                        "/documents/local-md:Local~20Shell~2FLocal~20Shell.md?sourceKind=local_folder&rootUri={root_uri}&treeView=filetree"
                    ))
                    .header("x-mnote-actor-id", "user_test")
                    .header("x-mnote-actor-type", "user")
                    .body(Body::empty())
                    .expect("request"),
            )
            .await
            .expect("response");

        let _ = std::fs::remove_dir_all(&root);

        assert_eq!(response.status(), StatusCode::OK);
        assert_eq!(
            response
                .headers()
                .get(header::CACHE_CONTROL)
                .and_then(|value| value.to_str().ok()),
            Some("no-store")
        );
        let body = to_bytes(response.into_body(), usize::MAX)
            .await
            .expect("body");
        let html = String::from_utf8(body.to_vec()).expect("html");
        assert!(html.contains("Local Shell"));
        assert!(html.contains("data-page-aggregate-snapshot=\"mnote.page_aggregate.v1\""));
        assert!(html.contains("\"saveEndpoint\":\"/api/page-body/write\""));
        assert!(html.contains("Child Page"));
        assert!(html.contains("asset.png"));
        assert!(html.contains("data-row-kind=\"markdown\""));
        assert!(html.contains("data-page-openable=\"false\""));
        assert!(html.contains("fileAction === 'open' && rowKind === 'folder'"));
        assert!(html.contains("openTrigger.getAttribute('data-page-openable') === 'false'"));
        assert!(html.contains("data-mnote-action=\"open-local-folder\""));
        assert!(html.contains("refreshSessionFromExternalFileChange"));
        assert!(html.contains("refreshSessionFromExternalChange"));
        assert!(html.contains("treeExternalConflictMessage"));
        assert!(html.contains("tree:delta"));
        assert!(html.contains("tree:resync"));
        assert!(html.contains("mnote-web-tree-live"));
        assert!(html.contains("refreshMindmapRuntimesFromTreePayload"));
        assert!(html.contains("__MNOTE_LEPTOS_MINDMAP_BRIDGES__"));
        assert!(html.contains("/api/local-folder/events"));
        assert!(html.contains("localFolderEventChannelKey"));
        assert!(html.contains("url.searchParams.set('documentId', session.documentId);"));
        assert!(html.contains("if (!documentId) return;"));
        assert!(html.contains("new EventSource(url.toString())"));
        assert!(html.contains("localFolderEventRegistry"));
        assert!(html
            .contains("if (!response.ok) {\n        if (session.sourceKind === 'local_folder')"));
        assert!(html.contains("shouldSuppressLocalFolderSelfChange"));
        assert!(html.contains("kind.includes('Modify(Name')"));
        assert!(html.contains("targetSession.views.size === 0"));
        assert!(html.contains("session.views.size === 0"));
        assert!(html.contains("targetSession.saving"));
        assert!(html.contains("lastSelfSaveSignalAt"));
        assert!(html.contains("clearSessionConflictSurface(view.session);"));
        assert!(html.contains("command: 'replaceContent'"));
        assert!(html.contains("external-change-conflict"));
        assert!(html.contains("mnote-editor-conflict-panel"));
        assert!(html.contains("mnote-conflict-accept-disk"));
        assert!(html.contains("mnote-conflict-keep-current"));
        assert!(html.contains("mnote-conflict-open-diff"));
        assert!(html.contains("mnote-conflict-merge-text"));
        assert!(html.contains("mnote-conflict-merge-save"));
        assert!(html.contains("agent run"));
        assert!(!html.contains(
            "setInterval(() => {\n        void pollLocalMarkdownExternalChange();\n      }, 1200);"
        ));
    }

    #[test]
    fn document_conflict_panel_runtime_contains_dom_helpers() {
        const DOCUMENT_CONFLICT_PANEL_RUNTIME_JS: &str =
            include_str!("../../browser/document-conflict-panel-runtime.js");
        assert!(DOCUMENT_CONFLICT_PANEL_RUNTIME_JS.contains("function clearSessionConflictSurface"));
        assert!(DOCUMENT_CONFLICT_PANEL_RUNTIME_JS.contains("function createSessionConflictPanel"));
        assert!(DOCUMENT_CONFLICT_PANEL_RUNTIME_JS
            .contains("data-testid', 'mnote-editor-conflict-panel"));
        assert!(DOCUMENT_CONFLICT_PANEL_RUNTIME_JS
            .contains("data-testid', 'mnote-conflict-accept-disk"));
        assert!(
            DOCUMENT_CONFLICT_PANEL_RUNTIME_JS.contains("data-testid', 'mnote-conflict-open-diff")
        );
        assert!(DOCUMENT_CONFLICT_PANEL_RUNTIME_JS.contains("onAcceptDisk"));
        assert!(DOCUMENT_CONFLICT_PANEL_RUNTIME_JS.contains("onKeepCurrent"));
        assert!(DOCUMENT_CONFLICT_PANEL_RUNTIME_JS.contains("onOpenDiff"));
        assert!(DOCUMENT_CONFLICT_PANEL_RUNTIME_JS
            .contains("function populateSessionConflictDiffPanel"));
        assert!(DOCUMENT_CONFLICT_PANEL_RUNTIME_JS
            .contains("data-testid', 'mnote-conflict-current-text"));
        assert!(
            DOCUMENT_CONFLICT_PANEL_RUNTIME_JS.contains("data-testid', 'mnote-conflict-disk-text")
        );
        assert!(DOCUMENT_CONFLICT_PANEL_RUNTIME_JS
            .contains("data-testid', 'mnote-conflict-merge-use-current"));
        assert!(DOCUMENT_CONFLICT_PANEL_RUNTIME_JS
            .contains("data-testid', 'mnote-conflict-merge-use-disk"));
        assert!(DOCUMENT_CONFLICT_PANEL_RUNTIME_JS.contains("currentMergeText"));
        assert!(DOCUMENT_CONFLICT_PANEL_RUNTIME_JS.contains("onUseCurrent"));
        assert!(DOCUMENT_CONFLICT_PANEL_RUNTIME_JS.contains("onUseDisk"));
        assert!(DOCUMENT_CONFLICT_PANEL_RUNTIME_JS.contains("onSaveMerge"));
        assert!(DOCUMENT_CONFLICT_PANEL_RUNTIME_JS.contains("function runSessionConflictAction"));
        assert!(DOCUMENT_CONFLICT_PANEL_RUNTIME_JS
            .contains("runSessionConflictAction: runSessionConflictAction"));
    }

    #[tokio::test]
    async fn sidebar_and_filetree_do_not_return_dev_fixtures_by_default() {
        let config = AppConfig {
            service_name: "mnote-web".into(),
            service_version: "0.1.0".into(),
            bind_addr: "127.0.0.1:0".into(),
            public_bind_addr: "127.0.0.1:3000".into(),
            legacy_next_base_url: Some("http://127.0.0.1:3100".into()),
            enable_legacy_next_compat: true,
            enable_debug_shell_routes: false,
            enable_editor_actor: true,
            hermes_base_path: "/api/hermes".into(),
            compat_next_base_path: "/api/compat/next".into(),
            convex_url: None,
            convex_admin_key: None,
            allow_dev_fixtures: false,
            query_fixtures_json: None,
            mutation_fixtures_json: None,
            dev_user_id: "dev-user".into(),
            dev_user_name: "开发用户".into(),
            dev_user_email: "dev@mnote.local".into(),
        };
        let headers = HeaderMap::new();
        let context = RequestContext::from_http_parts(
            &Method::GET,
            &"/documents/doc_1?workspaceId=ws_demo"
                .parse::<Uri>()
                .expect("uri"),
            &headers,
        );

        let sidebar_html =
            super::load_sidebar_tree_html(&config, &context, "ws_demo", Some("doc_1")).await;
        let filetree_html =
            super::load_file_tree_html(&config, &context, "ws_demo", Some("doc_1"), None).await;
        let workspace_projection = super::load_workspace_shell_projection(
            &config,
            &context,
            "ws_demo",
            Some("doc_1"),
            "个人空间",
        )
        .await;

        assert!(sidebar_html.is_none());
        assert!(filetree_html.is_none());
        assert!(workspace_projection.degraded);
        assert!(workspace_projection.my_page_items.is_empty());
        assert!(!workspace_projection.dev_fixture);
    }

    #[tokio::test]
    async fn document_shell_renders_local_markdown_attachment_name_in_html() {
        let root = std::env::temp_dir().join(format!(
            "mnote-local-document-shell-media-{}",
            std::process::id()
        ));
        let _ = std::fs::remove_dir_all(&root);
        std::fs::create_dir_all(root.join("docs")).expect("create local docs");
        std::fs::write(
            root.join("docs").join("blocks.md"),
            "---\ntitle: Complex Title\n---\n[Spec](assets/spec.pdf)\n",
        )
        .expect("write local md");

        let root_uri = format!("file://{}", root.display());
        init_local_workspace(&root, "user_test");
        let response = app()
            .oneshot(
                Request::builder()
                    .uri(format!(
                        "/documents/local-md:docs~2Fblocks.md?sourceKind=local_folder&rootUri={root_uri}"
                    ))
                    .header("x-mnote-actor-id", "user_test")
                    .header("x-mnote-actor-type", "user")
                    .body(Body::empty())
                    .expect("request"),
            )
            .await
            .expect("response");

        let _ = std::fs::remove_dir_all(&root);

        assert_eq!(response.status(), StatusCode::OK);
        let body = to_bytes(response.into_body(), usize::MAX)
            .await
            .expect("body");
        let html = String::from_utf8(body.to_vec()).expect("html");
        assert!(html.contains("Spec"));
        assert!(html.contains("assets/spec.pdf"));
        assert!(html.contains(r#"data-mnote-source-kind="local_folder""#));
        assert!(html.contains(r#"data-mnote-root-uri="file://"#));
    }

    #[tokio::test]
    async fn document_shell_renders_secondary_pane_contract_when_query_present() {
        let response = app()
            .oneshot(
                Request::builder()
                    .uri("/documents/doc_1?workspaceId=ws_demo&secondaryDocumentId=doc_1")
                    .body(Body::empty())
                    .expect("request"),
            )
            .await
            .expect("response");

        assert_eq!(response.status(), StatusCode::OK);
        let body = to_bytes(response.into_body(), usize::MAX)
            .await
            .expect("body");
        let html = String::from_utf8(body.to_vec()).expect("html");
        assert!(html.contains("data-has-secondary-pane=\"true\""));
        assert!(html.contains("data-pane-role=\"secondary\""));
        assert!(html.contains("data-mnote-pane-close=\"secondary\""));
        assert!(html.contains("__MNOTE_SECONDARY_PAGE_AGGREGATE__"));
        assert!(html.contains("__MNOTE_SECONDARY_EDITOR_BOOTSTRAP__"));
        assert!(html.contains("\"paneRole\":\"secondary\""));
        assert!(html.contains("\"secondaryRequested\":true"));
        assert!(html.contains("\"secondaryInvalid\":false"));
    }

    #[tokio::test]
    async fn document_shell_bootstrap_preserves_inline_mark_conversion() {
        let response = app()
            .oneshot(
                Request::builder()
                    .uri("/documents/doc_1?workspaceId=ws_demo")
                    .body(Body::empty())
                    .expect("request"),
            )
            .await
            .expect("response");

        assert_eq!(response.status(), StatusCode::OK);
        let body = to_bytes(response.into_body(), usize::MAX)
            .await
            .expect("body");
        let html = String::from_utf8(body.to_vec()).expect("html");
        assert!(html.contains("legacyInlineContentToTiptap"));
        assert!(html.contains("legacyStylesToTiptapMarks"));
        assert!(html.contains("legacyMarkArrayToTiptapMarks"));
        assert!(html.contains("firstNonEmptyText(block?.props?.sourcePath"));
        assert!(html.contains("marks.push({ type: 'bold' })"));
        assert!(html.contains("marks.push({ type: 'italic' })"));
        assert!(html.contains("marks.push({ type: 'underline' })"));
        assert!(html.contains("marks.push({ type: 'strike' })"));
        assert!(html.contains("marks.push({ type: 'code' })"));
        assert!(html.contains("marks.push({ type: 'link', attrs: { href } })"));
        assert!(html.contains("styles.link = href"));
        assert!(html.contains("contentNodes.map((node) => {"));
        assert!(html.contains("payload: { type: 'text', text"));
        assert!(html.contains("typeof payload.text === 'string'"));
        assert!(html.contains("payload.type === 'hard_break'"));
        assert!(html.contains("typeof body?.fileVersion === 'string'"));
        assert!(html.contains("expectedFileVersion: session.conflictDetectionKey"));
        assert!(html.contains("node?.attrs?.mnoteBlockType === 'mindmap'"));
        assert!(html.contains("blockType: 'mindmap'"));
        assert!(html.contains("...mindmapPropsFromAttrs(node?.attrs, blockId)"));
        assert!(html.contains("mindmapPropsFromAttrs(block.props || {}, block.blockId)"));
        assert!(html.contains("mnoteBlockType: 'mindmap'"));
        assert!(html.contains("block.blockType === 'mindmap'"));
        assert!(html.contains("content: block.blockType === 'mindmap'"));
        assert!(html.contains("? ''"));
        assert!(!html.contains(
            "block.contentNodes.map((node) => typeof node?.text === 'string' ? node.text : '').join('')"
        ));
    }
}
