use crate::app::AppState;
use crate::context::RequestContext;
use crate::error::WebError;
use crate::routes::command_support::{
    build_tree_target, ensure_non_empty, ensure_sort_order,
    execute_runtime_command_via_convex_with_artifacts, read_optional_non_empty,
};
use crate::routes::local_folder_source::{
    execute_local_tree_command, load_local_folder_file_tree_snapshot,
    load_local_folder_page_tree_snapshot, local_folder_watch_revision,
    local_workspace_id_from_root_uri,
};
use crate::routes::query_support::{
    fetch_documents_meta_via_convex, resolve_effective_workspace_id,
};
use crate::routes::snapshot_support::{load_projection_snapshot, ProjectionSnapshotSpec};
use crate::transport::convex::execute_convex_mutation_by_name;
use crate::tree_shell::filetree_renderer::{
    render_initial_filetree_html, FileTreeInitialRenderInput, FileTreeRenderRow,
};
use crate::tree_shell::filetree_selection::FileTreeSelectionState;
use crate::tree_shell::page_renderer::{
    render_initial_page_tree_html, PageTreeInitialRenderInput, PageTreeRenderRow,
};
use crate::tree_shell::picker_renderer::{
    render_initial_picker_html, PickerInitialRenderInput, PickerRenderRow,
};
use crate::tree_shell::renderer_input::{
    FileTreeRendererInput, PageTreeRendererInput, PickerRendererInput, TreeShellCommandDispatcher,
    TreeShellRendererInput,
};
use crate::tree_shell::runtime_api::{
    reduce_tree_shell_runtime as reduce_tree_shell_runtime_request, TreeShellRuntimeRequest,
    TreeShellRuntimeResult,
};
use axum::extract::{Extension, Query, State};
use axum::http::{header, HeaderValue, StatusCode};
use axum::response::{Html, IntoResponse, Response};
use axum::Json;
use bridge_runtime::RuntimeCommandEnvelopeWire;
use core_protocol::KernelProjectionKind;
use serde::Deserialize;
use serde_json::{json, Value};
use std::collections::{BTreeMap, BTreeSet};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};

static TREE_DOCUMENT_COUNTER: AtomicU64 = AtomicU64::new(1);

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TreeShellQuery {
    pub workspace_id: Option<String>,
    pub root_node_id: Option<String>,
    pub depth: Option<u32>,
    pub active_document_id: Option<String>,
    pub focused_document_id: Option<String>,
    pub active_picker_item_key: Option<String>,
    pub actor_id: Option<String>,
    pub channel: Option<String>,
    pub host: Option<String>,
    pub mode: Option<String>,
    pub source_kind: Option<String>,
    pub root_uri: Option<String>,
    pub allow_root_pick: Option<String>,
    pub exclude_ids: Option<String>,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LocalFolderWatchQuery {
    pub root_uri: String,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TreeCommandEnvelope {
    pub action: String,
    pub workspace_id: Option<String>,
    pub source_kind: Option<String>,
    pub root_uri: Option<String>,
    #[serde(default)]
    pub source_capabilities: Vec<String>,
    pub target_node_id: Option<String>,
    pub target_resource_meta: Option<Value>,
    pub selection: Option<Value>,
    pub operation: Option<String>,
    pub document_id: Option<String>,
    pub parent_id: Option<String>,
    pub target_parent_id: Option<String>,
    pub title: Option<String>,
    pub access_scope: Option<String>,
    pub content: Option<Value>,
    pub sort_order: Option<i64>,
    #[serde(default)]
    pub items: Vec<TreeCommandCopyItem>,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TreeCommandCopyItem {
    pub document_id: String,
    #[serde(default)]
    pub recursive: bool,
}

#[derive(Debug, Clone, Default)]
pub struct TreeCommandEnvelopeContext {
    pub source_kind: Option<String>,
    pub root_uri: Option<String>,
    pub source_capabilities: Vec<String>,
    pub target_node_id: Option<String>,
    pub target_resource_meta: Option<Value>,
    pub selection: Option<Value>,
    pub operation: Option<String>,
}

impl TreeCommandEnvelopeContext {
    fn from_envelope(envelope: &TreeCommandEnvelope) -> Self {
        Self {
            source_kind: read_optional_non_empty(envelope.source_kind.clone()),
            root_uri: read_optional_non_empty(envelope.root_uri.clone()),
            source_capabilities: envelope
                .source_capabilities
                .iter()
                .filter_map(|capability| read_optional_non_empty(Some(capability.clone())))
                .collect(),
            target_node_id: read_optional_non_empty(envelope.target_node_id.clone()),
            target_resource_meta: envelope.target_resource_meta.clone(),
            selection: envelope.selection.clone(),
            operation: read_optional_non_empty(envelope.operation.clone()),
        }
    }
}

#[derive(Debug)]
pub enum TreeCommandRequest {
    Create {
        workspace_id: Option<String>,
        document_id: String,
        parent_id: Option<String>,
        title: String,
        access_scope: Option<String>,
        content: Option<Value>,
    },
    CreateFolder {
        workspace_id: Option<String>,
        document_id: String,
        parent_id: Option<String>,
        title: String,
    },
    Rename {
        workspace_id: Option<String>,
        document_id: String,
        title: String,
    },
    Move {
        workspace_id: Option<String>,
        document_id: String,
        parent_id: Option<String>,
        sort_order: i64,
    },
    Archive {
        workspace_id: Option<String>,
        document_id: String,
    },
    Restore {
        workspace_id: Option<String>,
        document_id: String,
    },
    Copy {
        workspace_id: Option<String>,
        document_id: String,
        target_parent_id: Option<String>,
        items: Vec<TreeCommandCopyItem>,
        title: Option<String>,
    },
    DropFiles {
        workspace_id: Option<String>,
        parent_id: Option<String>,
        files_json: String,
    },
    Purge {
        workspace_id: Option<String>,
        document_id: String,
    },
}

fn escape_html(input: &str) -> String {
    input
        .replace('&', "&amp;")
        .replace('<', "&lt;")
        .replace('>', "&gt;")
        .replace('"', "&quot;")
        .replace('\'', "&#39;")
}

fn escape_inline_json(input: &str) -> String {
    input
        .replace('&', "\\u0026")
        .replace('<', "\\u003c")
        .replace('>', "\\u003e")
}

fn normalize_title(value: Option<String>) -> String {
    let trimmed = value.unwrap_or_default().trim().to_string();
    if trimmed.is_empty() {
        "无标题".into()
    } else {
        trimmed
    }
}

fn normalize_channel(value: Option<String>) -> String {
    let trimmed = value.unwrap_or_default().trim().to_string();
    if trimmed.is_empty() {
        "mnote-tree-shell-v1".into()
    } else {
        trimmed
    }
}

fn normalize_tree_mode(value: Option<&str>) -> &'static str {
    match value.unwrap_or_default().trim() {
        "picker" => "picker",
        "filetree" => "filetree",
        _ => "page",
    }
}

fn normalize_bool_flag(value: Option<&str>, default: bool) -> bool {
    match value
        .unwrap_or_default()
        .trim()
        .to_ascii_lowercase()
        .as_str()
    {
        "1" | "true" | "yes" | "on" => true,
        "0" | "false" | "no" | "off" => false,
        _ => default,
    }
}

fn build_workspace_source_wire(
    context: &RequestContext,
    workspace_id: &str,
    envelope_context: &TreeCommandEnvelopeContext,
) -> bridge_runtime::RuntimeSourceWire {
    let source_kind = envelope_context
        .source_kind
        .clone()
        .unwrap_or_else(|| "convex_workspace".into());
    let root_uri = envelope_context.root_uri.clone().or_else(|| {
        if source_kind == "convex_workspace" {
            Some(format!("convex://workspace/{workspace_id}"))
        } else {
            None
        }
    });
    let capabilities =
        if envelope_context.source_capabilities.is_empty() && source_kind == "convex_workspace" {
            vec![
                "load-snapshot".into(),
                "preflight-command".into(),
                "execute-command".into(),
                "resolve-page-aggregate".into(),
            ]
        } else {
            envelope_context.source_capabilities.clone()
        };

    bridge_runtime::RuntimeSourceWire {
        channel: context.source.channel.clone(),
        client: context.source.client.clone(),
        source_kind: Some(source_kind),
        root_uri,
        workspace_id: Some(workspace_id.to_string()),
        capabilities,
    }
}

fn attach_tree_command_envelope_context(
    mut payload: Value,
    envelope_context: &TreeCommandEnvelopeContext,
) -> Value {
    if let Some(map) = payload.as_object_mut() {
        if let Some(target_node_id) = envelope_context.target_node_id.as_ref() {
            map.insert("targetNodeId".into(), json!(target_node_id));
        }
        if let Some(target_resource_meta) = envelope_context.target_resource_meta.as_ref() {
            map.insert("targetResourceMeta".into(), target_resource_meta.clone());
        }
        if let Some(selection) = envelope_context.selection.as_ref() {
            map.insert("selection".into(), selection.clone());
        }
        if let Some(operation) = envelope_context.operation.as_ref() {
            map.insert("operation".into(), json!(operation));
        }
    }
    payload
}

fn parse_exclude_ids(value: Option<&str>) -> Vec<String> {
    value
        .unwrap_or_default()
        .split(',')
        .map(str::trim)
        .filter(|item| !item.is_empty())
        .map(ToOwned::to_owned)
        .collect()
}

fn collect_projection_item_ids(projection: &Value) -> Vec<String> {
    projection
        .get("items")
        .and_then(Value::as_array)
        .map(|items| {
            items
                .iter()
                .filter_map(|item| {
                    item.get("rowId")
                        .and_then(Value::as_str)
                        .or_else(|| item.get("nodeId").and_then(Value::as_str))
                        .map(str::trim)
                        .filter(|value| !value.is_empty())
                        .map(ToOwned::to_owned)
                })
                .collect()
        })
        .unwrap_or_default()
}

fn collect_expanded_ids(projection: &Value) -> BTreeSet<String> {
    projection
        .get("items")
        .and_then(Value::as_array)
        .map(|items| {
            items
                .iter()
                .filter(|item| {
                    item.get("expandedByDefault")
                        .and_then(Value::as_bool)
                        .unwrap_or(false)
                })
                .filter_map(|item| {
                    item.get("nodeId")
                        .and_then(Value::as_str)
                        .map(str::trim)
                        .filter(|value| !value.is_empty())
                        .map(ToOwned::to_owned)
                })
                .collect()
        })
        .unwrap_or_default()
}

pub(crate) fn collect_page_tree_render_rows(projection: &Value) -> Vec<PageTreeRenderRow> {
    let mut rows: Vec<PageTreeRenderRow> = projection
        .get("items")
        .and_then(Value::as_array)
        .map(|items| {
            items
                .iter()
                .filter_map(|item| {
                    let node_id = item
                        .get("nodeId")
                        .and_then(Value::as_str)
                        .map(str::trim)
                        .filter(|value| !value.is_empty())?;
                    let row_kind = item
                        .get("rowKind")
                        .and_then(Value::as_str)
                        .unwrap_or_default();
                    if row_kind != "document" {
                        return None;
                    }
                    Some(PageTreeRenderRow {
                        node_id: node_id.to_string(),
                        parent_node_id: item
                            .get("parentNodeId")
                            .or_else(|| item.get("parentId"))
                            .and_then(Value::as_str)
                            .map(str::trim)
                            .filter(|value| !value.is_empty())
                            .map(ToOwned::to_owned),
                        title: item
                            .get("title")
                            .and_then(Value::as_str)
                            .map(str::trim)
                            .filter(|value| !value.is_empty())
                            .unwrap_or("无标题")
                            .to_string(),
                        depth: item.get("depth").and_then(Value::as_u64).unwrap_or(0) as u32,
                        expandable: item
                            .get("expandable")
                            .and_then(Value::as_bool)
                            .unwrap_or_else(|| {
                                item.get("childCount")
                                    .and_then(Value::as_u64)
                                    .map(|count| count > 0)
                                    .unwrap_or(false)
                            }),
                        expanded: item
                            .get("expandedByDefault")
                            .and_then(Value::as_bool)
                            .unwrap_or(false),
                        openable: item
                            .get("resourceMeta")
                            .and_then(Value::as_object)
                            .and_then(|meta| meta.get("documentId"))
                            .and_then(Value::as_str)
                            .map(str::trim)
                            .filter(|value| !value.is_empty())
                            .is_some()
                            || !node_id.starts_with("local-dir:"),
                    })
                })
                .collect()
        })
        .unwrap_or_default();

    let parent_by_id = rows
        .iter()
        .map(|row| (row.node_id.clone(), row.parent_node_id.clone()))
        .collect::<BTreeMap<_, _>>();
    for row in &mut rows {
        if row.depth == 0 && row.parent_node_id.is_some() {
            let mut depth = 0_u32;
            let mut cursor = row.parent_node_id.as_deref();
            while let Some(parent_id) = cursor {
                depth += 1;
                cursor = parent_by_id
                    .get(parent_id)
                    .and_then(|parent| parent.as_deref());
                if depth > 32 {
                    break;
                }
            }
            row.depth = depth;
        }
    }

    rows
}

pub(crate) fn collect_filetree_render_rows(
    projection: &Value,
    active_document_id: Option<&str>,
) -> Vec<FileTreeRenderRow> {
    let active_document_id = active_document_id
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .map(ToOwned::to_owned);
    let selected_ids = active_document_id
        .as_deref()
        .map(|document_id| BTreeSet::from([format!("index:{document_id}")]))
        .unwrap_or_default();

    projection
        .get("items")
        .and_then(Value::as_array)
        .map(|items| {
            items
                .iter()
                .filter_map(|item| {
                    let row_id = item
                        .get("rowId")
                        .and_then(Value::as_str)
                        .map(str::trim)
                        .filter(|value| !value.is_empty())?;
                    let node_id = item
                        .get("nodeId")
                        .and_then(Value::as_str)
                        .map(str::trim)
                        .filter(|value| !value.is_empty())?;
                    let resource_meta = item.get("resourceMeta").and_then(Value::as_object);
                    let document_id = resource_meta
                        .and_then(|meta| meta.get("documentId"))
                        .and_then(Value::as_str)
                        .map(str::trim)
                        .filter(|value| !value.is_empty())
                        .map(ToOwned::to_owned);
                    let selected = selected_ids.contains(row_id)
                        || active_document_id
                            .as_deref()
                            .is_some_and(|active_id| document_id.as_deref() == Some(active_id));
                    Some(FileTreeRenderRow {
                        row_id: row_id.to_string(),
                        row_kind: item
                            .get("rowKind")
                            .and_then(Value::as_str)
                            .map(str::trim)
                            .filter(|value| !value.is_empty())
                            .unwrap_or("document")
                            .to_string(),
                        node_id: node_id.to_string(),
                        parent_node_id: item
                            .get("parentNodeId")
                            .and_then(Value::as_str)
                            .map(str::trim)
                            .filter(|value| !value.is_empty())
                            .map(ToOwned::to_owned),
                        title: item
                            .get("title")
                            .and_then(Value::as_str)
                            .map(str::trim)
                            .filter(|value| !value.is_empty())
                            .unwrap_or("无标题")
                            .to_string(),
                        depth: item.get("depth").and_then(Value::as_u64).unwrap_or(0) as u32,
                        expandable: item
                            .get("expandable")
                            .and_then(Value::as_bool)
                            .unwrap_or_else(|| {
                                item.get("childCount")
                                    .and_then(Value::as_u64)
                                    .map(|count| count > 0)
                                    .unwrap_or(false)
                            }),
                        expanded: item
                            .get("expandedByDefault")
                            .and_then(Value::as_bool)
                            .unwrap_or(false),
                        icon_kind: item
                            .get("iconHint")
                            .and_then(Value::as_str)
                            .or_else(|| {
                                resource_meta
                                    .and_then(|meta| meta.get("iconHint"))
                                    .and_then(Value::as_str)
                            })
                            .map(str::trim)
                            .filter(|value| !value.is_empty())
                            .unwrap_or("file")
                            .to_string(),
                        document_id,
                        asset_id: resource_meta
                            .and_then(|meta| meta.get("assetId"))
                            .and_then(Value::as_str)
                            .map(str::trim)
                            .filter(|value| !value.is_empty())
                            .map(ToOwned::to_owned),
                        object_identity: resource_meta
                            .and_then(|meta| meta.get("objectIdentity"))
                            .and_then(|value| serde_json::to_string(value).ok()),
                        selected,
                    })
                })
                .collect()
        })
        .unwrap_or_default()
}

fn collect_picker_render_rows(
    projection: &Value,
    active_picker_item_key: Option<&str>,
    active_document_id: Option<&str>,
    exclude_ids: &[String],
) -> Vec<PickerRenderRow> {
    let active_key = active_picker_item_key
        .or(active_document_id)
        .map(str::trim)
        .filter(|value| !value.is_empty());
    let excluded_ids = exclude_ids.iter().cloned().collect::<BTreeSet<_>>();

    projection
        .get("items")
        .and_then(Value::as_array)
        .map(|items| {
            items
                .iter()
                .filter_map(|item| {
                    let node_id = item
                        .get("nodeId")
                        .and_then(Value::as_str)
                        .map(str::trim)
                        .filter(|value| !value.is_empty())?;
                    if excluded_ids.contains(node_id) {
                        return None;
                    }
                    let row_kind = item
                        .get("rowKind")
                        .and_then(Value::as_str)
                        .unwrap_or_default();
                    if row_kind != "document" {
                        return None;
                    }
                    Some(PickerRenderRow {
                        node_id: node_id.to_string(),
                        parent_node_id: item
                            .get("parentNodeId")
                            .and_then(Value::as_str)
                            .map(str::trim)
                            .filter(|value| !value.is_empty())
                            .map(ToOwned::to_owned),
                        title: item
                            .get("title")
                            .and_then(Value::as_str)
                            .map(str::trim)
                            .filter(|value| !value.is_empty())
                            .unwrap_or("无标题")
                            .to_string(),
                        depth: item.get("depth").and_then(Value::as_u64).unwrap_or(0) as u32,
                        expandable: item
                            .get("expandable")
                            .and_then(Value::as_bool)
                            .unwrap_or_else(|| {
                                item.get("childCount")
                                    .and_then(Value::as_u64)
                                    .map(|count| count > 0)
                                    .unwrap_or(false)
                            }),
                        expanded: item
                            .get("expandedByDefault")
                            .and_then(Value::as_bool)
                            .unwrap_or(false),
                        active: active_key
                            .map(|active_key| active_key == node_id)
                            .unwrap_or(false),
                    })
                })
                .collect()
        })
        .unwrap_or_default()
}

fn build_tree_shell_command_dispatcher(channel: &str) -> TreeShellCommandDispatcher {
    TreeShellCommandDispatcher {
        channel: channel.into(),
        command_names: [
            "tree.node.create",
            "tree.node.rename",
            "tree.subtree.move",
            "tree.resource.copy",
            "tree.resource.move",
            "tree.resource.upload",
        ]
        .into_iter()
        .map(ToOwned::to_owned)
        .collect(),
    }
}

fn build_tree_shell_renderer_input(
    projection: &Value,
    mode: &str,
    channel: &str,
    active_document_id: Option<&str>,
    focused_document_id: Option<&str>,
    active_picker_item_key: Option<&str>,
    exclude_ids: &[String],
) -> TreeShellRendererInput {
    let projection_item_ids = collect_projection_item_ids(projection);
    let expanded_ids = collect_expanded_ids(projection);
    let command_dispatcher = build_tree_shell_command_dispatcher(channel);
    match mode {
        "filetree" => {
            let selection = active_document_id
                .map(str::trim)
                .filter(|value| !value.is_empty())
                .map(|document_id| {
                    FileTreeSelectionState::from_selected(&[format!("index:{document_id}")])
                })
                .unwrap_or_default();
            TreeShellRendererInput::filetree(FileTreeRendererInput {
                projection_item_ids,
                expanded_ids,
                filetree_selection: selection,
                command_dispatcher,
            })
        }
        "picker" => TreeShellRendererInput::picker(PickerRendererInput {
            projection_item_ids,
            expanded_ids,
            active_picker_item: active_picker_item_key
                .map(str::trim)
                .filter(|value| !value.is_empty())
                .map(ToOwned::to_owned),
            excluded_picker_ids: exclude_ids.iter().cloned().collect(),
            command_dispatcher,
        }),
        _ => TreeShellRendererInput::page(PageTreeRendererInput {
            projection_item_ids,
            expanded_ids,
            focused_id: focused_document_id
                .or(active_document_id)
                .map(str::trim)
                .filter(|value| !value.is_empty())
                .map(ToOwned::to_owned),
            command_dispatcher,
        }),
    }
}

fn override_actor_context(context: &RequestContext, actor_id: Option<&str>) -> RequestContext {
    let mut next = context.clone();
    let actor_id = actor_id
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .map(ToOwned::to_owned);
    if let Some(actor_id) = actor_id {
        next.auth.actor_id = actor_id;
        next.auth.actor_type = "user".into();
    }
    next
}

fn generate_tree_document_id() -> String {
    let millis = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_millis();
    let counter = TREE_DOCUMENT_COUNTER.fetch_add(1, Ordering::Relaxed);
    format!("tree_{millis}_{counter}")
}

fn build_tree_shell_html(
    workspace_id: &str,
    root_node_id: Option<&str>,
    active_document_id: Option<&str>,
    focused_document_id: Option<&str>,
    active_picker_item_key: Option<&str>,
    channel: &str,
    host: Option<&str>,
    context: &RequestContext,
    projection: &Value,
    mode: &str,
    allow_root_pick: bool,
    exclude_ids: &[String],
    dataset: &Value,
) -> String {
    let renderer_input = build_tree_shell_renderer_input(
        projection,
        mode,
        channel,
        active_document_id,
        focused_document_id,
        active_picker_item_key,
        exclude_ids,
    );
    let app_state = json!({
        "workspaceId": workspace_id,
        "rootNodeId": root_node_id,
        "activeDocumentId": active_document_id,
        "focusedDocumentId": focused_document_id,
        "activePickerItemKey": active_picker_item_key,
        "actorId": context.auth.actor_id,
        "channel": channel,
        "host": host,
        "mode": mode,
        "sourceKind": projection
            .get("sourceKind")
            .and_then(Value::as_str)
            .unwrap_or("convex_workspace"),
        "rootUri": projection
            .get("rootUri")
            .and_then(Value::as_str)
            .unwrap_or(""),
        "localWatchRevision": projection.get("watchRevision").cloned().unwrap_or(Value::Null),
        "allowRootPick": allow_root_pick,
        "excludeIds": exclude_ids,
        "rendererInput": renderer_input,
        "requestId": context.trace.request_id,
        "traceId": context.trace.trace_id,
        "commandPath": "/api/tree/commands",
        "items": projection.get("items").cloned().unwrap_or_else(|| Value::Array(Vec::new())),
        "mediaAssets": dataset.get("media_assets").cloned().unwrap_or_else(|| Value::Array(Vec::new())),
        "mindmapAssets": dataset.get("mindmap_assets").cloned().unwrap_or_else(|| Value::Array(Vec::new())),
        "tableAssets": dataset.get("table_assets").cloned().unwrap_or_else(|| Value::Array(Vec::new())),
        "mindmapAssetChildren": dataset.get("mindmap_asset_children").cloned().unwrap_or_else(|| json!({})),
    });
    let app_state_json = serde_json::to_string(&app_state).unwrap_or_else(|_| "{}".into());
    let projection_json = serde_json::to_string_pretty(projection).unwrap_or_else(|_| "{}".into());
    let initial_tree_html = match mode {
        "page" => render_initial_page_tree_html(&PageTreeInitialRenderInput {
            rows: collect_page_tree_render_rows(projection),
            active_node_id: active_document_id
                .map(str::trim)
                .filter(|value| !value.is_empty())
                .map(ToOwned::to_owned),
            focused_node_id: focused_document_id
                .or(active_document_id)
                .map(str::trim)
                .filter(|value| !value.is_empty())
                .map(ToOwned::to_owned),
        }),
        "filetree" => render_initial_filetree_html(&FileTreeInitialRenderInput {
            rows: collect_filetree_render_rows(projection, active_document_id),
        }),
        "picker" => render_initial_picker_html(&PickerInitialRenderInput {
            rows: collect_picker_render_rows(
                projection,
                active_picker_item_key,
                active_document_id,
                exclude_ids,
            ),
            allow_root_pick,
            root_active: active_picker_item_key
                .map(str::trim)
                .map(|value| value == "__root__")
                .unwrap_or(false),
        }),
        _ => String::new(),
    };
    let root_label = root_node_id.unwrap_or("workspace_root");
    let active_label = active_document_id.unwrap_or("未指定");
    let template = r##"<!doctype html>
<html lang="zh-CN">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>mnote Tree Shell</title>
    <style>
      :root {
        color-scheme: light;
        --bg: #f3efe7;
        --panel: rgba(255, 252, 247, 0.96);
        --panel-strong: #ffffff;
        --ink: #18222f;
        --muted: #64748b;
        --line: rgba(148, 163, 184, 0.28);
        --accent: #0f6c84;
        --accent-soft: rgba(15, 108, 132, 0.12);
        --danger: #b42318;
      }
      * {
        box-sizing: border-box;
      }
      body {
        margin: 0;
        font-family: "Noto Sans CJK SC", "Source Han Sans SC", sans-serif;
        color: var(--ink);
        background:
          radial-gradient(circle at top left, rgba(15, 108, 132, 0.18), transparent 32%),
          linear-gradient(180deg, #fbf8f3 0%, var(--bg) 100%);
      }
      main {
        min-height: 100vh;
        display: grid;
        grid-template-rows: auto auto minmax(0, 1fr);
        gap: 16px;
        padding: 18px;
      }
      .hero,
      .status-card,
      .tree-card {
        background: var(--panel);
        border: 1px solid var(--line);
        border-radius: 22px;
        box-shadow: 0 18px 40px rgba(24, 34, 47, 0.08);
      }
      .hero {
        padding: 18px 20px;
        display: grid;
        gap: 8px;
      }
      .eyebrow {
        font-size: 12px;
        letter-spacing: 0.12em;
        text-transform: uppercase;
        color: var(--accent);
      }
      h1 {
        margin: 0;
        font-size: clamp(24px, 4vw, 34px);
        line-height: 1.05;
      }
      .summary {
        margin: 0;
        color: var(--muted);
        line-height: 1.55;
      }
      .meta-grid {
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
        gap: 10px;
      }
      .meta-pill {
        border-radius: 14px;
        padding: 10px 12px;
        background: rgba(255, 255, 255, 0.78);
        border: 1px solid rgba(148, 163, 184, 0.2);
      }
      .meta-pill strong {
        display: block;
        margin-bottom: 4px;
        font-size: 12px;
        color: var(--muted);
      }
      .status-card {
        padding: 14px 16px;
        display: grid;
        gap: 10px;
      }
      .toolbar {
        display: flex;
        gap: 10px;
        flex-wrap: wrap;
        align-items: center;
      }
      .toolbar button,
      .tree-action,
      .tree-toggle,
      .tree-link {
        font: inherit;
      }
      .toolbar button {
        border: 0;
        border-radius: 12px;
        padding: 10px 14px;
        background: var(--accent);
        color: #fff;
        cursor: pointer;
      }
      .toolbar button:hover {
        filter: brightness(0.97);
      }
      .toolbar button:disabled {
        opacity: 0.55;
        cursor: progress;
      }
      .status-text {
        font-size: 14px;
        color: var(--muted);
      }
      .status-text[data-tone="error"] {
        color: var(--danger);
      }
      .tree-card {
        display: grid;
        grid-template-rows: auto minmax(0, 1fr) auto;
        min-height: 0;
        overflow: hidden;
      }
      .tree-card-header {
        display: flex;
        justify-content: space-between;
        gap: 12px;
        align-items: baseline;
        padding: 16px 18px 12px;
        border-bottom: 1px solid rgba(148, 163, 184, 0.18);
      }
      .tree-card-title {
        font-size: 18px;
        font-weight: 700;
      }
      .tree-card-meta {
        font-size: 12px;
        color: var(--muted);
      }
      .tree-scroll {
        min-height: 0;
        overflow: auto;
        padding: 14px 12px 18px;
      }
      .tree-root,
      .tree-children {
        list-style: none;
        margin: 0;
        padding: 0;
      }
      .tree-children {
        margin-left: 22px;
        padding-left: 14px;
        border-left: 1px dashed rgba(15, 108, 132, 0.2);
      }
      .tree-node + .tree-node {
        margin-top: 8px;
      }
      .tree-row {
        display: grid;
        grid-template-columns: auto minmax(0, 1fr) auto;
        gap: 8px;
        align-items: center;
        border-radius: 16px;
        border: 1px solid rgba(148, 163, 184, 0.18);
        background: var(--panel-strong);
        padding: 8px;
      }
      .tree-row[data-focused="true"] {
        border-color: rgba(15, 108, 132, 0.48);
        box-shadow: inset 0 0 0 1px rgba(15, 108, 132, 0.18);
      }
      .tree-row[data-active="true"] {
        border-color: rgba(15, 108, 132, 0.32);
        box-shadow: inset 0 0 0 1px rgba(15, 108, 132, 0.12);
        background: linear-gradient(180deg, rgba(15, 108, 132, 0.08), rgba(255, 255, 255, 0.96));
      }
      .tree-row[data-selected="true"] {
        background: rgba(15, 108, 132, 0.1);
        border-color: rgba(15, 108, 132, 0.24);
      }
      .tree-row[data-drop-feedback="true"] {
        background: rgba(148, 163, 184, 0.18);
        border-color: rgba(15, 108, 132, 0.28);
      }
      .tree-row[data-draggable="true"] {
        cursor: grab;
      }
      .tree-row[data-draggable="true"]:active {
        cursor: grabbing;
      }
      .tree-toggle,
      .tree-action {
        border: 0;
        border-radius: 12px;
        background: transparent;
        color: var(--muted);
        cursor: pointer;
        padding: 6px 8px;
      }
      .tree-toggle:hover,
      .tree-action:hover {
        background: rgba(15, 108, 132, 0.08);
        color: var(--ink);
      }
      .tree-toggle[disabled],
      .tree-action[disabled] {
        opacity: 0.4;
        cursor: not-allowed;
      }
      .tree-spacer {
        width: 34px;
        height: 30px;
      }
      .tree-link {
        border: 0;
        background: transparent;
        padding: 6px 8px;
        text-align: left;
        cursor: pointer;
        display: grid;
        gap: 4px;
        min-width: 0;
      }
      .tree-link-title {
        font-size: 14px;
        font-weight: 600;
        color: var(--ink);
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
      }
      .tree-link-meta {
        font-size: 12px;
        color: var(--muted);
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
      }
      .tree-kind-badge {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        min-width: 22px;
        height: 22px;
        border-radius: 999px;
        background: rgba(15, 108, 132, 0.08);
        color: var(--accent);
        font-size: 11px;
        font-weight: 700;
      }
      .tree-actions {
        display: flex;
        gap: 4px;
        flex-wrap: wrap;
        justify-content: flex-end;
        opacity: 0;
        pointer-events: none;
        transition: opacity 140ms ease;
      }
      .tree-row:hover .tree-actions,
      .tree-row[data-focused="true"] .tree-actions,
      .tree-row:focus-within .tree-actions {
        opacity: 1;
        pointer-events: auto;
      }
      .tree-link:focus-visible,
      .tree-toggle:focus-visible,
      .tree-action:focus-visible,
      .tree-row:focus-visible {
        outline: 2px solid rgba(15, 108, 132, 0.35);
        outline-offset: 2px;
      }
      .tree-empty {
        border-radius: 18px;
        border: 1px dashed rgba(148, 163, 184, 0.36);
        padding: 24px 18px;
        text-align: center;
        color: var(--muted);
        background: rgba(255, 255, 255, 0.64);
      }
      .tree-card-footer {
        border-top: 1px solid rgba(148, 163, 184, 0.18);
        padding: 12px 16px 16px;
      }
      details {
        border-radius: 16px;
        border: 1px solid rgba(148, 163, 184, 0.22);
        background: rgba(255, 255, 255, 0.72);
        overflow: hidden;
      }
      summary {
        cursor: pointer;
        list-style: none;
        padding: 12px 14px;
        font-weight: 600;
      }
      summary::-webkit-details-marker {
        display: none;
      }
      pre {
        margin: 0;
        padding: 0 14px 14px;
        overflow: auto;
        font-size: 12px;
        line-height: 1.55;
        color: #243447;
      }
      /* 说明：这里用覆盖式样式把原本的开发态卡片壳压成真正的侧栏树视图，
         避免继续保留 hero/status/debug 大面板。 */
      body {
        background: #ffffff;
      }
      main {
        min-height: 100vh;
        display: grid;
        grid-template-rows: auto minmax(0, 1fr);
        gap: 0;
        padding: 0;
      }
      .hero,
      .status-card,
      .tree-card-footer,
      details,
      summary,
      pre {
        display: none !important;
      }
      .tree-card {
        border: 0;
        border-radius: 0;
        box-shadow: none;
        background: #ffffff;
      }
      .tree-card-header {
        padding: 8px 10px 6px;
        border-bottom: 1px solid rgba(31, 35, 40, 0.08);
        background: #ffffff;
        min-height: 40px;
      }
      .tree-card-title {
        font-size: 12px;
        font-weight: 600;
        color: #6b7280;
      }
      .tree-card-meta {
        display: none;
      }
      .toolbar {
        gap: 6px;
        justify-content: flex-end;
      }
      .toolbar button {
        border-radius: 6px;
        padding: 4px 8px;
        background: transparent;
        color: #6b7280;
        font-size: 12px;
      }
      .toolbar button:hover {
        background: rgba(15, 23, 42, 0.05);
        filter: none;
        color: #1f2328;
      }
      .tree-scroll {
        padding: 6px 4px 12px;
      }
      .tree-children {
        margin-left: 18px;
        padding-left: 10px;
        border-left: 0;
      }
      .tree-node + .tree-node {
        margin-top: 1px;
      }
      .tree-row {
        grid-template-columns: auto auto minmax(0, 1fr) auto;
        gap: 4px;
        min-height: 26px;
        border-radius: 6px;
        border: 0;
        background: transparent;
        padding: 1px 4px;
        position: relative;
      }
      .tree-row:hover {
        background: rgba(15, 23, 42, 0.04);
      }
      .tree-row[data-focused="true"] {
        border-color: transparent;
        box-shadow: none;
        background: rgba(37, 99, 235, 0.08);
      }
      .tree-row[data-active="true"],
      .tree-row[data-selected="true"] {
        border-color: transparent;
        box-shadow: none;
        background: rgba(37, 99, 235, 0.12);
      }
      .tree-row[data-drop-target="true"] {
        background: rgba(15, 108, 132, 0.14);
        outline: 1px solid rgba(15, 108, 132, 0.35);
      }
      .tree-row[data-cut="true"] {
        opacity: 0.55;
      }
      .tree-row[data-drop-position="before"]::before,
      .tree-row[data-drop-position="after"]::after {
        content: "";
        position: absolute;
        left: 20px;
        right: 8px;
        height: 2px;
        border-radius: 999px;
        background: rgba(37, 99, 235, 0.82);
      }
      .tree-row[data-drop-position="before"]::before {
        top: 0;
      }
      .tree-row[data-drop-position="after"]::after {
        bottom: 0;
      }
      .tree-root[data-drop-target="true"] {
        background: rgba(15, 108, 132, 0.05);
      }
      .tree-context-menu {
        position: fixed;
        z-index: 50;
        min-width: 188px;
        padding: 4px;
        border: 1px solid rgba(31, 35, 40, 0.12);
        border-radius: 6px;
        background: #ffffff;
        box-shadow: 0 12px 28px rgba(15, 23, 42, 0.16);
      }
      .tree-context-menu[hidden] {
        display: none;
      }
      .tree-menu-item {
        width: 100%;
        min-height: 26px;
        border: 0;
        border-radius: 4px;
        padding: 4px 8px;
        background: transparent;
        color: #1f2328;
        font-size: 12px;
        text-align: left;
      }
      .tree-menu-item:hover:not(:disabled),
      .tree-menu-item:focus-visible:not(:disabled) {
        background: rgba(37, 99, 235, 0.08);
        outline: none;
      }
      .tree-menu-item:disabled {
        color: #9ca3af;
        cursor: default;
      }
      .tree-menu-separator {
        height: 1px;
        margin: 4px 2px;
        background: rgba(31, 35, 40, 0.08);
      }
      .tree-preflight-backdrop {
        position: fixed;
        inset: 0;
        z-index: 60;
        display: flex;
        align-items: center;
        justify-content: center;
        padding: 20px;
        background: rgba(15, 23, 42, 0.28);
      }
      .tree-preflight-dialog {
        width: min(420px, 100%);
        border-radius: 8px;
        border: 1px solid rgba(31, 35, 40, 0.12);
        background: #ffffff;
        box-shadow: 0 20px 48px rgba(15, 23, 42, 0.22);
      }
      .tree-preflight-body {
        padding: 16px;
      }
      .tree-preflight-title {
        margin: 0 0 8px;
        color: #1f2328;
        font-size: 14px;
        font-weight: 600;
      }
      .tree-preflight-list {
        margin: 0;
        padding-left: 18px;
        color: #4b5563;
        font-size: 12px;
        line-height: 1.5;
      }
      .tree-preflight-actions {
        display: flex;
        justify-content: flex-end;
        gap: 8px;
        padding: 10px 16px;
        border-top: 1px solid rgba(31, 35, 40, 0.08);
      }
      .tree-preflight-actions button {
        min-width: 64px;
        min-height: 28px;
        border: 1px solid rgba(31, 35, 40, 0.12);
        border-radius: 6px;
        padding: 4px 10px;
        background: #ffffff;
        color: #1f2328;
        font-size: 12px;
      }
      .tree-preflight-actions [data-role="confirm"] {
        border-color: #2563eb;
        background: #2563eb;
        color: #ffffff;
      }
      .tree-toggle,
      .tree-action {
        border-radius: 4px;
        width: 20px;
        height: 20px;
        padding: 0;
        display: inline-flex;
        align-items: center;
        justify-content: center;
      }
      .tree-spacer {
        width: 16px;
        height: 16px;
      }
      .tree-link {
        padding: 2px 4px;
        gap: 0;
        min-width: 0;
      }
      .tree-link-title {
        font-size: 13px;
        font-weight: 500;
        line-height: 1.35;
        color: #1f2328;
      }
      .tree-rename-input {
        width: 100%;
        min-width: 80px;
        height: 22px;
        border: 1px solid rgba(37, 99, 235, 0.8);
        border-radius: 4px;
        padding: 0 4px;
        background: #fff;
        color: #1f2328;
        font: inherit;
        line-height: 20px;
        outline: none;
      }
      .tree-link-meta {
        display: none;
      }
      .tree-kind-badge {
        min-width: 16px;
        width: 16px;
        height: 16px;
        border-radius: 4px;
        background: transparent;
        color: #6b7280;
        font-size: 10px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
      }
      .tree-kind-badge svg,
      .tree-action svg {
        width: 14px;
        height: 14px;
        display: block;
      }
      .tree-kind-badge[data-kind="page"] {
        color: #2563eb;
      }
      .tree-kind-badge[data-kind="index"] {
        color: #0f766e;
      }
      .tree-kind-badge[data-kind="mindmap"] {
        color: #7c3aed;
      }
      .tree-kind-badge[data-kind="table"] {
        color: #b45309;
      }
      .tree-kind-badge[data-kind="pdf"] {
        color: #dc2626;
      }
      .tree-kind-badge[data-kind="book"] {
        color: #0f766e;
      }
      .tree-kind-badge[data-kind="image"] {
        color: #0891b2;
      }
      .tree-kind-badge[data-kind="video"] {
        color: #ea580c;
      }
      .tree-kind-badge[data-kind="audio"] {
        color: #16a34a;
      }
      .tree-kind-badge[data-kind="file"] {
        color: #64748b;
      }
      .tree-actions {
        gap: 2px;
        align-items: center;
        flex-wrap: nowrap;
      }
      .tree-action {
        color: #6b7280;
      }
      .tree-action:hover {
        color: #1f2328;
        background: rgba(15, 23, 42, 0.06);
      }
      .tree-row[data-shell-mode="page"] .tree-link {
        padding-right: 42px;
      }
      .tree-row[data-shell-mode="page"] .tree-actions {
        position: absolute;
        right: 6px;
        top: 50%;
        transform: translateY(-50%);
      }
      .tree-row[data-shell-mode="filetree"] .tree-link {
        padding-right: 30px;
      }
      .tree-row[data-shell-mode="filetree"] .tree-actions {
        position: absolute;
        right: 6px;
        top: 50%;
        transform: translateY(-50%);
      }
      .tree-empty {
        border: 0;
        border-radius: 0;
        padding: 16px 10px;
        text-align: left;
        background: transparent;
      }
      @media (max-width: 860px) {
        main {
          padding: 0;
        }
        .tree-row {
          grid-template-columns: auto auto minmax(0, 1fr);
        }
        .tree-actions {
          position: static;
          justify-content: flex-end;
          padding-left: 0;
        }
      }
    </style>
  </head>
  <body>
    <main>
      <section class="tree-card">
        <div class="tree-card-header">
          <div class="tree-card-title" id="tree-shell-title">Sidebar / Page Tree</div>
          <div class="tree-card-meta" id="tree-shell-summary">workspace=__WORKSPACE_ID__ · root=__ROOT_LABEL__ · active=__ACTIVE_LABEL__</div>
          <div class="toolbar" id="tree-shell-toolbar">
            <button id="tree-create-root" data-testid="tree-create-root" type="button">新建</button>
          </div>
        </div>
        <div class="tree-scroll" id="tree-shell-app">__INITIAL_TREE_HTML__</div>
      </section>
      <div class="status-text" id="tree-shell-status" data-tone="normal" hidden>Tree shell 已加载</div>
      <div class="status-text" id="tree-shell-last-action" data-tone="normal" hidden></div>
    </main>

    <script id="tree-shell-state" type="application/json">__APP_STATE__</script>
    <script>
      (() => {
        const stateElement = document.getElementById("tree-shell-state");
        const appElement = document.getElementById("tree-shell-app");
        const statusElement = document.getElementById("tree-shell-status");
        const lastActionElement = document.getElementById("tree-shell-last-action");
        const createRootButton = document.getElementById("tree-create-root");
        if (!stateElement || !appElement || !statusElement || !lastActionElement || !createRootButton) {
          return;
        }

        const parseState = () => {
          try {
            return JSON.parse(stateElement.textContent || "{}");
          } catch {
            return {};
          }
        };

        const state = parseState();
        const rendererInput =
          state.rendererInput && typeof state.rendererInput === "object"
            ? state.rendererInput
            : {};
        const rendererFiletreeSelection =
          rendererInput.filetreeSelection && typeof rendererInput.filetreeSelection === "object"
            ? rendererInput.filetreeSelection
            : {};
        const filetreeSelectionReducer =
          rendererInput.filetreeSelectionReducer &&
          typeof rendererInput.filetreeSelectionReducer === "object"
            ? rendererInput.filetreeSelectionReducer
            : {};
        const pickerStateReducer =
          rendererInput.pickerStateReducer &&
          typeof rendererInput.pickerStateReducer === "object"
            ? rendererInput.pickerStateReducer
            : {};
        const pageFocusKeyboardReducer =
          rendererInput.pageFocusKeyboardReducer &&
          typeof rendererInput.pageFocusKeyboardReducer === "object"
            ? rendererInput.pageFocusKeyboardReducer
            : {};
        const runtimeArtifact =
          rendererInput.runtimeArtifact && typeof rendererInput.runtimeArtifact === "object"
            ? rendererInput.runtimeArtifact
            : {};
        const runtimeApi =
          runtimeArtifact.runtimeApi && typeof runtimeArtifact.runtimeApi === "object"
            ? runtimeArtifact.runtimeApi
            : {};
        const normalizeStringArray = (value) =>
          Array.isArray(value)
            ? value
                .map((item) => (typeof item === "string" ? item.trim() : ""))
                .filter(Boolean)
            : [];
        const hostOverride =
          window.__MNOTE_TREE_SHELL_OVERRIDE__ &&
          typeof window.__MNOTE_TREE_SHELL_OVERRIDE__ === "object"
            ? window.__MNOTE_TREE_SHELL_OVERRIDE__
            : {};
        const channel =
          typeof state.channel === "string" && state.channel.trim()
            ? state.channel.trim()
            : "mnote-tree-shell-v1";
        const workspaceId =
          typeof state.workspaceId === "string" && state.workspaceId.trim()
            ? state.workspaceId.trim()
            : "";
        const sourceKind =
          typeof state.sourceKind === "string" && state.sourceKind.trim()
            ? state.sourceKind.trim()
            : "convex_workspace";
        const rootUri =
          typeof state.rootUri === "string" && state.rootUri.trim()
            ? state.rootUri.trim()
            : "";
        const initialLocalWatchRevision =
          state.localWatchRevision &&
          typeof state.localWatchRevision === "object" &&
          typeof state.localWatchRevision.revision === "string"
            ? state.localWatchRevision.revision
            : "";
        const actorId =
          typeof state.actorId === "string" && state.actorId.trim()
            ? state.actorId.trim()
            : "";
        const activeDocumentId =
          typeof state.activeDocumentId === "string" && state.activeDocumentId.trim()
            ? state.activeDocumentId.trim()
            : "";
        const focusedDocumentId =
          typeof state.focusedDocumentId === "string" && state.focusedDocumentId.trim()
            ? state.focusedDocumentId.trim()
            : "";
        const activePickerItemKey =
          typeof rendererInput.activePickerItem === "string" && rendererInput.activePickerItem.trim()
            ? rendererInput.activePickerItem.trim()
            : typeof state.activePickerItemKey === "string" && state.activePickerItemKey.trim()
              ? state.activePickerItemKey.trim()
              : "";
        const mode = (() => {
          const rawMode =
            typeof state.mode === "string" ? state.mode.trim() : "";
          if (rawMode === "picker") return "picker";
          if (rawMode === "filetree") return "filetree";
          return "page";
        })();
        const allowRootPick = state.allowRootPick === true;
        const excludedIds = new Set(
          normalizeStringArray(rendererInput.excludedPickerIds).length > 0
            ? normalizeStringArray(rendererInput.excludedPickerIds)
            : normalizeStringArray(state.excludeIds),
        );
        const pickerStateReducerContractName =
          typeof pickerStateReducer.contractName === "string" &&
          pickerStateReducer.contractName.trim()
            ? pickerStateReducer.contractName.trim()
            : "";
        const pickerStateReducerActions = new Set(
          normalizeStringArray(pickerStateReducer.actions),
        );
        const pageFocusKeyboardReducerContractName =
          typeof pageFocusKeyboardReducer.contractName === "string" &&
          pageFocusKeyboardReducer.contractName.trim()
            ? pageFocusKeyboardReducer.contractName.trim()
            : "";
        const pageFocusKeyboardReducerActions = new Set(
          normalizeStringArray(pageFocusKeyboardReducer.actions),
        );
        const commandPath =
          typeof state.commandPath === "string" && state.commandPath.trim()
            ? state.commandPath.trim()
            : "/api/tree/commands";
        const mediaAssets = Array.isArray(state.mediaAssets) ? state.mediaAssets : [];
        const mindmapAssets = Array.isArray(state.mindmapAssets) ? state.mindmapAssets : [];
        const tableAssets = Array.isArray(state.tableAssets) ? state.tableAssets : [];
        const mindmapAssetChildren =
          state.mindmapAssetChildren && typeof state.mindmapAssetChildren === "object"
            ? state.mindmapAssetChildren
            : {};
        const titleElement = document.getElementById("tree-shell-title");
        const summaryElement = document.getElementById("tree-shell-summary");
        const toolbarElement = document.getElementById("tree-shell-toolbar");
        const targetOrigin = (() => {
          try {
            if (!document.referrer) return "*";
            return new URL(document.referrer).origin || "*";
          } catch {
            return "*";
          }
        })();

        const normalizeText = (value, fallback = "") => {
          if (typeof value !== "string") return fallback;
          const trimmed = value.trim();
          return trimmed || fallback;
        };
        const initialRenameRowId = (() => {
          try {
            return normalizeText(new URL(window.location.href).searchParams.get("renameRowId"));
          } catch {
            return "";
          }
        })();
        const runtimeReduceEndpoint = normalizeText(
          runtimeApi.reduceEndpoint,
          "/api/tree/runtime/reduce",
        );

        const normalizeParent = (value) => {
          const normalized = normalizeText(value);
          return normalized || null;
        };

        const normalizeNumber = (value, fallback = Number.MAX_SAFE_INTEGER) => {
          return Number.isFinite(value) ? Number(value) : fallback;
        };

        const normalizeRowKind = (value) => {
          const normalized = normalizeText(value).toLowerCase();
          if (normalized === "index") return "index";
          if (normalized === "asset") return "asset";
          if (normalized === "folder") return "folder";
          if (normalized === "markdown") return "markdown";
          if (normalized === "asset_folder") return "asset_folder";
          return "document";
        };

        const normalizeCapabilities = (value) =>
          Array.isArray(value)
            ? value
                .map((item) => normalizeText(item))
                .filter(Boolean)
            : [];

        const normalizeResourceMeta = (value) => {
          if (!value || typeof value !== "object") {
            return {
              resourceKind: "",
              documentId: "",
              assetId: "",
              assetKind: "",
              objectIdentity: null,
              blockAssetRelation: null,
            };
          }
          const objectIdentity =
            value?.objectIdentity && typeof value.objectIdentity === "object"
              ? value.objectIdentity
              : null;
          const blockAssetRelation =
            value?.blockAssetRelation && typeof value.blockAssetRelation === "object"
              ? value.blockAssetRelation
              : null;
          return {
            resourceKind: normalizeText(value?.resourceKind),
            documentId: normalizeText(value?.documentId),
            assetId: normalizeText(value?.assetId),
            assetKind: normalizeText(value?.assetKind),
            objectIdentity,
            blockAssetRelation,
          };
        };

        const rawItems = Array.isArray(hostOverride.items) ? hostOverride.items : state.items;
        const normalizedItems = Array.isArray(rawItems)
          ? rawItems
              .map((item) => {
                const nodeId = normalizeText(item?.nodeId);
                const resourceMeta = normalizeResourceMeta(item?.resourceMeta);
                const rowKind = normalizeRowKind(item?.rowKind);
                const fallbackRowId =
                  rowKind === "index"
                    ? `index:${resourceMeta.documentId || nodeId.replace(/^index:/, "")}`
                    : rowKind === "asset"
                      ? `asset:${resourceMeta.assetId || nodeId.replace(/^asset:/, "")}`
                      : rowKind === "asset_folder"
                        ? `asset-folder:${resourceMeta.assetId || nodeId.replace(/^asset-folder:/, "")}`
                        : `doc:${resourceMeta.documentId || nodeId}`;
                return {
                  rowId: normalizeText(item?.rowId, fallbackRowId),
                  rowKind,
                  nodeId,
                  parentNodeId: normalizeParent(item?.parentNodeId),
                  title: normalizeText(item?.title, rowKind === "index" ? "index.md" : "无标题"),
                  depth: normalizeNumber(item?.depth, 0),
                  childCount: normalizeNumber(item?.childCount, 0),
                  position: normalizeNumber(item?.position),
                  expandedByDefault: item?.expandedByDefault !== false,
                  iconHint: normalizeText(item?.iconHint),
                  capabilities: normalizeCapabilities(item?.capabilities),
                  resourceMeta,
                };
              })
              .filter((item) => item.nodeId && !excludedIds.has(item.nodeId))
          : [];

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

        const compareItems = (left, right) => {
          const byPosition = left.position - right.position;
          if (byPosition !== 0) return byPosition;
          return left.title.localeCompare(right.title, "zh-CN");
        };

        normalizedItems.forEach((item) => {
          const parentId = item.parentNodeId && itemById.has(item.parentNodeId) ? item.parentNodeId : null;
          if (!parentId) {
            roots.push(item);
            return;
          }
          const bucket = childrenByParentId.get(parentId) || [];
          bucket.push(item);
          childrenByParentId.set(parentId, bucket);
        });

        roots.sort(compareItems);
        childrenByParentId.forEach((bucket) => bucket.sort(compareItems));

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

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

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

        let busy = false;

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

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

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

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

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

        const FILETREE_DRAG_MIME = "application/x-mnote-filetree-row-ids";

        const getFileTreeRowDocumentId = (item) => {
          if (!item) return "";
          if (item.resourceMeta?.documentId) return item.resourceMeta.documentId;
          if (item.rowKind === "document" || item.rowKind === "markdown") return item.nodeId;
          if (item.rowKind === "index") return item.nodeId.replace(/^index:/, "");
          return "";
        };

        const getFileTreeRowAssetId = (item) => {
          if (!item) return "";
          if (item.resourceMeta?.assetId) return item.resourceMeta.assetId;
          if (item.rowKind === "asset") return item.nodeId.replace(/^asset:/, "");
          if (item.rowKind === "asset_folder") return item.nodeId.replace(/^asset-folder:/, "");
          return "";
        };

        const getFileTreeRowIconKind = (item) => {
          if (!item) return "file";
          const iconHint = normalizeText(item.iconHint).toLowerCase();
          if (iconHint === "mindmap") return "mindmap";
          if (iconHint === "table") return "table";
          if (iconHint === "index") return "index";
          if (iconHint === "page") return "page";
          if (item.rowKind === "document") return "page";
          if (item.rowKind === "folder") return "folder";
          if (item.rowKind === "markdown") return "file";
          if (item.rowKind === "index") return "index";
          if (item.rowKind === "asset_folder") return "mindmap";
          if (item.resourceMeta?.resourceKind === "table") return "table";
          if (item.resourceMeta?.resourceKind === "mindmap") return "mindmap";
          return "file";
        };

        const getFileTreeRowMetaLabel = (item) => {
          if (!item) return "";
          if (item.rowKind === "document") {
            return `${getFileTreeRowDocumentId(item)} · 页面`;
          }
          if (item.rowKind === "folder") {
            return "本地文件夹";
          }
          if (item.rowKind === "markdown") {
            return `${getFileTreeRowDocumentId(item)} · Markdown`;
          }
          if (item.rowKind === "index") {
            return "页面正文";
          }
          if (item.rowKind === "asset_folder") {
            return `${item.resourceMeta?.resourceKind || "mindmap"} · 资源目录`;
          }
          return `${item.resourceMeta?.resourceKind || item.resourceMeta?.assetKind || "asset"} · ${getFileTreeRowAssetId(item)}`;
        };

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

        const getFileTreeDropTargetFromElement = (element) => {
          const row = element instanceof Element
            ? element.closest('.tree-row[data-shell-mode="filetree"]')
            : null;
          if (!(row instanceof HTMLElement)) {
            return {
              rowId: null,
              rowKind: "root",
              nodeId: null,
              documentId: null,
              assetId: null,
            };
          }
          return {
            rowId: normalizeText(row.dataset.rowId) || null,
            rowKind: normalizeText(row.dataset.rowKind, "document"),
            nodeId: normalizeText(row.dataset.nodeId) || null,
            documentId: normalizeText(row.dataset.documentId) || null,
            assetId: normalizeText(row.dataset.assetId) || null,
          };
        };

        const getFileTreeDropTargetFromEvent = (event) => {
          const target = getFileTreeDropTargetFromElement(event.target);
          if (!target.rowId) return { ...target, dropPosition: "inside" };
          const row = event.target instanceof Element
            ? event.target.closest('.tree-row[data-shell-mode="filetree"]')
            : null;
          if (!(row instanceof HTMLElement)) return { ...target, dropPosition: "inside" };
          const rect = row.getBoundingClientRect();
          const offset = rect.height > 0 ? (event.clientY - rect.top) / rect.height : 0.5;
          const item = fileTreeRowById.get(target.rowId);
          if (offset < 0.25) return { ...target, dropPosition: "before" };
          if (offset > 0.75) return { ...target, dropPosition: "after" };
          return {
            ...target,
            dropPosition: item && canExpandFileTreeRow(item) ? "inside" : "after",
          };
        };

        const resolveLocalFileTreeParentId = (target) => {
          if (!target || target.rowKind === "root") return null;
          const targetItem = target.rowId ? fileTreeRowById.get(target.rowId) : null;
          if (targetItem && targetItem.rowKind === "folder") {
            return targetItem.nodeId;
          }
          if (targetItem && targetItem.parentNodeId) {
            return targetItem.parentNodeId;
          }
          return null;
        };

        const isFileTreeDescendantOf = (candidate, ancestor) => {
          let cursor = candidate;
          while (cursor && cursor.parentNodeId) {
            if (cursor.parentNodeId === ancestor.nodeId) return true;
            cursor = itemById.get(cursor.parentNodeId) || null;
          }
          return false;
        };

        const validateFileTreeInternalDrop = (target, rowIds, copy) => {
          const sourceRowIds = Array.isArray(rowIds) ? rowIds.filter(Boolean) : [];
          if (sourceRowIds.length === 0) return { ok: false, reason: "empty" };
          const targetItem = target?.rowId ? fileTreeRowById.get(target.rowId) : null;
          if (targetItem && targetItem.capabilities.some((capability) =>
            capability === "readonly" || capability === "readOnly" || capability === "permissionDenied"
          )) {
            return { ok: false, reason: "readonly" };
          }
          for (const rowId of sourceRowIds) {
            const sourceItem = fileTreeRowById.get(rowId);
            if (!sourceItem) return { ok: false, reason: "cross_workspace" };
            if (targetItem && targetItem.rowId === sourceItem.rowId) {
              return { ok: false, reason: "self" };
            }
            if (targetItem && sourceItem.rowKind === "folder" && isFileTreeDescendantOf(targetItem, sourceItem)) {
              return { ok: false, reason: "descendant" };
            }
            if (copy && sourceItem.rowKind === "folder") {
              return { ok: false, reason: "copy_folder_unsupported" };
            }
          }
          return { ok: true, reason: "" };
        };

        const filterRedundantFileTreeRowIds = (rowIds) => {
          const selected = new Set(Array.isArray(rowIds) ? rowIds.filter(Boolean) : []);
          return Array.from(selected).filter((rowId) => {
            const item = fileTreeRowById.get(rowId);
            if (!item) return false;
            let cursor = item;
            while (cursor && cursor.parentNodeId) {
              const parent = itemById.get(cursor.parentNodeId) || null;
              if (parent && selected.has(parent.rowId)) {
                return false;
              }
              cursor = parent;
            }
            return true;
          });
        };

        const validateFileTreeWritableTarget = (target) => {
          const targetItem = target?.rowId ? fileTreeRowById.get(target.rowId) : null;
          if (
            targetItem &&
            targetItem.capabilities.some((capability) =>
              capability === "readonly" ||
              capability === "readOnly" ||
              capability === "permissionDenied"
            )
          ) {
            return { ok: false, reason: "readonly", targetItem };
          }
          return { ok: true, reason: "", targetItem };
        };

        const getFileTreeRowLabel = (item) =>
          item?.title || item?.rowId || item?.nodeId || "选中项";

        const getFileTreeTargetLabel = (target) => {
          if (!target || target.rowKind === "root" || !target.rowId) return "Explorer 根目录";
          const item = fileTreeRowById.get(target.rowId);
          if (!item) return target.rowId;
          if (item.rowKind === "markdown" || item.rowKind === "document" || item.rowKind === "index") {
            return `${item.title} 的父目录`;
          }
          return item.title;
        };

        const showFileTreePreflight = (preflight) =>
          new Promise((resolve) => {
            if (activeFileTreePreflightElement) {
              activeFileTreePreflightElement.remove();
              activeFileTreePreflightElement = null;
            }

            const backdrop = document.createElement("div");
            backdrop.className = "tree-preflight-backdrop";
            backdrop.dataset.testid = "tree-preflight";
            backdrop.dataset.preflightKind = preflight.kind || "";
            const dialog = document.createElement("div");
            dialog.className = "tree-preflight-dialog";
            dialog.setAttribute("role", "dialog");
            dialog.setAttribute("aria-modal", "true");
            const body = document.createElement("div");
            body.className = "tree-preflight-body";
            const title = document.createElement("h2");
            title.className = "tree-preflight-title";
            title.textContent = preflight.title || "操作预检";
            body.appendChild(title);

            const list = document.createElement("ul");
            list.className = "tree-preflight-list";
            (preflight.lines || []).forEach((line) => {
              const item = document.createElement("li");
              item.textContent = line;
              list.appendChild(item);
            });
            body.appendChild(list);
            dialog.appendChild(body);

            const actions = document.createElement("div");
            actions.className = "tree-preflight-actions";
            const cancel = document.createElement("button");
            cancel.type = "button";
            cancel.dataset.role = "cancel";
            cancel.textContent = "取消";
            const confirm = document.createElement("button");
            confirm.type = "button";
            confirm.dataset.role = "confirm";
            confirm.textContent = preflight.confirmLabel || "继续";
            actions.appendChild(cancel);
            actions.appendChild(confirm);
            dialog.appendChild(actions);
            backdrop.appendChild(dialog);

            const finish = (accepted) => {
              backdrop.remove();
              activeFileTreePreflightElement = null;
              document.removeEventListener("keydown", onKeyDown, true);
              resolve(accepted);
            };
            const onKeyDown = (event) => {
              if (event.key === "Escape") {
                event.preventDefault();
                finish(false);
              }
            };
            cancel.addEventListener("click", () => finish(false));
            confirm.addEventListener("click", () => finish(true));
            document.addEventListener("keydown", onKeyDown, true);
            document.body.appendChild(backdrop);
            activeFileTreePreflightElement = backdrop;
            cancel.focus({ preventScroll: true });
          });

        const runFileTreePreflight = async (kind, context = {}) => {
          const sourceRowIds = Array.isArray(context.rowIds) ? context.rowIds.filter(Boolean) : [];
          const sourceItems = sourceRowIds
            .map((rowId) => fileTreeRowById.get(rowId))
            .filter(Boolean);
          const targetLabel = getFileTreeTargetLabel(context.target);
          const sourceLabel =
            sourceItems.length === 0
              ? "外部文件"
              : sourceItems.map(getFileTreeRowLabel).join("、");
          const fileNames = Array.isArray(context.files)
            ? context.files.map((file) => file.name || "未命名文件").filter(Boolean)
            : [];

          if (context.validation && context.validation.ok === false) {
            if (context.validation.reason === "readonly") {
              await showFileTreePreflight({
                kind: "readonly",
                title: "操作预检失败",
                confirmLabel: "知道了",
                lines: [
                  "目标: readonly",
                  "原因: 当前目标为只读或权限不足。",
                  "结果: 不会发送 execute。",
                ],
              });
              return false;
            }
            setLastAction(`预检拒绝: ${context.validation.reason}`, "error");
            return false;
          }

          const writableTarget = validateFileTreeWritableTarget(context.target);
          if (!writableTarget.ok) {
            await showFileTreePreflight({
              kind: "readonly",
              title: "操作预检失败",
              confirmLabel: "知道了",
              lines: [
                `目标: ${getFileTreeRowLabel(writableTarget.targetItem)}`,
                "原因: 当前目标为只读或权限不足。",
                "结果: 不会发送 execute。",
              ],
            });
            return false;
          }

          const preflight = (() => {
            if (kind === "delete") {
              return {
                kind,
                title: "删除预检",
                confirmLabel: "删除",
                lines: [
                  `目标: ${sourceLabel}`,
                  `影响: ${Math.max(1, sourceItems.length)} 个文件树节点`,
                  sourceKind === "local_folder"
                    ? "删除后进入本地 .mnote/trash。"
                    : "删除将进入云端回收站。",
                ],
              };
            }
            if (kind === "paste") {
              return {
                kind,
                title: context.copy ? "粘贴复制预检" : "粘贴移动预检",
                confirmLabel: "粘贴",
                lines: [
                  `来源: ${sourceLabel}`,
                  `目标: ${targetLabel}`,
                  "命名冲突策略: 使用递增命名，不静默覆盖。",
                ],
              };
            }
            if (kind === "dropFiles") {
              return {
                kind,
                title: "外部拖入预检",
                confirmLabel: sourceKind === "local_folder" ? "拖入" : "发送",
                lines: [
                  `文件: ${fileNames.join("、") || "外部文件"}`,
                  `目标: ${targetLabel}`,
                  sourceKind === "local_folder"
                    ? "目标可写时复制进本地文件夹；命名冲突使用递增命名。"
                    : "Convex 目标交给宿主上传到对象存储；命名冲突由上传 executor 处理。",
                ],
              };
            }
            return {
              kind: context.copy ? "copy" : "move",
              title: context.copy ? "复制预检" : "移动预检",
              confirmLabel: context.copy ? "复制" : "移动",
              lines: [
                `来源: ${sourceLabel}`,
                `目标: ${targetLabel}`,
                context.copy
                  ? "命名冲突策略: 使用递增命名，不静默覆盖。"
                  : "会检查自拖自身、后代目标和跨 workspace 来源。",
              ],
            };
          })();

          return await showFileTreePreflight(preflight);
        };

        const executeLocalFileTreeInternalDrop = async (target, rowIds, copy, trigger = "drop") => {
          const validation = validateFileTreeInternalDrop(target, rowIds, copy);
          if (!validation.ok) {
            setLastAction(`已拒绝非法拖拽: ${validation.reason}`, "error");
            return false;
          }
          const accepted = await runFileTreePreflight(trigger === "paste" ? "paste" : "move", {
            target,
            rowIds,
            copy,
            validation,
          });
          if (!accepted) return false;
          const parentId = resolveLocalFileTreeParentId(target);
          const sourceRowIds = filterRedundantFileTreeRowIds(rowIds);
          if (sourceRowIds.length === 0) return false;
          for (const rowId of sourceRowIds) {
            const sourceItem = fileTreeRowById.get(rowId);
            const documentId = getFileTreeRowDocumentId(sourceItem) || sourceItem?.nodeId || "";
            if (!documentId) continue;
            await sendCommand({
              action: copy ? "copy" : "move",
              workspaceId,
              documentId,
              parentId,
              sortOrder: 0,
            });
          }
          setStatus(copy ? "复制成功" : "移动成功");
          setLastAction(copy ? "本地文件树复制完成" : "本地文件树移动完成");
          scheduleRefresh();
          return true;
        };

        const executeLocalFileTreeExternalDrop = async (target, files) => {
          const droppedFiles = Array.from(files || []);
          if (droppedFiles.length === 0) return false;
          const accepted = await runFileTreePreflight("dropFiles", {
            target,
            files: droppedFiles,
          });
          if (!accepted) return false;
          const parentId = resolveLocalFileTreeParentId(target);
          const payload = await Promise.all(
            droppedFiles.map(async (file) => ({
              name: file.name || "dropped-file",
              text: await file.text(),
            })),
          );
          await sendCommand({
            action: "dropFiles",
            workspaceId,
            parentId,
            content: payload,
          });
          setStatus("外部文件拖入成功");
          setLastAction(`已拖入 ${payload.length} 个本地文件`);
          scheduleRefresh();
          return true;
        };

        const clearFileTreeDropFeedback = () => {
          if (fileTreeHoverExpandTimer) {
            window.clearTimeout(fileTreeHoverExpandTimer);
            fileTreeHoverExpandTimer = 0;
          }
          if (activeFileTreeDropRowId) {
            const previousRow = appElement.querySelector(
              `.tree-row[data-shell-mode="filetree"][data-row-id="${activeFileTreeDropRowId}"]`,
            );
            if (previousRow instanceof HTMLElement) {
              previousRow.dataset.dropTarget = "false";
              delete previousRow.dataset.dropPosition;
            }
          }
          activeFileTreeDropRowId = null;
          activeFileTreeDropPosition = null;
          activeFileTreeRootDrop = false;
          appElement.querySelectorAll('.tree-root[data-drop-target="true"]').forEach((element) => {
            if (element instanceof HTMLElement) {
              element.dataset.dropTarget = "false";
            }
          });
        };

        const setFileTreeDropFeedback = (target) => {
          const nextRowId = target?.rowId || null;
          const nextDropPosition = target?.dropPosition || "inside";
          if (activeFileTreeDropRowId && activeFileTreeDropRowId !== nextRowId) {
            const previousRow = appElement.querySelector(
              `.tree-row[data-shell-mode="filetree"][data-row-id="${activeFileTreeDropRowId}"]`,
            );
            if (previousRow instanceof HTMLElement) {
              previousRow.dataset.dropTarget = "false";
              delete previousRow.dataset.dropPosition;
            }
          }
          if (nextRowId) {
            const nextRow = appElement.querySelector(
              `.tree-row[data-shell-mode="filetree"][data-row-id="${nextRowId}"]`,
            );
            if (nextRow instanceof HTMLElement) {
              nextRow.dataset.dropTarget = "true";
              nextRow.dataset.dropPosition = nextDropPosition;
            }
            activeFileTreeDropRowId = nextRowId;
            activeFileTreeDropPosition = nextDropPosition;
            activeFileTreeRootDrop = false;
            if (fileTreeHoverExpandTimer) {
              window.clearTimeout(fileTreeHoverExpandTimer);
              fileTreeHoverExpandTimer = 0;
            }
            const targetItem = fileTreeRowById.get(nextRowId);
            if (
              nextDropPosition === "inside" &&
              targetItem &&
              canExpandFileTreeRow(targetItem) &&
              !expanded.has(targetItem.nodeId)
            ) {
              fileTreeHoverExpandTimer = window.setTimeout(() => {
                expanded.add(targetItem.nodeId);
                fileTreeHoverExpandTimer = 0;
                renderTree();
              }, 650);
            }
            appElement.querySelectorAll('.tree-root[data-drop-target="true"]').forEach((element) => {
              if (element instanceof HTMLElement) {
                element.dataset.dropTarget = "false";
              }
            });
            return;
          }
          activeFileTreeDropRowId = null;
          activeFileTreeDropPosition = null;
          const root = appElement.querySelector(".tree-root");
          if (root instanceof HTMLElement) {
            root.dataset.dropTarget = "true";
          }
          activeFileTreeRootDrop = true;
        };

        const updateFileTreeDropFeedback = (rowId) => {
          if (rowId) {
            setFileTreeDropFeedback({ rowId });
            return;
          }
          clearFileTreeDropFeedback();
        };

        const inferDefaultFileTreeDropDocumentId = () => {
          const candidateRowIds = [
            fileTreeFocusedRowId,
            fileTreeAnchorRowId,
            ...Array.from(selectedFileTreeRowIds),
          ].filter(Boolean);

          for (const rowId of candidateRowIds) {
            if (!rowId) continue;
            const item = fileTreeRowById.get(rowId);
            const documentId = getFileTreeRowDocumentId(item);
            if (documentId) {
              return documentId;
            }
          }

          const firstDocRowId = visibleFileTreeRowIds.find((rowId) => rowId.startsWith("doc:"));
          const firstDocItem = firstDocRowId ? fileTreeRowById.get(firstDocRowId) : null;
          return getFileTreeRowDocumentId(firstDocItem) || null;
        };

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

          commitFileTreeSelection(nextSelection);
        };

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

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

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

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

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

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

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

        const handleFileTreeKeyDown = (event, item) => {
          if (mode !== "filetree") return;
          if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "c") {
            event.preventDefault();
            setFileTreeClipboard("copy");
            return;
          }
          if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "x") {
            event.preventDefault();
            setFileTreeClipboard("cut");
            return;
          }
          if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "v") {
            event.preventDefault();
            void pasteFileTreeClipboardInto(item?.rowId || fileTreeFocusedRowId);
            return;
          }
          if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "a") {
            event.preventDefault();
            commitFileTreeSelection({
              selectedRowIds: visibleFileTreeRowIds,
              anchorRowId: visibleFileTreeRowIds[0] || null,
              focusedRowId: visibleFileTreeRowIds[visibleFileTreeRowIds.length - 1] || null,
            });
            renderTree();
            return;
          }
          if (event.key === "F2") {
            event.preventDefault();
            const rowId = item?.rowId || fileTreeFocusedRowId;
            if (rowId) beginInlineRename("filetree", rowId);
            return;
          }
          if (event.key === "ArrowDown") {
            event.preventDefault();
            focusFileTreeRowByOffset(1);
            return;
          }
          if (event.key === "ArrowUp") {
            event.preventDefault();
            focusFileTreeRowByOffset(-1);
            return;
          }
          if (event.key === "Home") {
            event.preventDefault();
            focusFileTreeRowByOffset(-visibleFileTreeRowIds.length);
            return;
          }
          if (event.key === "End") {
            event.preventDefault();
            focusFileTreeRowByOffset(visibleFileTreeRowIds.length);
            return;
          }
          if (event.key === "Enter") {
            event.preventDefault();
            if (item) {
              const documentId = getFileTreeRowDocumentId(item);
              if (documentId) handleNavigate(documentId);
            }
            return;
          }
          if (event.key === "Delete" || event.key === "Backspace") {
            event.preventDefault();
            void runFileTreeDelete(item?.rowId || fileTreeFocusedRowId);
          }
        };

        const scheduleRefresh = (options = {}) => {
          window.setTimeout(() => {
            const renameRowId = normalizeText(options.renameRowId);
            if (renameRowId) {
              const url = new URL(window.location.href);
              url.searchParams.set("renameRowId", renameRowId);
              window.location.assign(url.toString());
              return;
            }
            window.location.reload();
          }, 80);
        };

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

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

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

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

        if (sourceKind === "local_folder" && rootUri) {
          let localWatchRevision = initialLocalWatchRevision;
          let localWatchRefreshTimer = 0;
          const refreshFromLocalWatch = () => {
            if (localWatchRefreshTimer) return;
            localWatchRefreshTimer = window.setTimeout(() => {
              window.location.reload();
            }, 180);
          };
          const pollLocalFolderRevision = async () => {
            if (busy || document.hidden) return;
            const url = new URL("/api/tree/local-folder-watch", window.location.origin);
            url.searchParams.set("rootUri", rootUri);
            const response = await fetch(url.toString(), { headers: { "accept": "application/json" } });
            if (!response.ok) return;
            const payload = await response.json();
            const nextRevision =
              payload &&
              payload.result &&
              typeof payload.result.revision === "string"
                ? payload.result.revision
                : "";
            if (!nextRevision) return;
            if (!localWatchRevision) {
              localWatchRevision = nextRevision;
              return;
            }
            if (nextRevision !== localWatchRevision) {
              localWatchRevision = nextRevision;
              refreshFromLocalWatch();
            }
          };
          window.setInterval(() => {
            void pollLocalFolderRevision();
          }, 1200);
        }

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

        const ICONS = {
          add: `
            <svg viewBox="0 0 16 16" fill="none" aria-hidden="true">
              <path d="M8 3.2v9.6M3.2 8h9.6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
            </svg>
          `,
          more: `
            <svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
              <circle cx="4" cy="8" r="1.2"/>
              <circle cx="8" cy="8" r="1.2"/>
              <circle cx="12" cy="8" r="1.2"/>
            </svg>
          `,
          edit: `
            <svg viewBox="0 0 16 16" fill="none" aria-hidden="true">
              <path d="M3.4 11.8 3 13l1.2-.4 6.6-6.6-1.4-1.4-6 6.2Z" stroke="currentColor" stroke-width="1.1" stroke-linejoin="round"/>
              <path d="m9.9 4.6 1.5-1.5a1 1 0 0 1 1.4 0l.6.6a1 1 0 0 1 0 1.4l-1.5 1.5" stroke="currentColor" stroke-width="1.1" stroke-linecap="round"/>
            </svg>
          `,
          up: `
            <svg viewBox="0 0 16 16" fill="none" aria-hidden="true">
              <path d="M8 12.6V4.2M8 4.2 5.4 6.8M8 4.2l2.6 2.6" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
            </svg>
          `,
          page: `
            <svg viewBox="0 0 16 16" fill="none" aria-hidden="true">
              <path d="M4 2.8h5.2l2.8 2.8v7.6H4V2.8Z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/>
              <path d="M9.2 2.8v2.8H12" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/>
            </svg>
          `,
          index: `
            <svg viewBox="0 0 16 16" fill="none" aria-hidden="true">
              <path d="M4 2.8h5.2l2.8 2.8v7.6H4V2.8Z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/>
              <path d="M6 7h4M6 9h4M6 11h3" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/>
            </svg>
          `,
          mindmap: `
            <svg viewBox="0 0 16 16" fill="none" aria-hidden="true">
              <circle cx="8" cy="8" r="1.6" fill="currentColor"/>
              <circle cx="4" cy="4.5" r="1.3" stroke="currentColor" stroke-width="1.1"/>
              <circle cx="12" cy="4.5" r="1.3" stroke="currentColor" stroke-width="1.1"/>
              <circle cx="12" cy="11.5" r="1.3" stroke="currentColor" stroke-width="1.1"/>
              <path d="M6.8 7 4.9 5.4M9.2 7l1.9-1.6M9.1 9l2 1.6" stroke="currentColor" stroke-width="1.1" stroke-linecap="round"/>
            </svg>
          `,
          table: `
            <svg viewBox="0 0 16 16" fill="none" aria-hidden="true">
              <rect x="3.2" y="3.2" width="9.6" height="9.6" rx="1.4" stroke="currentColor" stroke-width="1.2"/>
              <path d="M3.8 6.6h8.4M6.6 3.8v8.4M9.4 3.8v8.4" stroke="currentColor" stroke-width="1.1"/>
            </svg>
          `,
          pdf: `
            <svg viewBox="0 0 16 16" fill="none" aria-hidden="true">
              <path d="M4 2.8h5.2l2.8 2.8v7.6H4V2.8Z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/>
              <path d="M6 10.8V6.5h1.5a1.2 1.2 0 1 1 0 2.4H6m3.2-2.4v4.3m0 0c1.1 0 1.8-.8 1.8-2.1 0-1.3-.7-2.2-1.8-2.2m-1.7 4.3h1.7" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"/>
            </svg>
          `,
          book: `
            <svg viewBox="0 0 16 16" fill="none" aria-hidden="true">
              <path d="M4.2 3.2h6.2a1.6 1.6 0 0 1 1.6 1.6v7.4H5.4a1.2 1.2 0 0 0-1.2 1.2V4.4a1.2 1.2 0 0 1 1.2-1.2Z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/>
              <path d="M5.4 12.2V4.1M7 6h3.1M7 8.2h3.1" stroke="currentColor" stroke-width="1.1" stroke-linecap="round"/>
            </svg>
          `,
          image: `
            <svg viewBox="0 0 16 16" fill="none" aria-hidden="true">
              <rect x="3" y="3" width="10" height="10" rx="1.5" stroke="currentColor" stroke-width="1.2"/>
              <circle cx="6.2" cy="6.2" r="1.1" stroke="currentColor" stroke-width="1"/>
              <path d="M4.5 11 7.1 8.6l1.8 1.7 1.7-1.5L12 11" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"/>
            </svg>
          `,
          video: `
            <svg viewBox="0 0 16 16" fill="none" aria-hidden="true">
              <rect x="3" y="3.4" width="7.8" height="9.2" rx="1.4" stroke="currentColor" stroke-width="1.2"/>
              <path d="m9.8 7 2.8-1.7v5.4L9.8 9" stroke="currentColor" stroke-width="1.1" stroke-linejoin="round"/>
            </svg>
          `,
          audio: `
            <svg viewBox="0 0 16 16" fill="none" aria-hidden="true">
              <path d="M6.4 4.2v7.6a1.5 1.5 0 1 1-1-1.4V5.6l5.2-1.2v5.2a1.5 1.5 0 1 1-1-1.4V3.5L6.4 4.2Z" stroke="currentColor" stroke-width="1.1" stroke-linejoin="round"/>
            </svg>
          `,
          file: `
            <svg viewBox="0 0 16 16" fill="none" aria-hidden="true">
              <path d="M4 2.8h5.2l2.8 2.8v7.6H4V2.8Z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/>
              <path d="M6 8.2h4M6 10.4h2.8" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/>
            </svg>
          `,
        };

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        const getVisiblePickerEntries = () => {
          if (mode !== "picker") {
            return [];
          }

          const visible = [];
          if (allowRootPick) {
            visible.push({
              pickerItemKey: "__root__",
              item: null,
            });
          }

          const walk = (entries) => {
            entries.forEach((item) => {
              visible.push({
                pickerItemKey: item.nodeId,
                item,
              });
              if (item.childCount > 0 && expanded.has(item.nodeId)) {
                walk(getSiblings(item.nodeId));
              }
            });
          };

          walk(roots);
          return visible;
        };

        const isPickerEntryPickable = (entry) => {
          if (!entry) return false;
          if (entry.pickerItemKey === "__root__") {
            return allowRootPick;
          }
          const documentId = normalizeText(entry.item?.nodeId || entry.pickerItemKey);
          return Boolean(documentId && !excludedIds.has(documentId));
        };

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

        const normalizePickerItemKey = (pickerItemKey) => {
          const normalizedItemKey = normalizeText(pickerItemKey);
          if (normalizedItemKey === "__root__" && allowRootPick) {
            return "__root__";
          }
          if (normalizedItemKey && itemById.has(normalizedItemKey) && !excludedIds.has(normalizedItemKey)) {
            return normalizedItemKey;
          }
          return "";
        };

        const resolveCurrentPickerItemKey = () => {
          const fromActive = normalizePickerItemKey(currentActivePickerItemKey);
          if (fromActive) return fromActive;
          const fromDocument = normalizePickerItemKey(currentActiveDocumentId);
          if (fromDocument) return fromDocument;
          return getPickablePickerEntries()[0]?.pickerItemKey || "";
        };

        const computePickerStateActionResult = (action) => {
          const currentPickerItemKey = resolveCurrentPickerItemKey();
          if (
            mode !== "picker" ||
            pickerStateReducerContractName !== "rust_picker_state_reducer_v1" ||
            !pickerStateReducerActions.has(action?.kind || "")
          ) {
            return {
              nextItemKey: currentPickerItemKey,
              pickedDocumentId:
                action?.kind === "pick" && currentPickerItemKey !== "__root__"
                  ? currentPickerItemKey || null
                  : null,
              pickedRoot: action?.kind === "pick" && currentPickerItemKey === "__root__",
            };
          }

          const pickable = getPickablePickerEntries();
          if (pickable.length === 0) {
            return {
              nextItemKey: "",
              pickedDocumentId: null,
              pickedRoot: false,
            };
          }
          const currentIndex = pickable.findIndex(
            (entry) => entry.pickerItemKey === currentPickerItemKey,
          );
          const resolvedIndex = currentIndex >= 0 ? currentIndex : 0;
          const actionKind = action.kind;

          if (actionKind === "normalize") {
            return {
              nextItemKey: pickable[resolvedIndex]?.pickerItemKey || "",
              pickedDocumentId: null,
              pickedRoot: false,
            };
          }

          if (actionKind === "focus") {
            const nextItemKey = normalizePickerItemKey(action.itemKey);
            return {
              nextItemKey: nextItemKey || pickable[0]?.pickerItemKey || "",
              pickedDocumentId: null,
              pickedRoot: false,
            };
          }

          if (actionKind === "pick") {
            const target = pickable[resolvedIndex];
            const targetKey = target?.pickerItemKey || "";
            return {
              nextItemKey: targetKey,
              pickedDocumentId:
                targetKey && targetKey !== "__root__" ? targetKey : null,
              pickedRoot: targetKey === "__root__",
            };
          }

          let nextIndex = resolvedIndex;
          if (actionKind === "next") {
            nextIndex = Math.min(pickable.length - 1, resolvedIndex + 1);
          } else if (actionKind === "previous") {
            nextIndex = Math.max(0, resolvedIndex - 1);
          } else if (actionKind === "home") {
            nextIndex = 0;
          } else if (actionKind === "end") {
            nextIndex = pickable.length - 1;
          } else {
            return {
              nextItemKey: currentPickerItemKey,
              pickedDocumentId: null,
              pickedRoot: false,
            };
          }

          return {
            nextItemKey: pickable[nextIndex]?.pickerItemKey || "",
            pickedDocumentId: null,
            pickedRoot: false,
          };
        };

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

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

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

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

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

        const reducePageActionWithRuntime = async (action, item) => {
          if (mode !== "page" || !runtimeReduceEndpoint) {
            return null;
          }
          const runtimeAction = buildPageRuntimeAction(action, item);
          if (!runtimeAction) return null;
          const response = await fetch(runtimeReduceEndpoint, {
            method: "POST",
            headers: { "content-type": "application/json" },
            body: JSON.stringify({
              mode: "page",
              requestId: `page-runtime-${Date.now()}`,
              environment: buildPageRuntimeEnvironment(),
              state: readPageRuntimeState(action, item),
              action: runtimeAction,
            }),
          });
          if (!response.ok) {
            throw new Error(await readErrorMessage(response));
          }
          return response.json();
        };

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

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

        const reconcilePageRuntimeResult = (runtimeResult, item) => {
          const result = normalizePageRuntimeResult(runtimeResult);
          if (!result) return false;
          const previousExpanded = new Set(expanded);
          let shouldPatchTree = false;
          if (Array.isArray(result.expandedIds)) {
            shouldPatchTree = commitPageExpandedIds(result.expandedIds) || shouldPatchTree;
          }
          if (result.focusedId && result.focusedId !== focusedNodeId) {
            focusedNodeId = result.focusedId;
            postPageFocusChange(result.focusedId);
            shouldPatchTree = true;
          }
          const itemNodeId = normalizeText(item?.nodeId);
          if (itemNodeId && previousExpanded.has(itemNodeId) !== expanded.has(itemNodeId)) {
            postPageExpandChange(itemNodeId, expanded.has(itemNodeId));
          }
          if (shouldPatchTree) {
            patchPageTreeAfterRuntimeState(
              Array.isArray(result.expandedIds) ? [itemNodeId, ...result.expandedIds] : [itemNodeId],
              result.focusedId || focusedNodeId,
            );
          }
          return shouldPatchTree;
        };

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

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

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

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

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

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

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

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

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

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

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

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

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

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

          applyPickerStateAction({ kind: normalizedCommand });
        };

        const closeFileTreeContextMenu = () => {
          if (!activeFileTreeMenuElement) return;
          activeFileTreeMenuElement.remove();
          activeFileTreeMenuElement = null;
        };

        const buildFileTreeMenuTarget = (rowId, rowKind, documentId, assetId) => {
          const item = rowId ? fileTreeRowById.get(rowId) : null;
          return {
            item,
            rowId: rowId || null,
            rowKind: rowKind || item?.rowKind || "root",
            documentId: documentId || getFileTreeRowDocumentId(item) || null,
            assetId: assetId || getFileTreeRowAssetId(item) || null,
          };
        };

        const createFileTreeMenuItem = (kind, label, options = {}) => ({
          kind,
          label,
          disabled: options.disabled === true,
          reason: normalizeText(options.reason),
          separatorBefore: options.separatorBefore === true,
        });

        const buildFileTreeContextMenuProfile = (target) => {
          const selectedCount = selectedFileTreeRowIds.size;
          const isMulti = selectedCount > 1 && target.rowId && selectedFileTreeRowIds.has(target.rowId);
          const item = target.item;
          const rowKind = target.rowKind || "root";
          const canPaste = Boolean(fileTreeClipboard.action && fileTreeClipboard.rowIds.length > 0);
          const hasDocument = Boolean(getFileTreeRowDocumentId(item));
          const hasAsset = Boolean(getFileTreeRowAssetId(item));
          const localSource = sourceKind === "local_folder";
          const convexSource = sourceKind === "convex_workspace";
          const canCreateFolder = localSource;
          const canCreatePage = rowKind === "root" || rowKind === "folder" || rowKind === "document";
          const canRename =
            rowKind === "markdown" ||
            rowKind === "document" ||
            rowKind === "folder" ||
            rowKind === "index" ||
            (localSource && rowKind === "asset");
          const canCopyCut =
            rowKind === "markdown" ||
            rowKind === "document" ||
            rowKind === "index";
          const canDelete =
            rowKind === "markdown" ||
            rowKind === "document" ||
            rowKind === "index";

          if (rowKind === "root") {
            return [
              createFileTreeMenuItem("newPage", "新建页面", {
                disabled: !canCreatePage,
                reason: "当前 root 不支持新建页面",
              }),
              createFileTreeMenuItem("newFolder", "新建文件夹", {
                disabled: !canCreateFolder,
                reason: convexSource ? "Convex workspace 当前未暴露 folder capability" : "当前 source 不支持新建文件夹",
              }),
              createFileTreeMenuItem("upload", "上传/导入", {
                disabled: true,
                reason: localSource ? "外部文件请拖入 Explorer" : "Convex 上传 executor 尚未接入",
              }),
              createFileTreeMenuItem("refresh", "刷新", { separatorBefore: true }),
              createFileTreeMenuItem("collapseAll", "全部折叠"),
            ];
          }

          if (isMulti) {
            return [
              createFileTreeMenuItem("copy", "复制"),
              createFileTreeMenuItem("cut", "剪切"),
              createFileTreeMenuItem("delete", "删除", { disabled: false }),
              createFileTreeMenuItem("moveTo", "移动到", {
                separatorBefore: true,
                disabled: true,
                reason: "目标选择器尚未接入 filetree 菜单",
              }),
            ];
          }

          const items = [];
          if (rowKind === "folder") {
            items.push(createFileTreeMenuItem("newPage", "新建页面", {
              disabled: !canCreatePage,
              reason: "当前文件夹不支持新建页面",
            }));
            items.push(createFileTreeMenuItem("newFolder", "新建文件夹", {
              disabled: !canCreateFolder,
              reason: convexSource ? "Convex workspace 当前未暴露 folder capability" : "当前 source 不支持新建文件夹",
            }));
            items.push(createFileTreeMenuItem("paste", "粘贴", {
              disabled: !canPaste,
              reason: "剪贴板为空",
            }));
            items.push(createFileTreeMenuItem("rename", "重命名", {
              separatorBefore: true,
              disabled: !canRename,
              reason: "当前文件夹不支持重命名",
            }));
            items.push(createFileTreeMenuItem("delete", "删除", {
              disabled: true,
              reason: "目录删除尚未收口到统一 executor",
            }));
            return items;
          }

          items.push(createFileTreeMenuItem("open", "打开", {
            disabled: !hasDocument && !hasAsset,
            reason: "当前行没有可打开资源",
          }));
          items.push(createFileTreeMenuItem("rename", "重命名", {
            disabled: !canRename,
            reason: "当前资源暂不支持重命名",
          }));
          items.push(createFileTreeMenuItem("copy", "复制", {
            disabled: !canCopyCut,
            reason: "当前资源暂不支持复制",
          }));
          items.push(createFileTreeMenuItem("cut", "剪切", {
            disabled: !canCopyCut,
            reason: "当前资源暂不支持剪切",
          }));
          items.push(createFileTreeMenuItem("paste", "粘贴", {
            disabled: !canPaste,
            reason: "剪贴板为空",
          }));
          items.push(createFileTreeMenuItem("moveTo", "移动到", {
            disabled: true,
            reason: "目标选择器尚未接入 filetree 菜单",
          }));
          items.push(createFileTreeMenuItem("delete", "删除", {
            disabled: !canDelete,
            reason: "当前资源暂不支持删除",
          }));
          items.push(createFileTreeMenuItem("reveal", "Reveal", { separatorBefore: true }));
          if (hasAsset) {
            items.push(createFileTreeMenuItem("download", "下载", {
              disabled: true,
              reason: "下载 executor 尚未接入",
            }));
          }
          if (item?.capabilities?.includes("share")) {
            items.push(createFileTreeMenuItem("share", "分享", { separatorBefore: true }));
          }
          if (item?.capabilities?.includes("publish")) {
            items.push(createFileTreeMenuItem("publish", "发布"));
          }
          return items;
        };

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

        const executeFileTreeContextMenuAction = async (kind, target) => {
          closeFileTreeContextMenu();
          const item = target.item;
          if (kind === "open") {
            if (item) openHydratedFileTreeItem(item);
            return;
          }
          if (kind === "rename") {
            if (target.rowId) beginInlineRename("filetree", target.rowId);
            return;
          }
          if (kind === "copy" || kind === "cut") {
            if (target.rowId && !selectedFileTreeRowIds.has(target.rowId)) {
              commitFileTreeSelection({
                selectedRowIds: [target.rowId],
                anchorRowId: target.rowId,
                focusedRowId: target.rowId,
              });
              syncFileTreeSelectionDom();
            }
            setFileTreeClipboard(kind);
            return;
          }
          if (kind === "paste") {
            await pasteFileTreeClipboardInto(target.rowId);
            return;
          }
          if (kind === "delete") {
            await runFileTreeDelete(target.rowId);
            return;
          }
          if (kind === "newPage") {
            const parentId = item && item.rowKind === "folder"
              ? item.nodeId
              : item && item.rowKind === "document"
                ? item.nodeId
                : null;
            await handleCreate(parentId);
            return;
          }
          if (kind === "newFolder") {
            const parentId = item && item.rowKind === "folder" ? item.nodeId : null;
            const result = await sendCommand({
              action: "createFolder",
              workspaceId,
              documentId: "",
              parentId,
              title: "新建文件夹",
            });
            setStatus("创建文件夹成功");
            setLastAction("已创建新建文件夹");
            scheduleRefresh({
              renameRowId: localCreatedRowIdFromCommandResult(result, "folder"),
            });
            return result;
          }
          if (kind === "refresh") {
            window.location.reload();
            return;
          }
          if (kind === "collapseAll") {
            expanded.clear();
            renderTree();
            return;
          }
          if (kind === "reveal") {
            setLastAction(`Reveal ${target.documentId || target.assetId || target.rowId || "root"}`);
            postToHost("tree.filetree.reveal", {
              documentId: target.documentId,
              assetId: target.assetId,
              rowId: target.rowId,
              payload: target,
            });
            return;
          }
          postToHost(`tree.filetree.${kind}`, {
            documentId: target.documentId,
            assetId: target.assetId,
            rowId: target.rowId,
            payload: target,
          });
        };

        const openFileTreeContextMenu = ({
          documentId,
          assetId,
          rowId,
          rowKind,
          clientX,
          clientY,
        }) => {
          closeFileTreeContextMenu();
          const target = buildFileTreeMenuTarget(rowId, rowKind, documentId, assetId);
          const profile = buildFileTreeContextMenuProfile(target);
          const menu = document.createElement("div");
          menu.className = "tree-context-menu";
          menu.dataset.testid = "filetree-context-menu";
          menu.dataset.sourceKind = sourceKind;
          menu.dataset.rowKind = target.rowKind;
          menu.setAttribute("role", "menu");

          profile.forEach((entry) => {
            if (entry.separatorBefore) {
              const separator = document.createElement("div");
              separator.className = "tree-menu-separator";
              separator.setAttribute("role", "separator");
              menu.appendChild(separator);
            }
            const button = document.createElement("button");
            button.type = "button";
            button.className = "tree-menu-item";
            button.dataset.menuAction = entry.kind;
            button.setAttribute("role", "menuitem");
            button.textContent = entry.label;
            if (entry.disabled) {
              button.disabled = true;
              if (entry.reason) {
                button.title = entry.reason;
                button.dataset.disabledReason = entry.reason;
              }
            } else {
              button.addEventListener("click", () => {
                void executeFileTreeContextMenuAction(entry.kind, target);
              });
            }
            menu.appendChild(button);
          });

          const closeOnOutside = (event) => {
            if (activeFileTreeMenuElement && !activeFileTreeMenuElement.contains(event.target)) {
              closeFileTreeContextMenu();
              document.removeEventListener("mousedown", closeOnOutside, true);
              document.removeEventListener("keydown", closeOnKeyDown, true);
            }
          };
          const closeOnKeyDown = (event) => {
            if (event.key === "Escape") {
              closeFileTreeContextMenu();
              document.removeEventListener("mousedown", closeOnOutside, true);
              document.removeEventListener("keydown", closeOnKeyDown, true);
            }
          };

          document.body.appendChild(menu);
          const x = Number.isFinite(clientX) ? clientX : 16;
          const y = Number.isFinite(clientY) ? clientY : 16;
          const rect = menu.getBoundingClientRect();
          menu.style.left = `${Math.max(4, Math.min(x, window.innerWidth - rect.width - 4))}px`;
          menu.style.top = `${Math.max(4, Math.min(y, window.innerHeight - rect.height - 4))}px`;
          activeFileTreeMenuElement = menu;
          window.setTimeout(() => {
            document.addEventListener("mousedown", closeOnOutside, true);
            document.addEventListener("keydown", closeOnKeyDown, true);
          }, 0);
          setLastAction(
            assetId
              ? `已打开资源 ${assetId} 的更多操作`
              : `已打开文件树节点 ${documentId || rowId || "unknown"} 的更多操作`,
          );
          postToHost("tree.filetree.context-menu", {
            documentId,
            assetId,
            rowId,
            rowKind,
            payload: {
              documentId,
              assetId,
              rowId,
              rowKind,
              x: clientX,
              y: clientY,
            },
            x: clientX,
            y: clientY,
          });
        };

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        const patchPageTreeActiveDom = () => {
          if (mode !== "page") return;
          appElement.querySelectorAll('[data-rust-rendered-row="page"]').forEach((row) => {
            if (!(row instanceof HTMLElement)) return;
            const nodeId = normalizeText(row.dataset.nodeId);
            const isFocused = nodeId === focusedNodeId;
            row.dataset.active = String(nodeId === currentActiveDocumentId);
            row.dataset.focused = String(isFocused);
            row.tabIndex = isFocused ? 0 : -1;
            row.dataset.dropFeedback = String(activePageDropNodeId === nodeId);
          });
        };

        const patchPageTreeExpansionDom = (nodeId) => {
          if (mode !== "page") return false;
          const normalizedNodeId = normalizeText(nodeId);
          if (!normalizedNodeId) return false;
          const item = itemById.get(normalizedNodeId);
          if (!item) return false;
          const nodeElement = appElement.querySelector(
            `.tree-node[data-node-id="${CSS.escape(normalizedNodeId)}"]`,
          );
          if (!(nodeElement instanceof HTMLElement)) return false;
          const row = nodeElement.querySelector(
            `:scope > .tree-row[data-node-id="${CSS.escape(normalizedNodeId)}"]`,
          );
          const children = getSiblings(normalizedNodeId);
          const hasChildren = item.childCount > 0 && children.length > 0;
          const isExpanded = hasChildren && expanded.has(normalizedNodeId);
          if (row instanceof HTMLElement) {
            row.setAttribute("aria-expanded", hasChildren ? String(isExpanded) : "false");
            const toggleButton = row.querySelector('[data-testid="tree-node-toggle"]');
            if (toggleButton instanceof HTMLButtonElement) {
              toggleButton.textContent = isExpanded ? "▾" : "▸";
              toggleButton.setAttribute(
                "aria-label",
                `${isExpanded ? "折叠" : "展开"} ${item.title}`,
              );
            }
          }
          if (!hasChildren) {
            patchPageTreeActiveDom();
            return true;
          }
          let childrenList = Array.from(nodeElement.children).find(
            (child) => child instanceof HTMLElement && child.classList.contains("tree-children"),
          );
          if (isExpanded) {
            if (!(childrenList instanceof HTMLElement)) {
              childrenList = document.createElement("ul");
              childrenList.className = "tree-children";
              children.forEach((child) => {
                childrenList.appendChild(renderNode(child));
              });
              nodeElement.appendChild(childrenList);
            }
            childrenList.hidden = false;
            childrenList.style.display = "";
          } else if (childrenList instanceof HTMLElement) {
            childrenList.hidden = true;
            childrenList.style.display = "none";
          }
          patchPageTreeActiveDom();
          return true;
        };

        const hydrateInitialPageTree = () => {
          if (mode !== "page") return false;
          const root = appElement.querySelector('[data-rust-page-renderer="initial_v1"]');
          if (!(root instanceof HTMLElement)) {
            return false;
          }
          root.querySelectorAll('[data-rust-rendered-row="page"]').forEach((row) => {
            if (!(row instanceof HTMLElement)) return;
            const nodeId = normalizeText(row.dataset.nodeId);
            const item = itemById.get(nodeId);
            if (!item) return;
            bindPageRowEvents(row, item);
          });
          if (focusedNodeId) {
            focusRowElement(focusedNodeId);
          }
          return true;
        };

        const syncFileTreeSelectionDom = () => {
          if (mode !== "filetree") return;
          appElement.querySelectorAll('[data-rust-rendered-row="filetree"]').forEach((row) => {
            if (!(row instanceof HTMLElement)) return;
            const rowId = normalizeText(row.dataset.rowId);
            row.dataset.selected = String(Boolean(rowId && selectedFileTreeRowIds.has(rowId)));
          });
        };

        const openHydratedFileTreeItem = (item) => {
          const documentId = getFileTreeRowDocumentId(item);
          const assetId = getFileTreeRowAssetId(item);
          if (item.rowKind === "document" || item.rowKind === "index") {
            handleNavigate(documentId || item.nodeId);
            return;
          }
          setLastAction(`准备打开资源 ${assetId || item.rowId}`);
          postToHost("tree.asset.open", {
            documentId: documentId || null,
            assetId: assetId || null,
            objectIdentity: item.resourceMeta?.objectIdentity || null,
            target: { documentId: documentId || null },
            payload: {
              documentId: documentId || null,
              assetId: assetId || null,
              rowId: item.rowId,
              rowKind: item.rowKind,
              objectIdentity: item.resourceMeta?.objectIdentity || null,
            },
          });
        };

        const postHydratedFileTreeDropToHost = (type, target, extra = {}) => {
          postToHost(type, {
            workspaceId,
            rowId: target.rowId,
            rowKind: target.rowKind,
            targetRowId: target.rowId,
            targetRowKind: target.rowKind,
            documentId: target.documentId,
            assetId: target.assetId,
            ...extra,
            payload: {
              workspaceId,
              rowId: target.rowId,
              rowKind: target.rowKind,
              targetRowId: target.rowId,
              targetRowKind: target.rowKind,
              documentId: target.documentId,
              assetId: target.assetId,
              ...extra,
            },
          });
        };

        const attachHydratedFileTreeDragSource = (row, item) => {
          row.draggable = true;
          row.addEventListener("dragstart", (event) => {
            const rowIds = resolveFileTreeDraggedRowIds(item.rowId);
            draggingFileTreeRowIds = rowIds;
            if (event.dataTransfer) {
              const payload = JSON.stringify({
                type: "mnote-file-tree-dnd",
                version: 1,
                rowIds,
              });
              event.dataTransfer.effectAllowed = "copyMove";
              event.dataTransfer.setData(FILETREE_DRAG_MIME, payload);
              event.dataTransfer.setData("application/x-mnote-file-tree", payload);
              event.dataTransfer.setData("text/plain", payload);
            }
            setLastAction(`开始拖拽 ${rowIds.length} 个文件树节点`);
          });
          row.addEventListener("dragend", () => {
            draggingFileTreeRowIds = [];
            clearFileTreeDropFeedback();
          });
        };

        const bindFileTreeRootEvents = (root) => {
          root.dataset.dropTarget = String(activeFileTreeRootDrop);
          root.addEventListener("mousedown", (event) => {
            if (event.target !== event.currentTarget) return;
            clearFileTreeSelection();
          });
          root.addEventListener("contextmenu", (event) => {
            if (event.target !== event.currentTarget) return;
            event.preventDefault();
            clearFileTreeSelection();
            openFileTreeContextMenu({
              documentId: null,
              assetId: null,
              rowId: null,
              rowKind: "root",
              clientX: event.clientX,
              clientY: event.clientY,
            });
          });
          root.addEventListener("dragover", (event) => {
            const internalRowIds = readFileTreeInternalDropPayload(event);
            const files = Array.from(event.dataTransfer?.files || []);
            if (!internalRowIds && files.length === 0) {
              return;
            }
            event.preventDefault();
            const target = getFileTreeDropTargetFromEvent(event);
            if (internalRowIds && !validateFileTreeInternalDrop(target, internalRowIds, event.altKey === true).ok) {
              clearFileTreeDropFeedback();
              return;
            }
            if (event.dataTransfer) {
              event.dataTransfer.dropEffect =
                files.length > 0 || event.altKey ? "copy" : "move";
            }
            setFileTreeDropFeedback(target);
          });
          root.addEventListener("dragleave", (event) => {
            const relatedTarget =
              event.relatedTarget instanceof Node ? event.relatedTarget : null;
            if (relatedTarget && root.contains(relatedTarget)) {
              return;
            }
            clearFileTreeDropFeedback();
          });
          root.addEventListener("drop", (event) => {
            const internalRowIds = readFileTreeInternalDropPayload(event);
            const files = Array.from(event.dataTransfer?.files || []);
            if (!internalRowIds && files.length === 0) {
              return;
            }
            event.preventDefault();
            const target = getFileTreeDropTargetFromEvent(event);
            if (internalRowIds && !validateFileTreeInternalDrop(target, internalRowIds, event.altKey === true).ok) {
              clearFileTreeDropFeedback();
              return;
            }
            if (files.length > 0) {
              if (sourceKind === "local_folder") {
                void executeLocalFileTreeExternalDrop(target, files);
              } else {
                void (async () => {
                  const accepted = await runFileTreePreflight("dropFiles", { target, files });
                  if (!accepted) return;
                  setLastAction(`已发送 ${files.length} 个外部文件到宿主`);
                  postHydratedFileTreeDropToHost("tree.filetree.external-drop", target, {
                    files,
                  });
                })();
              }
            } else {
              if (sourceKind === "local_folder") {
                void executeLocalFileTreeInternalDrop(target, internalRowIds, event.altKey === true);
              } else {
                void (async () => {
                  const accepted = await runFileTreePreflight("move", {
                    target,
                    rowIds: internalRowIds,
                    copy: event.altKey === true,
                  });
                  if (!accepted) return;
                  setLastAction(
                    event.altKey
                      ? `已发送复制拖放到 ${target.rowId || "根目录"}`
                      : `已发送移动拖放到 ${target.rowId || "根目录"}`,
                  );
                  postHydratedFileTreeDropToHost("tree.filetree.internal-drop", target, {
                    rowIds: internalRowIds,
                    copy: event.altKey === true,
                  });
                })();
              }
            }
            draggingFileTreeRowIds = [];
            clearFileTreeDropFeedback();
          });
        };

        const bindFileTreeRowEvents = (row, item) => {
          if (!(row instanceof HTMLElement) || !item) return;
          const documentId = getFileTreeRowDocumentId(item) || null;
          const assetId = getFileTreeRowAssetId(item) || null;
          row.dataset.active = String(
            item.rowKind === "document" && documentId === currentActiveDocumentId
          );
          row.dataset.nodeId = item.nodeId;
          row.dataset.rowId = item.rowId;
          row.dataset.rowKind = item.rowKind;
          row.dataset.documentId = documentId || "";
          row.dataset.assetId = assetId || "";
          row.dataset.objectIdentity = item.resourceMeta?.objectIdentity
            ? JSON.stringify(item.resourceMeta.objectIdentity)
            : "";
          row.dataset.objectKind = item.resourceMeta?.objectIdentity?.objectKind || "";
          row.dataset.shellMode = "filetree";
          row.dataset.selected = String(selectedFileTreeRowIds.has(item.rowId));
          row.dataset.dropTarget = String(activeFileTreeDropRowId === item.rowId);
          if (activeFileTreeDropRowId === item.rowId && activeFileTreeDropPosition) {
            row.dataset.dropPosition = activeFileTreeDropPosition;
          }
          row.tabIndex = 0;
          row.setAttribute("role", "treeitem");
          row.setAttribute("aria-level", String(item.depth + 1));
          row.setAttribute(
            "aria-expanded",
            canExpandFileTreeRow(item) ? String(expanded.has(item.nodeId)) : "false",
          );
          row.addEventListener("click", (event) => {
            selectFileTreeRow(item.rowId, event);
            row.focus({ preventScroll: true });
            syncFileTreeSelectionDom();
          });
          row.addEventListener("dblclick", () => {
            openHydratedFileTreeItem(item);
          });
          row.addEventListener("keydown", (event) => handleFileTreeKeyDown(event, item));
          row.addEventListener("contextmenu", (event) => {
            event.preventDefault();
            selectFileTreeContextRow(item.rowId);
            syncFileTreeSelectionDom();
            openFileTreeContextMenu({
              documentId,
              assetId,
              rowId: item.rowId,
              rowKind: item.rowKind,
              clientX: event.clientX,
              clientY: event.clientY,
            });
          });
          attachHydratedFileTreeDragSource(row, item);
          patchFileTreeCutDecoration();
          row.querySelectorAll("[data-rust-action]").forEach((element) => {
            if (!(element instanceof HTMLElement)) return;
            element.addEventListener("click", (event) => {
              const action = normalizeText(element.dataset.rustAction);
              if (action === "open") {
                openHydratedFileTreeItem(item);
                return;
              }
              if (action === "menu") {
                event.preventDefault();
                event.stopPropagation();
                selectFileTreeContextRow(item.rowId);
                syncFileTreeSelectionDom();
                const center = getElementCenter(element);
                openFileTreeContextMenu({
                  documentId,
                  assetId,
                  rowId: item.rowId,
                  rowKind: item.rowKind,
                  clientX: center.x,
                  clientY: center.y,
                });
              }
            });
          });
        };

        const hydrateInitialFileTree = () => {
          if (mode !== "filetree") return false;
          const root = appElement.querySelector('[data-rust-filetree-renderer="initial_v1"]');
          if (!(root instanceof HTMLElement)) {
            return false;
          }
          visibleFileTreeRowIds = [];
          bindFileTreeRootEvents(root);
          root.querySelectorAll('[data-rust-rendered-row="filetree"]').forEach((row) => {
            if (!(row instanceof HTMLElement)) return;
            const rowId = normalizeText(row.dataset.rowId);
            const nodeId = normalizeText(row.dataset.nodeId);
            const item = fileTreeRowById.get(rowId) || itemById.get(nodeId);
            if (!item) return;
            visibleFileTreeRowIds.push(item.rowId);
            bindFileTreeRowEvents(row, item);
          });
          normalizeFileTreeSelectionForVisibleRows();
          syncFileTreeSelectionDom();
          return true;
        };

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

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

        const focusPickerRowElement = (pickerItemKey) => {
          const normalizedItemKey = normalizeText(pickerItemKey);
          window.requestAnimationFrame(() => {
            const row =
              normalizedItemKey === "__root__"
                ? appElement.querySelector('[data-rust-rendered-row="picker-root"]')
                : appElement.querySelector(
                    `.tree-row[data-node-id="${CSS.escape(normalizedItemKey)}"]`,
                  );
            if (!(row instanceof HTMLElement)) return;
            row.focus({ preventScroll: true });
            row.scrollIntoView({ block: "nearest" });
          });
        };

        const patchPickerActiveDom = () => {
          if (mode !== "picker") return;
          appElement
            .querySelectorAll('[data-rust-rendered-row="picker"], [data-rust-rendered-row="picker-root"]')
            .forEach((row) => {
              if (!(row instanceof HTMLElement)) return;
              const nodeId = normalizeText(row.dataset.nodeId);
              const isRoot = row.dataset.rustRenderedRow === "picker-root";
              const isFocused = isRoot
                ? currentActivePickerItemKey === "__root__"
                : currentActivePickerItemKey === nodeId ||
                  (!currentActivePickerItemKey && currentActiveDocumentId === nodeId);
              row.dataset.focused = String(isFocused);
              row.tabIndex = isFocused ? 0 : -1;
            });
        };

        const hydrateInitialPickerTree = () => {
          if (mode !== "picker") return false;
          const root = appElement.querySelector('[data-rust-picker-renderer="initial_v1"]');
          if (!(root instanceof HTMLElement)) {
            return false;
          }
          root.querySelectorAll('[data-rust-rendered-row="picker-root"]').forEach((row) => {
            bindPickerRootEvents(row);
          });
          root.querySelectorAll('[data-rust-rendered-row="picker"]').forEach((row) => {
            if (!(row instanceof HTMLElement)) return;
            const nodeId = normalizeText(row.dataset.nodeId);
            const item = itemById.get(nodeId);
            if (!item) return;
            bindPickerRowEvents(row, item);
          });
          return true;
        };

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

        const createKindBadge = (kind) => {
          const badge = document.createElement("span");
          badge.className = "tree-kind-badge";
          badge.dataset.kind = kind;
          badge.innerHTML =
            kind === "mindmap"
              ? ICONS.mindmap
              : kind === "table"
                ? ICONS.table
                : kind === "pdf"
                  ? ICONS.pdf
                  : kind === "book"
                    ? ICONS.book
                    : kind === "image"
                      ? ICONS.image
                      : kind === "video"
                        ? ICONS.video
                        : kind === "audio"
                          ? ICONS.audio
                  : kind === "index"
                    ? ICONS.index
                    : kind === "page"
                    ? ICONS.page
                    : ICONS.file;
          return badge;
        };

        const createActionButton = (icon, testId, title, onClick, disabled) => {
          const button = document.createElement("button");
          button.type = "button";
          button.className = "tree-action";
          button.dataset.testid = testId;
          button.title = title;
          button.setAttribute("aria-label", title);
          button.innerHTML = icon;
          button.disabled = disabled || busy;
          button.addEventListener("click", (event) => {
            event.stopPropagation();
            onClick(button);
          });
          return button;
        };

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

          if (hasChildren) {
            const toggleButton = document.createElement("button");
            toggleButton.type = "button";
            toggleButton.className = "tree-toggle";
            toggleButton.setAttribute("data-testid", "tree-node-toggle");
            toggleButton.setAttribute(
              "aria-label",
              `${expanded.has(item.nodeId) ? "折叠" : "展开"} ${item.title}`,
            );
            toggleButton.textContent = expanded.has(item.nodeId) ? "▾" : "▸";
            toggleButton.addEventListener("click", (event) => {
              event.stopPropagation();
              applyPageKeyboardAction({ kind: "toggle", nodeId: item.nodeId }, item, event.currentTarget);
            });
            row.appendChild(toggleButton);
          } else {
            const spacer = document.createElement("div");
            spacer.className = "tree-spacer";
            row.appendChild(spacer);
          }

          row.appendChild(createKindBadge("page"));

          const linkButton = document.createElement("button");
          linkButton.type = "button";
          linkButton.className = "tree-link";
          linkButton.setAttribute("data-testid", "tree-node-open");
          linkButton.setAttribute("aria-label", `打开 ${item.title}`);
          linkButton.addEventListener("click", () => {
            if (mode === "picker") {
              applyPickerFocusByItemKey(item.nodeId, { focusDom: true });
              postPickerPickResultToHost(applyPickerStateAction({ kind: "pick" }));
              return;
            }
            handleNavigate(item.nodeId);
          });

          if (inlineRenameState.mode === "page" && inlineRenameState.id === item.nodeId) {
            const renameInput = document.createElement("input");
            renameInput.type = "text";
            renameInput.className = "tree-rename-input";
            renameInput.value = item.title;
            renameInput.setAttribute("aria-label", `重命名 ${item.title}`);
            renameInput.dataset.renameId = item.nodeId;
            attachInlineRenameInput(renameInput, item.title);
            linkButton.appendChild(renameInput);
          } else {
            const titleElement = document.createElement("span");
            titleElement.className = "tree-link-title";
            titleElement.textContent = item.title;
            linkButton.appendChild(titleElement);
          }

          const metaElement = document.createElement("span");
          metaElement.className = "tree-link-meta";
          metaElement.textContent = `${item.nodeId} · ${item.childCount} 个子页面`;
          linkButton.appendChild(metaElement);
          row.appendChild(linkButton);

          const actions = document.createElement("div");
          actions.className = "tree-actions";
          const siblingRows = getSiblings(item.parentNodeId);
          const siblingIndex = siblingRows.findIndex((entry) => entry.nodeId === item.nodeId);
          actions.appendChild(
            createActionButton(
              ICONS.add,
              "tree-action-create",
              `在 ${item.title} 下新建子页面`,
              () => handleCreate(item.nodeId),
              false,
            ),
          );
          if (mode === "page") {
            actions.appendChild(
              createActionButton(
                ICONS.edit,
                "tree-action-rename",
                `重命名 ${item.title}`,
                () => beginInlineRename("page", item.nodeId),
                false,
              ),
            );
            actions.appendChild(
              createActionButton(
                ICONS.up,
                "tree-action-move-up",
                `上移 ${item.title}`,
                () => void handleMove(item.nodeId, -1),
                siblingIndex <= 0,
              ),
            );
            actions.appendChild(
              createActionButton(
                ICONS.more,
                "tree-action-menu",
                `打开 ${item.title} 的更多操作`,
                (button) => {
                  const center = getElementCenter(button);
                  openContextMenu(
                    item.nodeId,
                    center.x,
                    center.y,
                  );
                },
                false,
              ),
            );
          }
          row.appendChild(actions);

          const nodeElement = document.createElement("li");
          nodeElement.className = "tree-node";
          nodeElement.dataset.nodeId = item.nodeId;
          nodeElement.appendChild(row);

          if (hasChildren && expanded.has(item.nodeId)) {
            const children = getSiblings(item.nodeId);
            if (children.length > 0) {
              const childrenList = document.createElement("ul");
              childrenList.className = "tree-children";
              children.forEach((child) => {
                childrenList.appendChild(renderNode(child));
              });
              nodeElement.appendChild(childrenList);
            }
          }

          return nodeElement;
        };

        const renderFileTree = () => {
          appElement.innerHTML = "";
          visibleFileTreeRowIds = [];
          clearFileTreeDropFeedback();

          const fileRoot = document.createElement("div");
          fileRoot.className = "tree-root";
          fileRoot.setAttribute("role", "tree");
          fileRoot.dataset.dropTarget = String(activeFileTreeRootDrop);
          fileRoot.addEventListener("mousedown", (event) => {
            if (event.target !== event.currentTarget) return;
            clearFileTreeSelection();
          });

          const openFileTreeItem = (item) => {
            const documentId = getFileTreeRowDocumentId(item);
            const assetId = getFileTreeRowAssetId(item);
            if (item.rowKind === "document" || item.rowKind === "index" || item.rowKind === "markdown") {
              handleNavigate(documentId || item.nodeId);
              return;
            }
            setLastAction(`准备打开资源 ${assetId || item.rowId}`);
            postToHost("tree.asset.open", {
              documentId: documentId || null,
              assetId: assetId || null,
              objectIdentity: item.resourceMeta?.objectIdentity || null,
              target: { documentId: documentId || null },
              payload: {
                documentId: documentId || null,
                assetId: assetId || null,
                rowId: item.rowId,
                rowKind: item.rowKind,
                objectIdentity: item.resourceMeta?.objectIdentity || null,
              },
            });
          };

          const postFileTreeDropToHost = (type, target, extra = {}) => {
            postToHost(type, {
              workspaceId,
              rowId: target.rowId,
              rowKind: target.rowKind,
              targetRowId: target.rowId,
              targetRowKind: target.rowKind,
              documentId: target.documentId,
              assetId: target.assetId,
              ...extra,
              payload: {
                workspaceId,
                rowId: target.rowId,
                rowKind: target.rowKind,
                targetRowId: target.rowId,
                targetRowKind: target.rowKind,
                documentId: target.documentId,
                assetId: target.assetId,
                ...extra,
              },
            });
          };

          const attachFileTreeDragSource = (row, item) => {
            row.draggable = true;
            row.addEventListener("dragstart", (event) => {
              const rowIds = resolveFileTreeDraggedRowIds(item.rowId);
              draggingFileTreeRowIds = rowIds;
              if (event.dataTransfer) {
                const payload = JSON.stringify({
                  type: "mnote-file-tree-dnd",
                  version: 1,
                  rowIds,
                });
                event.dataTransfer.effectAllowed = "copyMove";
                event.dataTransfer.setData(FILETREE_DRAG_MIME, payload);
                event.dataTransfer.setData("application/x-mnote-file-tree", payload);
                event.dataTransfer.setData("text/plain", payload);
              }
              setLastAction(`开始拖拽 ${rowIds.length} 个文件树节点`);
            });
            row.addEventListener("dragend", () => {
              draggingFileTreeRowIds = [];
              clearFileTreeDropFeedback();
            });
          };

          const appendFileTreeRow = (container, item) => {
            const children = getSiblings(item.nodeId);
            const hasBranches = canExpandFileTreeRow(item) && children.length > 0;
            const documentId = getFileTreeRowDocumentId(item) || null;
            const assetId = getFileTreeRowAssetId(item) || null;

            const row = document.createElement("div");
            row.className = "tree-row";
            row.style.marginLeft = `${item.depth * 22}px`;
            row.dataset.active = String(
              item.rowKind === "document" && documentId === currentActiveDocumentId
            );
            row.dataset.nodeId = item.nodeId;
            row.dataset.rowId = item.rowId;
            row.dataset.rowKind = item.rowKind;
            row.dataset.documentId = documentId || "";
            row.dataset.assetId = assetId || "";
            row.dataset.objectIdentity = item.resourceMeta?.objectIdentity
              ? JSON.stringify(item.resourceMeta.objectIdentity)
              : "";
            row.dataset.objectKind = item.resourceMeta?.objectIdentity?.objectKind || "";
            row.dataset.shellMode = "filetree";
            row.dataset.selected = String(selectedFileTreeRowIds.has(item.rowId));
            row.dataset.dropTarget = String(activeFileTreeDropRowId === item.rowId);
            if (activeFileTreeDropRowId === item.rowId && activeFileTreeDropPosition) {
              row.dataset.dropPosition = activeFileTreeDropPosition;
            }
            row.setAttribute(
              "data-testid",
              item.rowKind === "document"
                ? "filetree-doc-row"
                : item.rowKind === "index"
                  ? "filetree-index-row"
                  : "filetree-asset-row",
            );
            row.tabIndex = 0;
            row.setAttribute("role", "treeitem");
            row.setAttribute("aria-level", String(item.depth + 1));
            row.setAttribute("aria-expanded", hasBranches ? String(expanded.has(item.nodeId)) : "false");
            visibleFileTreeRowIds.push(item.rowId);

            row.addEventListener("click", (event) => {
              selectFileTreeRow(item.rowId, event);
              renderTree();
              const nextRow = appElement.querySelector(
                `.tree-row[data-shell-mode="filetree"][data-row-id="${CSS.escape(item.rowId)}"]`,
              );
              if (nextRow instanceof HTMLElement) {
                nextRow.focus({ preventScroll: true });
              }
            });
            row.addEventListener("dblclick", () => {
              openFileTreeItem(item);
            });
            row.addEventListener("keydown", (event) => handleFileTreeKeyDown(event, item));
            row.addEventListener("contextmenu", (event) => {
              event.preventDefault();
              selectFileTreeContextRow(item.rowId);
              renderTree();
              openFileTreeContextMenu({
                documentId,
                assetId,
                rowId: item.rowId,
                rowKind: item.rowKind,
                clientX: event.clientX,
                clientY: event.clientY,
              });
            });
            attachFileTreeDragSource(row, item);

            if (hasBranches) {
              const toggleButton = document.createElement("button");
              toggleButton.type = "button";
              toggleButton.className = "tree-toggle";
              toggleButton.setAttribute(
                "aria-label",
                `${expanded.has(item.nodeId) ? "折叠" : "展开"} ${item.title}`,
              );
              toggleButton.textContent = expanded.has(item.nodeId) ? "▾" : "▸";
              toggleButton.addEventListener("click", (event) => {
                event.stopPropagation();
                toggleExpand(item.nodeId);
              });
              row.appendChild(toggleButton);
            } else {
              const spacer = document.createElement("div");
              spacer.className = "tree-spacer";
              row.appendChild(spacer);
            }

            row.appendChild(createKindBadge(getFileTreeRowIconKind(item)));

            const linkButton = document.createElement("button");
            linkButton.type = "button";
            linkButton.className = "tree-link";
            if (item.rowKind === "document") {
              linkButton.setAttribute("data-testid", "filetree-doc-open");
            } else if (item.rowKind === "asset" || item.rowKind === "asset_folder") {
              linkButton.setAttribute("data-testid", "filetree-asset-open");
            }
            linkButton.addEventListener("click", () => openFileTreeItem(item));
            if (inlineRenameState.mode === "filetree" && inlineRenameState.id === item.rowId) {
              const renameInput = document.createElement("input");
              renameInput.type = "text";
              renameInput.className = "tree-rename-input";
              renameInput.value = item.title;
              renameInput.setAttribute("aria-label", `重命名 ${item.title}`);
              renameInput.dataset.renameId = item.rowId;
              attachInlineRenameInput(renameInput, item.title);
              linkButton.appendChild(renameInput);
            } else {
              const title = document.createElement("span");
              title.className = "tree-link-title";
              title.textContent = item.title;
              linkButton.appendChild(title);
            }
            const meta = document.createElement("span");
            meta.className = "tree-link-meta";
            meta.textContent = getFileTreeRowMetaLabel(item);
            linkButton.appendChild(meta);
            row.appendChild(linkButton);

            const actions = document.createElement("div");
            actions.className = "tree-actions";
            actions.appendChild(
              createActionButton(
                ICONS.edit,
                "filetree-action-rename",
                `重命名 ${item.title}`,
                () => beginInlineRename("filetree", item.rowId),
                false,
              ),
            );
            actions.appendChild(
              createActionButton(
                ICONS.more,
                "filetree-action-menu",
                `打开 ${item.title} 的更多操作`,
                (button) => {
                  const center = getElementCenter(button);
                  openFileTreeContextMenu({
                    documentId,
                    assetId,
                    rowId: item.rowId,
                    rowKind: item.rowKind,
                    clientX: center.x,
                    clientY: center.y,
                  });
                },
                false,
              ),
            );
            row.appendChild(actions);
            container.appendChild(row);

            if (!hasBranches || !expanded.has(item.nodeId)) return;
            children.forEach((child) => appendFileTreeRow(container, child));
          };

          fileRoot.addEventListener("dragover", (event) => {
            const internalRowIds = readFileTreeInternalDropPayload(event);
            const files = Array.from(event.dataTransfer?.files || []);
            if (!internalRowIds && files.length === 0) {
              return;
            }
            event.preventDefault();
            const target = getFileTreeDropTargetFromEvent(event);
            if (internalRowIds && !validateFileTreeInternalDrop(target, internalRowIds, event.altKey === true).ok) {
              clearFileTreeDropFeedback();
              return;
            }
            if (event.dataTransfer) {
              event.dataTransfer.dropEffect =
                files.length > 0 || event.altKey ? "copy" : "move";
            }
            setFileTreeDropFeedback(target);
          });
          fileRoot.addEventListener("dragleave", (event) => {
            const relatedTarget =
              event.relatedTarget instanceof Node ? event.relatedTarget : null;
            if (relatedTarget && fileRoot.contains(relatedTarget)) {
              return;
            }
            clearFileTreeDropFeedback();
          });
          fileRoot.addEventListener("contextmenu", (event) => {
            if (event.target !== event.currentTarget) return;
            event.preventDefault();
            clearFileTreeSelection();
            openFileTreeContextMenu({
              documentId: null,
              assetId: null,
              rowId: null,
              rowKind: "root",
              clientX: event.clientX,
              clientY: event.clientY,
            });
          });
          fileRoot.addEventListener("drop", (event) => {
            const internalRowIds = readFileTreeInternalDropPayload(event);
            const files = Array.from(event.dataTransfer?.files || []);
            if (!internalRowIds && files.length === 0) {
              return;
            }
            event.preventDefault();
            const target = getFileTreeDropTargetFromEvent(event);
            if (internalRowIds && !validateFileTreeInternalDrop(target, internalRowIds, event.altKey === true).ok) {
              clearFileTreeDropFeedback();
              return;
            }
            if (files.length > 0) {
              if (sourceKind === "local_folder") {
                void executeLocalFileTreeExternalDrop(target, files);
              } else {
                void (async () => {
                  const accepted = await runFileTreePreflight("dropFiles", { target, files });
                  if (!accepted) return;
                  setLastAction(`已发送 ${files.length} 个外部文件到宿主`);
                  postFileTreeDropToHost("tree.filetree.external-drop", target, {
                    files,
                  });
                })();
              }
            } else {
              if (sourceKind === "local_folder") {
                void executeLocalFileTreeInternalDrop(target, internalRowIds, event.altKey === true);
              } else {
                void (async () => {
                  const accepted = await runFileTreePreflight("move", {
                    target,
                    rowIds: internalRowIds,
                    copy: event.altKey === true,
                  });
                  if (!accepted) return;
                  setLastAction(
                    event.altKey
                      ? `已发送复制拖放到 ${target.rowId || "根目录"}`
                      : `已发送移动拖放到 ${target.rowId || "根目录"}`,
                  );
                  postFileTreeDropToHost("tree.filetree.internal-drop", target, {
                    rowIds: internalRowIds,
                    copy: event.altKey === true,
                  });
                })();
              }
            }
            draggingFileTreeRowIds = [];
            clearFileTreeDropFeedback();
          });

          if (roots.length === 0) {
            const empty = document.createElement("div");
            empty.className = "tree-empty";
            empty.textContent = "当前 file tree 没有可渲染的页面。";
            fileRoot.appendChild(empty);
          } else {
            roots.forEach((item) => appendFileTreeRow(fileRoot, item));
          }
          normalizeFileTreeSelectionForVisibleRows();
          appElement.appendChild(fileRoot);
          patchFileTreeCutDecoration();
        };

        const renderTree = () => {
          if (mode === "filetree") {
            renderFileTree();
            return;
          }
          appElement.innerHTML = "";
          if (mode === "picker" && allowRootPick) {
            const rootButton = document.createElement("button");
            rootButton.type = "button";
            rootButton.className = "tree-row";
            rootButton.setAttribute("data-testid", "tree-picker-root");
            rootButton.dataset.focused = String(resolvePickerRootFocused());
            rootButton.tabIndex = resolvePickerRootFocused() ? 0 : -1;
            rootButton.addEventListener("click", () => {
              applyPickerFocusByItemKey("__root__", { focusDom: true });
              postPickerPickResultToHost(applyPickerStateAction({ kind: "pick" }));
            });

            const spacer = document.createElement("div");
            spacer.className = "tree-spacer";
            rootButton.appendChild(spacer);

            const label = document.createElement("div");
            label.className = "tree-link";
            const title = document.createElement("span");
            title.className = "tree-link-title";
            title.textContent = "根目录";
            const meta = document.createElement("span");
            meta.className = "tree-link-meta";
            meta.textContent = "选择工作空间根目录";
            label.appendChild(title);
            label.appendChild(meta);
            rootButton.appendChild(label);
            appElement.appendChild(rootButton);
          }

          if (roots.length === 0) {
            const empty = document.createElement("div");
            empty.className = "tree-empty";
            empty.textContent =
              mode === "picker"
                ? "当前 projection 没有可选择的页面。"
                : "当前 projection 没有可渲染的页面，点击上方按钮先创建一个根页面。";
            appElement.appendChild(empty);
            return;
          }
          const list = document.createElement("ul");
          list.className = "tree-root";
          list.setAttribute("role", "tree");
          roots.forEach((item) => {
            list.appendChild(renderNode(item));
          });
          appElement.appendChild(list);
          if (mode === "page" && focusedNodeId) {
            focusRowElement(focusedNodeId);
          }
        };

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

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

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

          if (!changed) {
            return;
          }

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

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

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

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

    template
        .replace("__WORKSPACE_ID__", &escape_html(workspace_id))
        .replace("__ROOT_LABEL__", &escape_html(root_label))
        .replace("__ACTIVE_LABEL__", &escape_html(active_label))
        .replace("__PROJECTION_JSON__", &escape_html(&projection_json))
        .replace("__INITIAL_TREE_HTML__", &initial_tree_html)
        .replace("__APP_STATE__", &escape_inline_json(&app_state_json))
}

fn json_response(context: &RequestContext, result: Value) -> (StatusCode, Json<Value>) {
    (
        StatusCode::OK,
        Json(json!({
            "ok": true,
            "requestId": context.trace.request_id,
            "traceId": context.trace.trace_id,
            "result": result,
        })),
    )
}

pub async fn local_folder_watch(
    Extension(context): Extension<RequestContext>,
    Query(query): Query<LocalFolderWatchQuery>,
) -> Result<(StatusCode, Json<Value>), WebError> {
    let revision = local_folder_watch_revision(&query.root_uri)?;
    Ok(json_response(
        &context,
        json!({
            "sourceKind": "local_folder",
            "rootUri": revision.root_uri,
            "revision": revision.revision,
            "entryCount": revision.entry_count,
            "latestModifiedMs": revision.latest_modified_ms,
        }),
    ))
}

pub async fn tree_shell(
    State(state): State<AppState>,
    Extension(context): Extension<RequestContext>,
    Query(query): Query<TreeShellQuery>,
) -> Result<Response, WebError> {
    let effective_context = override_actor_context(&context, query.actor_id.as_deref());
    let mode = normalize_tree_mode(query.mode.as_deref());
    let allow_root_pick = normalize_bool_flag(query.allow_root_pick.as_deref(), false);
    let exclude_ids = parse_exclude_ids(query.exclude_ids.as_deref());
    let source_kind = query
        .source_kind
        .as_deref()
        .map(str::trim)
        .filter(|value| !value.is_empty());
    let (effective_workspace_id, snapshot) = if source_kind == Some("local_folder") {
        let root_uri = query
            .root_uri
            .as_deref()
            .map(str::trim)
            .filter(|value| !value.is_empty())
            .ok_or_else(|| {
                WebError::bad_request_code("local_folder_root_required", "缺少本地文件夹 rootUri")
            })?;
        (
            local_workspace_id_from_root_uri(root_uri)?,
            if mode == "filetree" {
                load_local_folder_file_tree_snapshot(root_uri)?
            } else {
                load_local_folder_page_tree_snapshot(root_uri)?
            },
        )
    } else {
        let effective_workspace_id = resolve_effective_workspace_id(
            &effective_context,
            query.workspace_id.as_deref(),
            true,
        )?
        .expect("workspace_required 已确保存在");
        let snapshot = load_projection_snapshot(
            state.config(),
            &effective_context,
            &ProjectionSnapshotSpec {
                workspace_id: &effective_workspace_id,
                root_node_id: query.root_node_id.as_deref(),
                depth: query.depth,
                query: None,
                max_results: None,
                projection: if mode == "filetree" {
                    KernelProjectionKind::FileTree
                } else {
                    KernelProjectionKind::PageTree
                },
            },
        )
        .await?;
        (effective_workspace_id, snapshot)
    };
    let html = build_tree_shell_html(
        &effective_workspace_id,
        query.root_node_id.as_deref(),
        query.active_document_id.as_deref(),
        query.focused_document_id.as_deref(),
        query.active_picker_item_key.as_deref(),
        &normalize_channel(query.channel),
        query.host.as_deref(),
        &effective_context,
        &snapshot.projection,
        mode,
        allow_root_pick,
        &exclude_ids,
        &snapshot.dataset,
    );
    let mut response = Html(html).into_response();
    response.headers_mut().insert(
        header::CONTENT_TYPE,
        HeaderValue::from_static("text/html; charset=utf-8"),
    );
    Ok(response)
}

fn create_command_wire(
    context: &RequestContext,
    workspace_id: &str,
    request: TreeCommandRequest,
    envelope_context: &TreeCommandEnvelopeContext,
) -> Result<RuntimeCommandEnvelopeWire, WebError> {
    match request {
        TreeCommandRequest::Create {
            workspace_id: _,
            document_id,
            parent_id,
            title,
            access_scope,
            content,
        } => {
            let document_id = ensure_non_empty(&document_id, "documentId", context)?;
            let parent_id = read_optional_non_empty(parent_id);
            let access_scope =
                read_optional_non_empty(access_scope).unwrap_or_else(|| "private".into());
            Ok(RuntimeCommandEnvelopeWire {
                name: "tree.node.create".into(),
                command_id: format!("tree_create_{}", context.trace.request_id),
                idempotency_key: context.source.idempotency_key.clone(),
                actor: bridge_runtime::RuntimeActorWire {
                    actor_type: context.auth.actor_type.clone(),
                    actor_id: context.auth.actor_id.clone(),
                    session_id: context.auth.session_id.clone(),
                },
                source: build_workspace_source_wire(context, workspace_id, envelope_context),
                target: Some(build_tree_target(
                    workspace_id,
                    Some(document_id.as_str()),
                    None,
                )),
                payload: attach_tree_command_envelope_context(
                    json!({
                        "documentId": document_id,
                        "workspaceId": workspace_id,
                        "parentId": parent_id,
                        "title": title,
                        "accessScope": access_scope,
                        "content": content.unwrap_or_else(|| Value::Array(Vec::new())),
                    }),
                    envelope_context,
                ),
                preflight_data: None,
                reason: Some("tree-shell create".into()),
                refs: vec!["mnote-web-tree".into()],
                dry_run: false,
                validate_only: false,
            })
        }
        TreeCommandRequest::CreateFolder { .. } => Err(WebError::bad_request_code(
            "tree_command_validation",
            "convex_workspace 暂不支持 folder create capability",
        )
        .with_context(context)
        .with_header("x-error-phase", "tree_command_validate")),
        TreeCommandRequest::Rename {
            workspace_id: _,
            document_id,
            title,
        } => {
            let document_id = ensure_non_empty(&document_id, "documentId", context)?;
            Ok(RuntimeCommandEnvelopeWire {
                name: "tree.node.rename".into(),
                command_id: format!("tree_rename_{}", context.trace.request_id),
                idempotency_key: context.source.idempotency_key.clone(),
                actor: bridge_runtime::RuntimeActorWire {
                    actor_type: context.auth.actor_type.clone(),
                    actor_id: context.auth.actor_id.clone(),
                    session_id: context.auth.session_id.clone(),
                },
                source: build_workspace_source_wire(context, workspace_id, envelope_context),
                target: Some(build_tree_target(
                    workspace_id,
                    Some(document_id.as_str()),
                    None,
                )),
                payload: attach_tree_command_envelope_context(
                    json!({
                        "documentId": document_id,
                        "title": title,
                    }),
                    envelope_context,
                ),
                preflight_data: None,
                reason: Some("tree-shell rename".into()),
                refs: vec!["mnote-web-tree".into()],
                dry_run: false,
                validate_only: false,
            })
        }
        TreeCommandRequest::Move {
            workspace_id: _,
            document_id,
            parent_id,
            sort_order,
        } => {
            let document_id = ensure_non_empty(&document_id, "documentId", context)?;
            let sort_order = ensure_sort_order(sort_order, context)?;
            let parent_id = read_optional_non_empty(parent_id);
            Ok(RuntimeCommandEnvelopeWire {
                name: "tree.subtree.move".into(),
                command_id: format!("tree_move_{}", context.trace.request_id),
                idempotency_key: context.source.idempotency_key.clone(),
                actor: bridge_runtime::RuntimeActorWire {
                    actor_type: context.auth.actor_type.clone(),
                    actor_id: context.auth.actor_id.clone(),
                    session_id: context.auth.session_id.clone(),
                },
                source: build_workspace_source_wire(context, workspace_id, envelope_context),
                target: Some(build_tree_target(
                    workspace_id,
                    Some(document_id.as_str()),
                    None,
                )),
                payload: attach_tree_command_envelope_context(
                    json!({
                        "documentId": document_id,
                        "parentId": parent_id,
                        "sortOrder": sort_order,
                    }),
                    envelope_context,
                ),
                preflight_data: None,
                reason: Some("tree-shell move".into()),
                refs: vec!["mnote-web-tree".into()],
                dry_run: false,
                validate_only: false,
            })
        }
        TreeCommandRequest::Archive {
            workspace_id: _,
            document_id,
        } => {
            let document_id = ensure_non_empty(&document_id, "documentId", context)?;
            Ok(RuntimeCommandEnvelopeWire {
                name: "tree.node.archive".into(),
                command_id: format!("tree_archive_{}", context.trace.request_id),
                idempotency_key: context.source.idempotency_key.clone(),
                actor: bridge_runtime::RuntimeActorWire {
                    actor_type: context.auth.actor_type.clone(),
                    actor_id: context.auth.actor_id.clone(),
                    session_id: context.auth.session_id.clone(),
                },
                source: build_workspace_source_wire(context, workspace_id, envelope_context),
                target: Some(build_tree_target(
                    workspace_id,
                    Some(document_id.as_str()),
                    None,
                )),
                payload: attach_tree_command_envelope_context(
                    json!({
                        "documentId": document_id,
                        "workspaceId": workspace_id,
                    }),
                    envelope_context,
                ),
                preflight_data: None,
                reason: Some("tree-shell archive".into()),
                refs: vec!["mnote-web-tree".into()],
                dry_run: false,
                validate_only: false,
            })
        }
        TreeCommandRequest::Restore {
            workspace_id: _,
            document_id,
        } => {
            let document_id = ensure_non_empty(&document_id, "documentId", context)?;
            Ok(RuntimeCommandEnvelopeWire {
                name: "tree.node.restore".into(),
                command_id: format!("tree_restore_{}", context.trace.request_id),
                idempotency_key: context.source.idempotency_key.clone(),
                actor: bridge_runtime::RuntimeActorWire {
                    actor_type: context.auth.actor_type.clone(),
                    actor_id: context.auth.actor_id.clone(),
                    session_id: context.auth.session_id.clone(),
                },
                source: build_workspace_source_wire(context, workspace_id, envelope_context),
                target: Some(build_tree_target(
                    workspace_id,
                    Some(document_id.as_str()),
                    None,
                )),
                payload: attach_tree_command_envelope_context(
                    json!({
                        "documentId": document_id,
                        "workspaceId": workspace_id,
                    }),
                    envelope_context,
                ),
                preflight_data: None,
                reason: Some("tree-shell restore".into()),
                refs: vec!["mnote-web-tree".into()],
                dry_run: false,
                validate_only: false,
            })
        }
        TreeCommandRequest::Copy {
            workspace_id: _,
            document_id,
            target_parent_id,
            items,
            title: _,
        } => {
            let fallback_document_id = ensure_non_empty(&document_id, "documentId", context)?;
            let copy_items = if items.is_empty() {
                vec![json!({
                    "documentId": fallback_document_id,
                    "recursive": true,
                })]
            } else {
                items
                    .into_iter()
                    .map(|item| {
                        Ok(json!({
                            "documentId": ensure_non_empty(&item.document_id, "items.documentId", context)?,
                            "recursive": item.recursive,
                        }))
                    })
                    .collect::<Result<Vec<_>, WebError>>()?
            };
            let target_parent_id = read_optional_non_empty(target_parent_id);
            Ok(RuntimeCommandEnvelopeWire {
                name: "tree.subtree.copy".into(),
                command_id: format!("tree_copy_{}", context.trace.request_id),
                idempotency_key: context.source.idempotency_key.clone(),
                actor: bridge_runtime::RuntimeActorWire {
                    actor_type: context.auth.actor_type.clone(),
                    actor_id: context.auth.actor_id.clone(),
                    session_id: context.auth.session_id.clone(),
                },
                source: build_workspace_source_wire(context, workspace_id, envelope_context),
                target: Some(build_tree_target(
                    workspace_id,
                    target_parent_id.as_deref(),
                    None,
                )),
                payload: attach_tree_command_envelope_context(
                    json!({
                        "workspaceId": workspace_id,
                        "targetParentId": target_parent_id,
                        "items": copy_items,
                    }),
                    envelope_context,
                ),
                preflight_data: None,
                reason: Some("tree-shell copy".into()),
                refs: vec!["mnote-web-tree".into()],
                dry_run: false,
                validate_only: false,
            })
        }
        TreeCommandRequest::DropFiles { .. } => Err(WebError::bad_request_code(
            "tree_command_validation",
            "convex_workspace 外部文件 drop 需要走上传 preflight / object storage executor",
        )
        .with_context(context)
        .with_header("x-error-phase", "tree_command_validate")),
        TreeCommandRequest::Purge {
            workspace_id: _,
            document_id,
        } => {
            let document_id = ensure_non_empty(&document_id, "documentId", context)?;
            Ok(RuntimeCommandEnvelopeWire {
                name: "tree.node.purge".into(),
                command_id: format!("tree_purge_{}", context.trace.request_id),
                idempotency_key: context.source.idempotency_key.clone(),
                actor: bridge_runtime::RuntimeActorWire {
                    actor_type: context.auth.actor_type.clone(),
                    actor_id: context.auth.actor_id.clone(),
                    session_id: context.auth.session_id.clone(),
                },
                source: build_workspace_source_wire(context, workspace_id, envelope_context),
                target: Some(build_tree_target(
                    workspace_id,
                    Some(document_id.as_str()),
                    None,
                )),
                payload: attach_tree_command_envelope_context(
                    json!({
                        "documentId": document_id,
                        "workspaceId": workspace_id,
                    }),
                    envelope_context,
                ),
                preflight_data: None,
                reason: Some("tree-shell purge".into()),
                refs: vec!["mnote-web-tree".into()],
                dry_run: false,
                validate_only: false,
            })
        }
    }
}

async fn load_tree_move_preflight_data(
    state: &AppState,
    context: &RequestContext,
    workspace_id: &str,
) -> Result<Value, WebError> {
    let spec = ProjectionSnapshotSpec {
        workspace_id,
        root_node_id: None,
        depth: Some(99),
        projection: KernelProjectionKind::SidebarTree,
        query: None,
        max_results: None,
    };
    let snapshot = load_projection_snapshot(state.config(), context, &spec)
        .await
        .map_err(|error| {
            WebError::bad_gateway_code(
                "tree_move_preflight_snapshot_failed",
                format!("移动前排序快照加载失败: {}", error.message()),
            )
            .with_context(context)
            .with_header("x-error-phase", "tree_move_preflight_snapshot")
        })?;
    let documents = snapshot
        .dataset
        .get("documents")
        .cloned()
        .unwrap_or_else(|| json!([]));
    Ok(json!({ "documents": documents }))
}

async fn resolve_tree_create_workspace_id(
    state: &AppState,
    context: &RequestContext,
    requested_workspace_id: Option<&str>,
    parent_id: Option<&str>,
) -> Result<String, WebError> {
    if let Some(workspace_id) =
        resolve_effective_workspace_id(context, requested_workspace_id, false)?
    {
        return Ok(workspace_id);
    }

    if let Some(parent_id) = parent_id.map(str::trim).filter(|value| !value.is_empty()) {
        let parent_meta =
            fetch_documents_meta_via_convex(state.config(), context, None, parent_id).await?;
        if let Some(workspace_id) = parent_meta
            .get("workspace_id")
            .or_else(|| parent_meta.get("workspaceId"))
            .and_then(Value::as_str)
            .map(str::trim)
            .filter(|value| !value.is_empty())
        {
            return Ok(workspace_id.to_string());
        }
    }

    let bootstrap = execute_convex_mutation_by_name(
        state.config(),
        context,
        "workspaces:ensureDefaultWorkspace",
        json!({
            "fallbackName": context.auth.actor_id,
            "workspaceIdIfCreate": generate_tree_document_id(),
        }),
        None,
        None,
        "tree_command_workspace_bootstrap",
    )
    .await?;
    bootstrap
        .get("activeWorkspaceId")
        .and_then(Value::as_str)
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .map(ToOwned::to_owned)
        .ok_or_else(|| {
            WebError::bad_gateway_code(
                "workspace_bootstrap_bad_response",
                "workspaces.ensureDefaultWorkspace 未返回 activeWorkspaceId",
            )
            .with_context(context)
            .with_header("x-error-phase", "tree_command_workspace_bootstrap")
        })
}

pub async fn tree_command(
    State(state): State<AppState>,
    Extension(context): Extension<RequestContext>,
    body: String,
) -> Result<(StatusCode, Json<Value>), WebError> {
    let raw_request: TreeCommandEnvelope = serde_json::from_str(&body).map_err(|error| {
        WebError::bad_request_code(
            "tree_command_invalid_json",
            format!("tree command 请求体非法: {error}"),
        )
        .with_context(&context)
        .with_header("x-error-phase", "tree_command_decode")
    })?;
    let envelope_context = TreeCommandEnvelopeContext::from_envelope(&raw_request);
    let request = match raw_request.action.trim() {
        "create" => TreeCommandRequest::Create {
            workspace_id: raw_request.workspace_id,
            document_id: read_optional_non_empty(raw_request.document_id)
                .unwrap_or_else(generate_tree_document_id),
            parent_id: raw_request.parent_id,
            title: normalize_title(raw_request.title),
            access_scope: raw_request.access_scope,
            content: raw_request.content,
        },
        "createFolder" | "create_folder" | "folder.create" => TreeCommandRequest::CreateFolder {
            workspace_id: raw_request.workspace_id,
            document_id: read_optional_non_empty(raw_request.document_id)
                .unwrap_or_else(generate_tree_document_id),
            parent_id: raw_request.parent_id,
            title: normalize_title(raw_request.title.or_else(|| Some("新建文件夹".into()))),
        },
        "rename" => TreeCommandRequest::Rename {
            workspace_id: raw_request.workspace_id,
            document_id: raw_request.document_id.unwrap_or_default(),
            title: normalize_title(raw_request.title),
        },
        "move" => TreeCommandRequest::Move {
            workspace_id: raw_request.workspace_id,
            document_id: raw_request.document_id.unwrap_or_default(),
            parent_id: raw_request.parent_id,
            sort_order: raw_request.sort_order.unwrap_or(-1),
        },
        "archive" | "delete" | "trash" => TreeCommandRequest::Archive {
            workspace_id: raw_request.workspace_id,
            document_id: raw_request.document_id.unwrap_or_default(),
        },
        "restore" => TreeCommandRequest::Restore {
            workspace_id: raw_request.workspace_id,
            document_id: raw_request.document_id.unwrap_or_default(),
        },
        "copy" => TreeCommandRequest::Copy {
            workspace_id: raw_request.workspace_id,
            document_id: raw_request.document_id.unwrap_or_default(),
            target_parent_id: raw_request.target_parent_id.or(raw_request.parent_id),
            items: raw_request.items,
            title: raw_request.title,
        },
        "dropFiles" | "drop_files" => TreeCommandRequest::DropFiles {
            workspace_id: raw_request.workspace_id,
            parent_id: raw_request.parent_id,
            files_json: match raw_request.content {
                Some(Value::String(value)) => value,
                Some(value) => value.to_string(),
                None => "[]".into(),
            },
        },
        "purge" => TreeCommandRequest::Purge {
            workspace_id: raw_request.workspace_id,
            document_id: raw_request.document_id.unwrap_or_default(),
        },
        other => {
            return Err(WebError::bad_request_code(
                "tree_command_validation",
                format!("不支持的 tree action: {other}"),
            )
            .with_context(&context)
            .with_header("x-error-phase", "tree_command_validate"));
        }
    };
    let requested_workspace_id = match &request {
        TreeCommandRequest::Create { workspace_id, .. } => workspace_id.as_deref(),
        TreeCommandRequest::CreateFolder { workspace_id, .. } => workspace_id.as_deref(),
        TreeCommandRequest::Rename { workspace_id, .. } => workspace_id.as_deref(),
        TreeCommandRequest::Move { workspace_id, .. } => workspace_id.as_deref(),
        TreeCommandRequest::Archive { workspace_id, .. } => workspace_id.as_deref(),
        TreeCommandRequest::Restore { workspace_id, .. } => workspace_id.as_deref(),
        TreeCommandRequest::Copy { workspace_id, .. } => workspace_id.as_deref(),
        TreeCommandRequest::DropFiles { workspace_id, .. } => workspace_id.as_deref(),
        TreeCommandRequest::Purge { workspace_id, .. } => workspace_id.as_deref(),
    };
    let (action, requested_document_id, requested_parent_id, requested_title, requested_sort_order) =
        match &request {
            TreeCommandRequest::Create {
                document_id,
                parent_id,
                title,
                ..
            } => (
                "create",
                document_id.clone(),
                parent_id.clone(),
                Some(title.clone()),
                None,
            ),
            TreeCommandRequest::CreateFolder {
                document_id,
                parent_id,
                title,
                ..
            } => (
                "createFolder",
                document_id.clone(),
                parent_id.clone(),
                Some(title.clone()),
                None,
            ),
            TreeCommandRequest::Rename {
                document_id, title, ..
            } => (
                "rename",
                document_id.clone(),
                None,
                Some(title.clone()),
                None,
            ),
            TreeCommandRequest::Move {
                document_id,
                parent_id,
                sort_order,
                ..
            } => (
                "move",
                document_id.clone(),
                parent_id.clone(),
                None,
                Some(*sort_order),
            ),
            TreeCommandRequest::Archive { document_id, .. } => {
                ("delete", document_id.clone(), None, None, None)
            }
            TreeCommandRequest::Restore { document_id, .. } => {
                ("restore", document_id.clone(), None, None, None)
            }
            TreeCommandRequest::Copy {
                document_id,
                target_parent_id,
                title,
                ..
            } => (
                "copy",
                document_id.clone(),
                target_parent_id.clone(),
                title.clone(),
                None,
            ),
            TreeCommandRequest::DropFiles {
                parent_id,
                files_json,
                ..
            } => (
                "dropFiles",
                String::new(),
                parent_id.clone(),
                Some(files_json.clone()),
                None,
            ),
            TreeCommandRequest::Purge { document_id, .. } => {
                ("purge", document_id.clone(), None, None, None)
            }
        };
    if envelope_context.source_kind.as_deref() == Some("local_folder") {
        let root_uri = envelope_context
            .root_uri
            .as_deref()
            .map(str::trim)
            .filter(|value| !value.is_empty())
            .ok_or_else(|| {
                WebError::bad_request_code("local_folder_root_required", "缺少本地文件夹 rootUri")
                    .with_context(&context)
            })?;
        let execution = execute_local_tree_command(
            root_uri,
            action,
            &requested_document_id,
            requested_parent_id.as_deref(),
            requested_title.as_deref(),
        )
        .map_err(|error| {
            error
                .with_context(&context)
                .with_header("x-error-phase", "tree_local_executor")
        })?;
        return Ok(json_response(
            &context,
            json!({
                "workspaceId": local_workspace_id_from_root_uri(root_uri)?,
                "action": action,
                "documentId": execution
                    .get("documentId")
                    .and_then(Value::as_str)
                    .unwrap_or(&requested_document_id),
                "parentId": requested_parent_id,
                "title": requested_title,
                "sortOrder": requested_sort_order,
                "updatedAt": Value::Null,
                "execution": execution,
                "artifacts": Value::Null,
                "artifactError": Value::Null,
            }),
        ));
    }
    let effective_workspace_id = match &request {
        TreeCommandRequest::Create { parent_id, .. } => {
            resolve_tree_create_workspace_id(
                &state,
                &context,
                requested_workspace_id,
                parent_id.as_deref(),
            )
            .await?
        }
        TreeCommandRequest::CreateFolder { .. } => {
            return Err(WebError::bad_request_code(
                "tree_command_validation",
                "convex_workspace 暂不支持 folder create capability",
            )
            .with_context(&context)
            .with_header("x-error-phase", "tree_command_validate"));
        }
        _ => resolve_effective_workspace_id(&context, requested_workspace_id, true)?
            .expect("workspace_required 已确保存在"),
    };
    let needs_move_preflight = matches!(&request, TreeCommandRequest::Move { .. });
    let mut command_wire = create_command_wire(
        &context,
        &effective_workspace_id,
        request,
        &envelope_context,
    )?;
    if needs_move_preflight {
        command_wire.preflight_data =
            Some(load_tree_move_preflight_data(&state, &context, &effective_workspace_id).await?);
    }
    let execution = execute_runtime_command_via_convex_with_artifacts(
        state.config(),
        &context,
        Some(&effective_workspace_id),
        command_wire,
    )
    .await?;
    let response_document_id = execution
        .result
        .get("id")
        .and_then(Value::as_str)
        .map(ToOwned::to_owned)
        .unwrap_or(requested_document_id);
    let artifacts = execution
        .artifacts
        .as_ref()
        .and_then(|artifacts| serde_json::to_value(artifacts).ok())
        .unwrap_or(Value::Null);
    let artifact_error = execution
        .artifact_error
        .as_ref()
        .map(|message| Value::String(message.clone()))
        .unwrap_or(Value::Null);

    Ok(json_response(
        &context,
        json!({
            "workspaceId": effective_workspace_id,
            "action": action,
            "documentId": response_document_id,
            "parentId": requested_parent_id,
            "title": requested_title,
            "sortOrder": requested_sort_order,
            "updatedAt": execution.result.get("updated_at").cloned().unwrap_or(Value::Null),
            "execution": execution.result,
            "artifacts": artifacts,
            "artifactError": artifact_error,
        }),
    ))
}

pub async fn reduce_tree_shell_runtime(
    Json(body): Json<TreeShellRuntimeRequest>,
) -> Json<TreeShellRuntimeResult> {
    Json(reduce_tree_shell_runtime_request(body))
}

#[cfg(test)]
mod tests {
    use super::{create_command_wire, TreeCommandEnvelopeContext, TreeCommandRequest};
    use crate::app::{build_app, AppConfig, AppState};
    use crate::context::RequestContext;
    use crate::routes::command_support::build_runtime_command_plan;
    use axum::body::Body;
    use axum::http::{HeaderMap, Method, Request, StatusCode, Uri};
    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: 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#"{"sidebar:datasetList":{"active_workspace_id":"ws_demo","documents":[{"id":"page_root","workspace_id":"ws_demo","title":"工作区首页","parent_id":null,"sort_order":0,"is_starred":true,"is_template":false,"created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"},{"id":"page_child","workspace_id":"ws_demo","title":"子页面","parent_id":"page_root","sort_order":1,"is_starred":false,"is_template":false,"created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"}],"media_assets":[{"id":"asset_file_1","workspace_id":"ws_demo","document_id":"page_root","asset_type":"file","file_name":"封面.png","mime_type":"image/png","created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"},{"id":"asset_child_1","workspace_id":"ws_demo","document_id":"page_root","asset_type":"file","file_name":"节点图片.png","mime_type":"image/png","created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"},{"id":"asset_pdf_1","workspace_id":"ws_demo","document_id":"page_root","asset_type":"file","file_name":"说明书.pdf","mime_type":"application/pdf","created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"},{"id":"asset_book_1","workspace_id":"ws_demo","document_id":"page_root","asset_type":"file","file_name":"小说.epub","mime_type":"application/epub+zip","created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"}],"mindmap_assets":[{"id":"mind_1","workspace_id":"ws_demo","document_id":"page_root","asset_type":"mindmap","file_name":"头脑风暴.json","mime_type":"application/json","created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"}],"table_assets":[{"id":"table_1","workspace_id":"ws_demo","document_id":"page_root","asset_type":"luckysheet","file_name":"预算.luckysheet","mime_type":"application/json","created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"}],"mindmap_asset_children":{"mind_1":["asset_child_1"]}}}"#.into()),
            mutation_fixtures_json: Some(r#"{"workspaces:ensureDefaultWorkspace":{"workspaces":[{"id":"ws_demo","name":"我的空间","type":"personal","iconUrl":null,"memberCount":1,"isDefault":true}],"activeWorkspaceId":"ws_demo"},"documents:createWithParentReference":{"ok":true,"id":"page_new","updated_at":"2026-04-17T00:00:00Z"},"documents:updateTitle":{"ok":true,"updated_at":"2026-04-17T00:00:00Z"},"documents:move":{"ok":true,"updated_at":"2026-04-17T00:00:00Z"},"documents:purge":{"ok":true,"deletedCount":1},"bridgeLogs:recordCommandLog":{"ok":true,"id":"clog_fixture"},"bridgeLogs:recordDomainEvent":{"ok":true,"id":"evt_fixture"}}"#.into()),
            dev_user_id: "dev-user".into(),
            dev_user_name: "开发用户".into(),
            dev_user_email: "dev@mnote.local".into(),
        }))
    }

    #[tokio::test]
    async fn tree_shell_returns_interactive_html_document() {
        let response = app()
            .oneshot(
                Request::builder()
                    .uri("/tree?workspaceId=ws_demo&rootNodeId=page_root&activeDocumentId=page_child&channel=test-shell")
                    .body(Body::empty())
                    .expect("request"),
            )
            .await
            .expect("response");

        assert_eq!(response.status(), StatusCode::OK);
        let content_type = response
            .headers()
            .get("content-type")
            .and_then(|value| value.to_str().ok())
            .unwrap_or_default();
        assert!(content_type.contains("text/html"));

        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
            .await
            .expect("body");
        let html = String::from_utf8(body.to_vec()).expect("utf8");
        assert!(html.contains("tree-create-root"));
        assert!(html.contains("tree.ready"));
        assert!(html.contains("test-shell"));
        assert!(html.contains("tree-action-menu"));
        assert!(html.contains("tree.page.context-menu"));
        assert!(html.contains("tree.page.expand.changed"));
        assert!(html.contains("tree.page.focus.changed"));
        assert!(html.contains("tree.shell.state.patch"));
        assert!(html.contains("\"contractName\":\"rust_page_focus_keyboard_reducer_v1\""));
        assert!(html.contains("applyPageKeyboardAction"));
        assert!(html.contains("reducePageActionWithRuntime"));
        let apply_page_action_start = html
            .find("const applyPageKeyboardAction = (action, item, sourceElement) => {")
            .expect("applyPageKeyboardAction should be embedded");
        let apply_page_action_end = html[apply_page_action_start..]
            .find("\n        const postPickerFocusChange")
            .expect("applyPageKeyboardAction should end before picker focus handler");
        let apply_page_action_body =
            &html[apply_page_action_start..apply_page_action_start + apply_page_action_end];
        assert!(
            !apply_page_action_body.contains("toggleExpand("),
            "page keyboard/expand should prefer runtime result instead of directly toggling local expansion state"
        );
        assert!(html.contains("patchPageTreeActiveDom"));
        assert!(html.contains("patchPageTreeExpansionDom"));
        assert!(html.contains("data-rust-page-renderer=\"initial_v1\""));
        assert!(html.contains("data-rust-rendered-row=\"page\""));
        assert!(html.contains("data-rust-action=\"toggle\""));
        assert!(html.contains("data-testid=\"tree-node-toggle\""));
        assert!(html.contains("hydrateInitialPageTree"));
        assert!(html.contains("const usedRustInitialPageRenderer = hydrateInitialPageTree();"));
        assert!(html.contains("applyCreatedDocumentLocally"));
        assert!(html.contains("applyRemovedDocumentLocally"));
        assert!(html.contains("sourceKind === \"convex_workspace\" && applyCreatedDocumentLocally"));
        assert!(
            html.contains("if (!deletableItems.every((item) => !fileTreeRowById.has(item.rowId)))")
        );
        assert!(html.contains("application/x-mnote-page-tree-node"));
        assert!(html.contains("页面已拖放到"));
        assert!(html.contains("setAttribute(\"role\", \"treeitem\")"));
        assert!(html.contains("setAttribute(\"aria-level\""));
    }

    #[tokio::test]
    async fn tree_shell_picker_mode_hides_command_toolbar_and_supports_root_pick() {
        let response = app()
            .oneshot(
                Request::builder()
                    .uri("/tree?workspaceId=ws_demo&mode=picker&allowRootPick=1&excludeIds=page_child")
                    .body(Body::empty())
                    .expect("request"),
            )
            .await
            .expect("response");

        assert_eq!(response.status(), StatusCode::OK);
        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
            .await
            .expect("body");
        let html = String::from_utf8(body.to_vec()).expect("utf8");
        assert!(html.contains("\"mode\":\"picker\""));
        assert!(html.contains("\"allowRootPick\":true"));
        assert!(html.contains("\"excludeIds\":[\"page_child\"]"));
        assert!(html.contains("data-rust-picker-renderer=\"initial_v1\""));
        assert!(html.contains("data-rust-rendered-row=\"picker-root\""));
        assert!(html.contains("tree.pick.root"));
        assert!(html.contains("tree.picker.command"));
        assert!(html.contains("tree.picker.focus.changed"));
        assert!(html.contains("\"contractName\":\"rust_picker_state_reducer_v1\""));
        assert!(html.contains("applyPickerStateAction"));
        assert!(html.contains("postPickerPickResultToHost"));
        assert!(html.contains("applyPickerFocusByItemKey(\"__root__\", { focusDom: true })"));
        assert!(html.contains("applyPickerFocusByItemKey(item.nodeId, { focusDom: true })"));
        assert!(html.contains("const shouldFocusDom = options.focusDom === true"));
        assert!(html.contains("if (shouldFocusDom) focusPickerRowElement"));
        assert!(html.contains("patchPickerActiveDom"));
        assert!(html.contains("hydrateInitialPickerTree"));
        assert!(html.contains("tabindex=\""));
        assert!(html.contains("const usedRustInitialRenderer = hydrateInitialRenderer();"));
        assert!(html.contains("__MNOTE_TREE_SHELL_OVERRIDE__"));
    }

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

        assert_eq!(response.status(), StatusCode::OK);
        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
            .await
            .expect("body");
        let html = String::from_utf8(body.to_vec()).expect("utf8");
        assert!(html.contains("filetree-doc-row"));
        assert!(html.contains("data-rust-filetree-renderer=\"initial_v1\""));
        assert!(html.contains("data-rust-rendered-row=\"filetree\""));
        assert!(html.contains("\"mediaAssets\""));
        assert!(html.contains("tree.filetree.selection.changed"));
        assert!(html.contains("\"contractName\":\"rust_filetree_selection_reducer_v1\""));
        assert!(html.contains("applyFileTreeSelectionAction"));
        assert!(html.contains("tree.filetree.internal-drop"));
        assert!(html.contains("tree.filetree.external-drop"));
        assert!(html.contains("\"rowKind\":\"asset_folder\""));
        assert!(html.contains("\"resourceMeta\""));
        assert!(html.contains("dragover"));
        assert!(html.contains("hydrateInitialFileTree"));
        assert!(html.contains("const usedRustInitialRenderer = hydrateInitialRenderer();"));
    }

    #[tokio::test]
    async fn tree_shell_filetree_mode_can_open_local_folder_readonly_snapshot() {
        let root =
            std::env::temp_dir().join(format!("mnote-local-folder-source-{}", std::process::id()));
        let _ = std::fs::remove_dir_all(&root);
        std::fs::create_dir_all(root.join("docs")).expect("create local docs dir");
        std::fs::write(root.join("README.md"), "# Local Root\n").expect("write local md");
        std::fs::write(root.join("docs").join("child.md"), "# Child\n").expect("write child md");
        std::fs::write(root.join("image.png"), b"png").expect("write asset");

        let root_uri = format!("file://{}", root.display());
        let response = app()
            .oneshot(
                Request::builder()
                    .uri(format!(
                        "/tree?mode=filetree&sourceKind=local_folder&rootUri={root_uri}"
                    ))
                    .body(Body::empty())
                    .expect("request"),
            )
            .await
            .expect("response");

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

        assert_eq!(response.status(), StatusCode::OK);
        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
            .await
            .expect("body");
        let html = String::from_utf8(body.to_vec()).expect("utf8");
        assert!(html.contains("local_folder"));
        assert!(html.contains("README.md"));
        assert!(html.contains("child.md"));
        assert!(html.contains("image.png"));
        assert!(html.contains("data-row-kind=\"folder\""));
        assert!(html.contains("data-row-kind=\"markdown\""));
        assert!(html.contains("data-document-id=\"local-md:README.md\""));
    }

    #[tokio::test]
    async fn tree_shell_page_mode_can_open_local_folder_md_only_snapshot() {
        let root = std::env::temp_dir().join(format!(
            "mnote-local-page-tree-source-{}",
            std::process::id()
        ));
        let _ = std::fs::remove_dir_all(&root);
        std::fs::create_dir_all(root.join("docs")).expect("create local docs dir");
        std::fs::write(
            root.join("README.md"),
            "---\ntitle: Frontmatter Title\n---\n# Ignored H1\n",
        )
        .expect("write local md");
        std::fs::write(root.join("docs").join("child.md"), "# Child H1\n").expect("write child md");
        std::fs::write(root.join("image.png"), b"png").expect("write asset");

        let root_uri = format!("file://{}", root.display());
        let response = app()
            .oneshot(
                Request::builder()
                    .uri(format!(
                        "/tree?mode=page&sourceKind=local_folder&rootUri={root_uri}"
                    ))
                    .body(Body::empty())
                    .expect("request"),
            )
            .await
            .expect("response");

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

        assert_eq!(response.status(), StatusCode::OK);
        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
            .await
            .expect("body");
        let html = String::from_utf8(body.to_vec()).expect("utf8");
        assert!(html.contains("local_folder"));
        assert!(html.contains("Frontmatter Title"));
        assert!(html.contains("Child H1"));
        assert!(html.contains(">docs<") || html.contains("docs"));
        assert!(!html.contains("image.png"));
    }

    #[tokio::test]
    async fn tree_command_local_folder_create_rename_copy_trash_restore_and_purge_use_same_endpoint(
    ) {
        let root =
            std::env::temp_dir().join(format!("mnote-local-tree-command-{}", std::process::id()));
        let _ = std::fs::remove_dir_all(&root);
        std::fs::create_dir_all(&root).expect("create local root");
        let root_uri = format!("file://{}", root.display());

        let create_response = app()
            .oneshot(
                Request::builder()
                    .method(Method::POST)
                    .uri("/api/tree/commands")
                    .header("content-type", "application/json")
                    .body(Body::from(format!(
                        r#"{{"action":"create","sourceKind":"local_folder","rootUri":"{root_uri}","title":"新页面"}}"#
                    )))
                    .expect("request"),
            )
            .await
            .expect("response");
        let create_status = create_response.status();
        let body = axum::body::to_bytes(create_response.into_body(), usize::MAX)
            .await
            .expect("body");
        assert_eq!(
            create_status,
            StatusCode::OK,
            "{}",
            String::from_utf8_lossy(&body)
        );
        let payload: Value = serde_json::from_slice(&body).expect("json");
        let document_id = payload["result"]["documentId"]
            .as_str()
            .expect("document id")
            .to_string();
        assert!(root.join("新页面.md").exists());
        assert!(root.join(".mnote").join("page-ids.json").exists());

        let rename_response = app()
            .oneshot(
                Request::builder()
                    .method(Method::POST)
                    .uri("/api/tree/commands")
                    .header("content-type", "application/json")
                    .body(Body::from(format!(
                        r#"{{"action":"rename","sourceKind":"local_folder","rootUri":"{root_uri}","documentId":"{document_id}","title":"重命名页面"}}"#
                    )))
                    .expect("request"),
            )
            .await
            .expect("response");
        let rename_status = rename_response.status();
        let rename_body = axum::body::to_bytes(rename_response.into_body(), usize::MAX)
            .await
            .expect("body");
        assert_eq!(
            rename_status,
            StatusCode::OK,
            "{}",
            String::from_utf8_lossy(&rename_body)
        );
        assert!(!root.join("新页面.md").exists());
        assert!(root.join("重命名页面.md").exists());

        std::fs::create_dir_all(root.join("docs")).expect("create docs dir");
        let move_response = app()
            .oneshot(
                Request::builder()
                    .method(Method::POST)
                    .uri("/api/tree/commands")
                    .header("content-type", "application/json")
                    .body(Body::from(format!(
                        r#"{{"action":"move","sourceKind":"local_folder","rootUri":"{root_uri}","documentId":"{document_id}","parentId":"local-dir:docs","sortOrder":0}}"#
                    )))
                    .expect("request"),
            )
            .await
            .expect("response");
        assert_eq!(move_response.status(), StatusCode::OK);
        assert!(!root.join("重命名页面.md").exists());
        assert!(root.join("docs").join("重命名页面.md").exists());

        let copy_response = app()
            .oneshot(
                Request::builder()
                    .method(Method::POST)
                    .uri("/api/tree/commands")
                    .header("content-type", "application/json")
                    .body(Body::from(format!(
                        r#"{{"action":"copy","sourceKind":"local_folder","rootUri":"{root_uri}","documentId":"{document_id}","parentId":"local-dir:docs"}}"#
                    )))
                    .expect("request"),
            )
            .await
            .expect("response");
        let copy_status = copy_response.status();
        let copy_body = axum::body::to_bytes(copy_response.into_body(), usize::MAX)
            .await
            .expect("body");
        assert_eq!(
            copy_status,
            StatusCode::OK,
            "{}",
            String::from_utf8_lossy(&copy_body)
        );
        let copy_payload: Value = serde_json::from_slice(&copy_body).expect("copy json");
        let copied_document_id = copy_payload["result"]["documentId"]
            .as_str()
            .expect("copied document id")
            .to_string();
        assert!(root.join("docs").join("重命名页面 2.md").exists());

        let folder_response = app()
            .oneshot(
                Request::builder()
                    .method(Method::POST)
                    .uri("/api/tree/commands")
                    .header("content-type", "application/json")
                    .body(Body::from(format!(
                        r#"{{"action":"createFolder","sourceKind":"local_folder","rootUri":"{root_uri}","title":"资料"}}"#
                    )))
                    .expect("request"),
            )
            .await
            .expect("response");
        assert_eq!(folder_response.status(), StatusCode::OK);
        assert!(root.join("资料").is_dir());

        let delete_response = app()
            .oneshot(
                Request::builder()
                    .method(Method::POST)
                    .uri("/api/tree/commands")
                    .header("content-type", "application/json")
                    .body(Body::from(format!(
                        r#"{{"action":"delete","sourceKind":"local_folder","rootUri":"{root_uri}","documentId":"{document_id}"}}"#
                    )))
                    .expect("request"),
            )
            .await
            .expect("response");
        assert_eq!(delete_response.status(), StatusCode::OK);
        assert!(!root.join("docs").join("重命名页面.md").exists());
        assert!(root
            .join(".mnote")
            .join("trash")
            .join("重命名页面.md")
            .exists());
        assert!(root.join(".mnote").join("trash-index.json").exists());

        let restore_response = app()
            .oneshot(
                Request::builder()
                    .method(Method::POST)
                    .uri("/api/tree/commands")
                    .header("content-type", "application/json")
                    .body(Body::from(format!(
                        r#"{{"action":"restore","sourceKind":"local_folder","rootUri":"{root_uri}","documentId":"{document_id}"}}"#
                    )))
                    .expect("request"),
            )
            .await
            .expect("response");
        assert_eq!(restore_response.status(), StatusCode::OK);
        assert!(root.join("docs").join("重命名页面.md").exists());

        let purge_response = app()
            .oneshot(
                Request::builder()
                    .method(Method::POST)
                    .uri("/api/tree/commands")
                    .header("content-type", "application/json")
                    .body(Body::from(format!(
                        r#"{{"action":"purge","sourceKind":"local_folder","rootUri":"{root_uri}","documentId":"{copied_document_id}"}}"#
                    )))
                    .expect("request"),
            )
            .await
            .expect("response");
        assert_eq!(purge_response.status(), StatusCode::OK);
        assert!(!root.join("docs").join("重命名页面 2.md").exists());

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

    #[tokio::test]
    async fn tree_command_local_folder_root_escape_returns_unified_error_envelope() {
        let root = std::env::temp_dir().join(format!(
            "mnote-local-tree-root-escape-{}",
            std::process::id()
        ));
        let _ = std::fs::remove_dir_all(&root);
        std::fs::create_dir_all(&root).expect("create local root");
        let root_uri = format!("file://{}", root.display());

        let response = app()
            .oneshot(
                Request::builder()
                    .method(Method::POST)
                    .uri("/api/tree/commands")
                    .header("content-type", "application/json")
                    .body(Body::from(format!(
                        r#"{{"action":"create","sourceKind":"local_folder","rootUri":"{root_uri}","parentId":"local:folder:../outside","title":"逃逸"}}"#
                    )))
                    .expect("request"),
            )
            .await
            .expect("response");

        let status = response.status();
        let headers = response.headers().clone();
        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
            .await
            .expect("body");
        let payload: Value = serde_json::from_slice(&body).expect("error json");
        let _ = std::fs::remove_dir_all(&root);

        assert_eq!(status, StatusCode::BAD_REQUEST);
        assert_eq!(payload["ok"], false);
        assert_eq!(payload["code"], "local_folder_root_escape");
        assert!(payload["message"]
            .as_str()
            .unwrap_or_default()
            .contains("root"));
        assert!(payload["requestId"].as_str().unwrap_or_default().len() > 0);
        assert_eq!(
            headers
                .get("x-error-code")
                .and_then(|value| value.to_str().ok()),
            Some("local_folder_root_escape")
        );
        assert_eq!(
            headers
                .get("x-error-phase")
                .and_then(|value| value.to_str().ok()),
            Some("tree_local_executor")
        );
    }

    #[tokio::test]
    async fn tree_shell_embeds_renderer_input_contract() {
        let filetree_response = app()
            .oneshot(
                Request::builder()
                    .uri("/tree?workspaceId=ws_demo&mode=filetree&activeDocumentId=page_root")
                    .body(Body::empty())
                    .expect("request"),
            )
            .await
            .expect("response");
        assert_eq!(filetree_response.status(), StatusCode::OK);
        let filetree_body = axum::body::to_bytes(filetree_response.into_body(), usize::MAX)
            .await
            .expect("body");
        let filetree_html = String::from_utf8(filetree_body.to_vec()).expect("utf8");
        assert!(filetree_html.contains("\"rendererInput\""));
        assert!(filetree_html.contains("\"mode\":\"fileTree\""));
        assert!(filetree_html.contains("\"filetreeSelection\""));
        assert!(filetree_html.contains("\"selectedRowIds\":[\"index:page_root\"]"));
        assert!(!filetree_html.contains("\"selectedRowIds\":[\"doc:page_root\""));
        assert!(filetree_html.contains("\"focusedRowId\":\"index:page_root\""));
        assert!(filetree_html.contains("data-row-id=\"doc:page_root\""));
        assert!(filetree_html.contains("data-row-id=\"index:page_root\""));
        assert!(filetree_html.contains("data-row-id=\"index:page_root\" data-row-kind=\"index\""));
        assert!(filetree_html.contains("\"commandDispatcher\""));
        assert!(filetree_html.contains("\"runtimeArtifact\""));
        assert!(filetree_html.contains("\"contractName\":\"rust_tree_shell_runtime_artifact_v1\""));
        assert!(filetree_html.contains("\"reduceEndpoint\":\"/api/tree/runtime/reduce\""));
        assert!(filetree_html.contains(
            "\"wasmModuleUrl\":\"/api/tree-shell-runtime/mnote-tree-shell-runtime_bg.wasm\""
        ));
        assert!(filetree_html
            .contains("\"jsGlueUrl\":\"/api/tree-shell-runtime/mnote-tree-shell-runtime.js\""));
        assert!(filetree_html.contains(
            "\"outputChannels\":[\"commandDispatchEvent\",\"domPatch\",\"intentEvent\"]"
        ));

        let picker_response = app()
            .oneshot(
                Request::builder()
                    .uri("/tree?workspaceId=ws_demo&mode=picker&activePickerItemKey=page_child&excludeIds=page_root")
                    .body(Body::empty())
                    .expect("request"),
            )
            .await
            .expect("response");
        assert_eq!(picker_response.status(), StatusCode::OK);
        let picker_body = axum::body::to_bytes(picker_response.into_body(), usize::MAX)
            .await
            .expect("body");
        let picker_html = String::from_utf8(picker_body.to_vec()).expect("utf8");
        assert!(picker_html.contains("\"mode\":\"picker\""));
        assert!(picker_html.contains("\"activePickerItem\":\"page_child\""));
        assert!(picker_html.contains("\"excludedPickerIds\":[\"page_root\"]"));
        assert!(picker_html.contains("\"runtimeArtifact\""));
        assert!(picker_html.contains("\"contractName\":\"rust_tree_shell_runtime_artifact_v1\""));
        assert!(picker_html.contains("\"reduceEndpoint\":\"/api/tree/runtime/reduce\""));
        assert!(picker_html.contains(
            "\"wasmModuleUrl\":\"/api/tree-shell-runtime/mnote-tree-shell-runtime_bg.wasm\""
        ));
        assert!(picker_html
            .contains("\"jsGlueUrl\":\"/api/tree-shell-runtime/mnote-tree-shell-runtime.js\""));
    }

    #[tokio::test]
    async fn tree_runtime_reduce_endpoint_returns_filetree_runtime_result() {
        let response = app()
            .oneshot(
                Request::builder()
                    .method("POST")
                    .uri("/api/tree/runtime/reduce")
                    .header("content-type", "application/json")
                    .body(Body::from(
                        r#"{"mode":"fileTree","requestId":"req-filetree-route","environment":{"visibleRowIds":["doc:root","asset:image"],"rows":[{"rowId":"doc:root","rowKind":"doc","documentId":"root","assetId":null},{"rowId":"asset:image","rowKind":"asset","documentId":"root","assetId":"image"}]},"state":{"selection":{"selectedRowIds":[],"anchorRowId":null,"focusedRowId":null},"dragRowIds":[],"dragEffect":null,"dropTargetRowId":null},"action":{"kind":"selectRow","rowId":"asset:image","modifiers":{"shiftKey":false,"ctrlKey":false,"metaKey":false}}}"#,
                    ))
                    .expect("request"),
            )
            .await
            .expect("response");

        assert_eq!(response.status(), StatusCode::OK);
        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
            .await
            .expect("body");
        let payload: Value = serde_json::from_slice(&body).expect("json");

        assert_eq!(payload["mode"], Value::String("fileTree".into()));
        assert_eq!(
            payload["requestId"],
            Value::String("req-filetree-route".into())
        );
        assert_eq!(
            payload["domPatches"][0]["kind"],
            Value::String("fileTreeState".into())
        );
        assert_eq!(
            payload["domPatches"][0]["selectedRowIds"][0],
            Value::String("asset:image".into())
        );

        let drop_target_response = app()
            .oneshot(
                Request::builder()
                    .method("POST")
                    .uri("/api/tree/runtime/reduce")
                    .header("content-type", "application/json")
                    .body(Body::from(
                        r#"{"mode":"fileTree","requestId":"req-filetree-drop-target-route","environment":{"visibleRowIds":["doc:root","asset:image"],"rows":[{"rowId":"doc:root","rowKind":"doc","documentId":"root","assetId":null},{"rowId":"asset:image","rowKind":"asset","documentId":"root","assetId":"image"}]},"state":{"selection":{"selectedRowIds":[],"anchorRowId":null,"focusedRowId":null},"dragRowIds":[],"dragEffect":null,"dropTargetRowId":null},"action":{"kind":"updateDropTarget","rowId":"asset:image"}}"#,
                    ))
                    .expect("request"),
            )
            .await
            .expect("response");

        assert_eq!(drop_target_response.status(), StatusCode::OK);
        let drop_target_body = axum::body::to_bytes(drop_target_response.into_body(), usize::MAX)
            .await
            .expect("body");
        let drop_target_payload: Value = serde_json::from_slice(&drop_target_body).expect("json");

        assert_eq!(
            drop_target_payload["domPatches"][0]["dropTargetRowId"],
            Value::String("asset:image".into())
        );
    }

    #[tokio::test]
    async fn tree_runtime_reduce_endpoint_returns_page_and_picker_runtime_results() {
        let page_response = app()
            .oneshot(
                Request::builder()
                    .method("POST")
                    .uri("/api/tree/runtime/reduce")
                    .header("content-type", "application/json")
                    .body(Body::from(
                        r#"{"mode":"page","requestId":"req-page-route","environment":{"visibleNodeIds":["doc:root","doc:child"],"expandableNodeIds":["doc:root"]},"state":{"focusedId":"doc:root","expandedIds":[],"dropFeedback":null},"action":{"kind":"moveNext"}}"#,
                    ))
                    .expect("request"),
            )
            .await
            .expect("response");

        assert_eq!(page_response.status(), StatusCode::OK);
        let page_body = axum::body::to_bytes(page_response.into_body(), usize::MAX)
            .await
            .expect("body");
        let page_payload: Value = serde_json::from_slice(&page_body).expect("json");

        assert_eq!(page_payload["mode"], Value::String("page".into()));
        assert_eq!(
            page_payload["requestId"],
            Value::String("req-page-route".into())
        );
        assert_eq!(
            page_payload["domPatches"][0]["kind"],
            Value::String("pageState".into())
        );
        assert_eq!(
            page_payload["domPatches"][0]["focusedId"],
            Value::String("doc:child".into())
        );

        let picker_response = app()
            .oneshot(
                Request::builder()
                    .method("POST")
                    .uri("/api/tree/runtime/reduce")
                    .header("content-type", "application/json")
                    .body(Body::from(
                        r#"{"mode":"picker","requestId":"req-picker-route","environment":{"items":[{"itemKey":"doc:root","documentId":"doc:root","pickable":true},{"itemKey":"doc:child","documentId":"doc:child","pickable":true}],"excludedIds":[],"allowRootPick":false},"state":{"activeItemKey":"doc:child"},"action":{"kind":"pick"}}"#,
                    ))
                    .expect("request"),
            )
            .await
            .expect("response");

        assert_eq!(picker_response.status(), StatusCode::OK);
        let picker_body = axum::body::to_bytes(picker_response.into_body(), usize::MAX)
            .await
            .expect("body");
        let picker_payload: Value = serde_json::from_slice(&picker_body).expect("json");

        assert_eq!(picker_payload["mode"], Value::String("picker".into()));
        assert_eq!(
            picker_payload["requestId"],
            Value::String("req-picker-route".into())
        );
        assert_eq!(
            picker_payload["hostEvents"][0]["kind"],
            Value::String("pickerPickDocument".into())
        );
        assert_eq!(
            picker_payload["hostEvents"][0]["documentId"],
            Value::String("doc:child".into())
        );
    }

    #[tokio::test]
    async fn tree_command_create_generates_document_id_when_missing() {
        let response = app()
            .oneshot(
                Request::builder()
                    .method("POST")
                    .uri("/api/tree/commands")
                    .header("content-type", "application/json")
                    .body(Body::from(
                        r#"{"action":"create","workspaceId":"ws_demo","parentId":"page_root","title":"新页面","accessScope":"private","content":[]}"#,
                    ))
                    .expect("request"),
            )
            .await
            .expect("response");

        assert_eq!(response.status(), StatusCode::OK);
        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
            .await
            .expect("body");
        let payload: Value = serde_json::from_slice(&body).expect("json");
        assert_eq!(payload["result"]["action"], Value::String("create".into()));
        assert_eq!(
            payload["result"]["documentId"],
            Value::String("page_new".into())
        );
    }

    #[tokio::test]
    async fn tree_command_create_uses_default_workspace_when_workspace_missing() {
        let response = app()
            .oneshot(
                Request::builder()
                    .method("POST")
                    .uri("/api/tree/commands")
                    .header("content-type", "application/json")
                    .body(Body::from(r#"{"action":"create","title":"新页面"}"#))
                    .expect("request"),
            )
            .await
            .expect("response");

        assert_eq!(response.status(), StatusCode::OK);
        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
            .await
            .expect("body");
        let payload: Value = serde_json::from_slice(&body).expect("json");
        assert_eq!(payload["result"]["action"], Value::String("create".into()));
        assert_eq!(
            payload["result"]["workspaceId"],
            Value::String("ws_demo".into())
        );
        assert_eq!(
            payload["result"]["documentId"],
            Value::String("page_new".into())
        );
    }

    #[tokio::test]
    async fn tree_command_purge_uses_tree_command_protocol() {
        let response = app()
            .oneshot(
                Request::builder()
                    .method("POST")
                    .uri("/api/tree/commands")
                    .header("content-type", "application/json")
                    .body(Body::from(
                        r#"{"action":"purge","workspaceId":"ws_demo","documentId":"page_child"}"#,
                    ))
                    .expect("request"),
            )
            .await
            .expect("response");

        assert_eq!(response.status(), StatusCode::OK);
        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
            .await
            .expect("body");
        let payload: Value = serde_json::from_slice(&body).expect("json");
        assert_eq!(payload["result"]["action"], Value::String("purge".into()));
        assert_eq!(
            payload["result"]["documentId"],
            Value::String("page_child".into())
        );
        assert_eq!(
            payload["result"]["execution"]["deletedCount"],
            Value::from(1)
        );
    }

    #[tokio::test]
    async fn tree_command_rejects_negative_sort_order() {
        let response = app()
            .oneshot(
                Request::builder()
                    .method("POST")
                    .uri("/api/tree/commands")
                    .header("content-type", "application/json")
                    .body(Body::from(
                        r#"{"action":"move","workspaceId":"ws_demo","documentId":"page_child","parentId":"page_root","sortOrder":-1}"#,
                    ))
                    .expect("request"),
            )
            .await
            .expect("response");

        assert_eq!(response.status(), StatusCode::BAD_REQUEST);
    }

    #[tokio::test]
    async fn tree_command_rename_defaults_blank_title_to_untitled() {
        let response = app()
            .oneshot(
                Request::builder()
                    .method("POST")
                    .uri("/api/tree/commands")
                    .header("content-type", "application/json")
                    .body(Body::from(
                        r#"{"action":"rename","workspaceId":"ws_demo","documentId":"page_child","title":"   "}"#,
                    ))
                    .expect("request"),
            )
            .await
            .expect("response");

        assert_eq!(response.status(), StatusCode::OK);
        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
            .await
            .expect("body");
        let payload: Value = serde_json::from_slice(&body).expect("json");
        assert_eq!(payload["result"]["title"], Value::String("无标题".into()));
    }

    #[tokio::test]
    async fn tree_command_move_returns_structured_payload() {
        let response = app()
            .oneshot(
                Request::builder()
                    .method("POST")
                    .uri("/api/tree/commands")
                    .header("content-type", "application/json")
                    .body(Body::from(
                        r#"{"action":"move","workspaceId":"ws_demo","documentId":"page_child","parentId":"page_root","sortOrder":1}"#,
                    ))
                    .expect("request"),
            )
            .await
            .expect("response");

        assert_eq!(response.status(), StatusCode::OK);
        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
            .await
            .expect("body");
        let payload: Value = serde_json::from_slice(&body).expect("json");
        assert_eq!(payload["result"]["action"], Value::String("move".into()));
        assert_eq!(
            payload["result"]["documentId"],
            Value::String("page_child".into())
        );
    }

    #[tokio::test]
    async fn tree_command_response_includes_rust_artifact_plan_for_domain_event() {
        let response = app()
            .oneshot(
                Request::builder()
                    .method("POST")
                    .uri("/api/tree/commands")
                    .header("content-type", "application/json")
                    .body(Body::from(
                        r#"{"action":"move","workspaceId":"ws_demo","documentId":"page_child","parentId":"page_root","sortOrder":1}"#,
                    ))
                    .expect("request"),
            )
            .await
            .expect("response");

        assert_eq!(response.status(), StatusCode::OK);
        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
            .await
            .expect("body");
        let payload: Value = serde_json::from_slice(&body).expect("json");
        assert_eq!(
            payload["result"]["artifacts"]["domainEvent"]["eventType"],
            Value::String("tree.subtree.moved".into())
        );
        assert_eq!(
            payload["result"]["artifacts"]["domainEvent"]["payload"]["streamDelta"]["op"],
            Value::String("move_document".into())
        );
        assert_eq!(
            payload["result"]["artifacts"]["commandLog"]["payload"]["streamDelta"]["op"],
            Value::String("move_document".into())
        );
    }

    #[tokio::test]
    async fn tree_route_contracts_command_response_keeps_trace_and_workspace_fields() {
        let response = app()
            .oneshot(
                Request::builder()
                    .method("POST")
                    .uri("/api/tree/commands")
                    .header("content-type", "application/json")
                    .header("Authorization", "Bearer demo-token")
                    .body(Body::from(
                        r#"{"action":"rename","workspaceId":"ws_demo","documentId":"page_child","title":"命名"}"#,
                    ))
                    .expect("request"),
            )
            .await
            .expect("response");

        assert_eq!(response.status(), StatusCode::OK);
        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
            .await
            .expect("body");
        let payload: Value = serde_json::from_slice(&body).expect("json");
        assert!(payload["requestId"].as_str().is_some());
        assert!(payload["traceId"].as_str().is_some());
        assert_eq!(payload["result"]["workspaceId"], "ws_demo");
    }

    #[tokio::test]
    async fn tree_route_contracts_compat_sidebar_route_is_not_registered_by_default() {
        let response = app()
            .oneshot(
                Request::builder()
                    .uri("/api/compat/next/sidebar?workspaceId=ws_demo")
                    .header("Authorization", "Bearer demo-token")
                    .body(Body::empty())
                    .expect("request"),
            )
            .await
            .expect("response");

        assert_eq!(response.status(), StatusCode::NOT_FOUND);
    }

    #[test]
    fn tree_commands_prefer_tree_protocol_names_in_command_wire() {
        let context = RequestContext::from_http_parts(
            &Method::POST,
            &"/api/tree/commands".parse::<Uri>().expect("uri"),
            &HeaderMap::new(),
        );

        let create_wire = create_command_wire(
            &context,
            "ws_demo",
            TreeCommandRequest::Create {
                workspace_id: Some("ws_demo".into()),
                document_id: "page_new".into(),
                parent_id: Some("page_root".into()),
                title: "新页面".into(),
                access_scope: Some("private".into()),
                content: Some(Value::Array(Vec::new())),
            },
            &TreeCommandEnvelopeContext::default(),
        )
        .expect("create wire");
        assert_eq!(create_wire.name, "tree.node.create");
        assert_eq!(
            create_wire.source.source_kind.as_deref(),
            Some("convex_workspace")
        );
        assert_eq!(
            create_wire.source.root_uri.as_deref(),
            Some("convex://workspace/ws_demo")
        );
        assert_eq!(create_wire.source.workspace_id.as_deref(), Some("ws_demo"));
        assert!(create_wire
            .source
            .capabilities
            .iter()
            .any(|capability| capability == "execute-command"));

        let rename_wire = create_command_wire(
            &context,
            "ws_demo",
            TreeCommandRequest::Rename {
                workspace_id: Some("ws_demo".into()),
                document_id: "page_child".into(),
                title: "重命名".into(),
            },
            &TreeCommandEnvelopeContext::default(),
        )
        .expect("rename wire");
        assert_eq!(rename_wire.name, "tree.node.rename");

        let move_wire = create_command_wire(
            &context,
            "ws_demo",
            TreeCommandRequest::Move {
                workspace_id: Some("ws_demo".into()),
                document_id: "page_child".into(),
                parent_id: Some("page_root".into()),
                sort_order: 1,
            },
            &TreeCommandEnvelopeContext::default(),
        )
        .expect("move wire");
        assert_eq!(move_wire.name, "tree.subtree.move");
    }

    #[test]
    fn tree_command_wire_carries_source_and_target_context_from_unified_envelope() {
        let context = RequestContext::from_http_parts(
            &Method::POST,
            &"/api/tree/commands".parse::<Uri>().expect("uri"),
            &HeaderMap::new(),
        );
        let envelope_context = TreeCommandEnvelopeContext {
            source_kind: Some("convex_workspace".into()),
            root_uri: Some("convex://workspace/ws_demo".into()),
            source_capabilities: vec![
                "load-snapshot".into(),
                "preflight-command".into(),
                "execute-command".into(),
            ],
            target_node_id: Some("doc:page_child".into()),
            target_resource_meta: Some(serde_json::json!({
                "resourceKind": "document",
                "documentId": "page_child",
                "workspaceId": "ws_demo"
            })),
            selection: Some(serde_json::json!({
                "rowIds": ["doc:page_child"]
            })),
            operation: Some("tree.node.rename".into()),
        };

        let rename_wire = create_command_wire(
            &context,
            "ws_demo",
            TreeCommandRequest::Rename {
                workspace_id: Some("ws_demo".into()),
                document_id: "page_child".into(),
                title: "重命名".into(),
            },
            &envelope_context,
        )
        .expect("rename wire");

        assert_eq!(
            rename_wire.source.source_kind.as_deref(),
            Some("convex_workspace")
        );
        assert_eq!(
            rename_wire.source.root_uri.as_deref(),
            Some("convex://workspace/ws_demo")
        );
        assert_eq!(
            rename_wire.payload["targetNodeId"],
            serde_json::json!("doc:page_child")
        );
        assert_eq!(
            rename_wire.payload["targetResourceMeta"]["documentId"],
            serde_json::json!("page_child")
        );
        assert_eq!(
            rename_wire.payload["selection"]["rowIds"][0],
            serde_json::json!("doc:page_child")
        );
        assert_eq!(
            rename_wire.payload["operation"],
            serde_json::json!("tree.node.rename")
        );
    }

    #[test]
    fn tree_commands_keep_documents_alias_mapping_for_runtime_plan() {
        let context = RequestContext::from_http_parts(
            &Method::POST,
            &"/api/tree/commands".parse::<Uri>().expect("uri"),
            &HeaderMap::new(),
        );

        let tree_create_wire = create_command_wire(
            &context,
            "ws_demo",
            TreeCommandRequest::Create {
                workspace_id: Some("ws_demo".into()),
                document_id: "page_new".into(),
                parent_id: Some("page_root".into()),
                title: "新页面".into(),
                access_scope: Some("private".into()),
                content: Some(Value::Array(Vec::new())),
            },
            &TreeCommandEnvelopeContext::default(),
        )
        .expect("tree create wire");
        let compat_create_wire = bridge_runtime::RuntimeCommandEnvelopeWire {
            name: "documents.create".into(),
            ..tree_create_wire.clone()
        };
        let tree_create_plan =
            build_runtime_command_plan(&context, Some("ws_demo"), tree_create_wire)
                .expect("tree create plan");
        let compat_create_plan =
            build_runtime_command_plan(&context, Some("ws_demo"), compat_create_wire)
                .expect("compat create plan");
        assert_eq!(
            tree_create_plan.function_name,
            "documents:createWithParentReference"
        );
        assert_eq!(
            tree_create_plan.function_name,
            compat_create_plan.function_name
        );

        let tree_rename_wire = create_command_wire(
            &context,
            "ws_demo",
            TreeCommandRequest::Rename {
                workspace_id: Some("ws_demo".into()),
                document_id: "page_child".into(),
                title: "重命名".into(),
            },
            &TreeCommandEnvelopeContext::default(),
        )
        .expect("tree rename wire");
        let compat_rename_wire = bridge_runtime::RuntimeCommandEnvelopeWire {
            name: "documents.title.update".into(),
            ..tree_rename_wire.clone()
        };
        let tree_rename_plan =
            build_runtime_command_plan(&context, Some("ws_demo"), tree_rename_wire)
                .expect("tree rename plan");
        let compat_rename_plan =
            build_runtime_command_plan(&context, Some("ws_demo"), compat_rename_wire)
                .expect("compat rename plan");
        assert_eq!(tree_rename_plan.function_name, "documents:updateTitle");
        assert_eq!(
            tree_rename_plan.function_name,
            compat_rename_plan.function_name
        );

        let tree_move_wire = create_command_wire(
            &context,
            "ws_demo",
            TreeCommandRequest::Move {
                workspace_id: Some("ws_demo".into()),
                document_id: "page_child".into(),
                parent_id: Some("page_root".into()),
                sort_order: 1,
            },
            &TreeCommandEnvelopeContext::default(),
        )
        .expect("tree move wire");
        let compat_move_wire = bridge_runtime::RuntimeCommandEnvelopeWire {
            name: "documents.move".into(),
            ..tree_move_wire.clone()
        };
        let tree_move_plan = build_runtime_command_plan(&context, Some("ws_demo"), tree_move_wire)
            .expect("tree move plan");
        let compat_move_plan =
            build_runtime_command_plan(&context, Some("ws_demo"), compat_move_wire)
                .expect("compat move plan");
        assert_eq!(tree_move_plan.function_name, "documents:move");
        assert_eq!(tree_move_plan.function_name, compat_move_plan.function_name);
    }
}
