Files
mnote/design/05-editor-mainline/done/5-28-leptos-tiptap-editor-runtime-module-extraction-v1.md
T

34 KiB
Raw Blame History

5-28 Leptos Tiptap 编辑器 Runtime 模块拆分 v1

创建时间:2026-05-24 状态:done

触发背景:

  • bugs/0524.md 第 4 条删除本地 Markdown 附件链接后 Ctrl+Z/Ctrl+Y 不可用,根因是编辑器删除路径绕过 Tiptap/ProseMirror history。
  • 修复过程中 CodeGraph 能索引 Rust 符号,但 rust/spikes/leptos-tiptap-spike/src/lib.rs 单文件约一万行,职责过多,主控和 worker 都需要反复字面搜索。
  • design/03-rust-web/process/3-19-rust-web-browser-runtime-module-extraction-v1.md 解决 Rust Web shell/browser host runtime 外置化;本设计补齐编辑器主线拆分。

1. 结论

leptos-tiptap-spike/src/lib.rs 的问题不是 raw string,而是编辑器 runtime 仍以单个巨型 Rust 文件承载过多职责:Tiptap 初始化、命令执行、附件上传、附件链接增强、history、块菜单、DOM overlay、mindmap node view、page options、AI surface 都混在一起。CodeGraph 对这种文件只能给出符号位置,不能给出清晰模块边界。

后续不应继续在该文件中追加新的编辑器行为。应分阶段把编辑器命令、附件链接/上传、history 安全命令、DOM bridge/overlay、mindmap node view 拆入独立 Rust 模块;保持 leptos-tiptap island 仍是当前文档页主编辑器 runtime,但让每个行为域能被单独搜索、测试、派发给 worker。

2. 当前代码基线

  • rust/spikes/leptos-tiptap-spike/src/lib.rs

    • 约一万行。
    • 同时包含 CSS、Tiptap extension 列表、slash menu、toolbar、block menu、table toolbar、attachment upload、attachment link handling、local folder path、history/undo、DOM selection、mindmap node view、runtime bridge。
    • 最近第 4 条 bug 的关键函数和逻辑散落在同一文件中,例如附件链接识别、keydown 处理、apply_html_updatesync_persisted_editor_commanddelete_top_level_block_html
  • reference-code/leptos-tiptap/

    • 提供底层 Tiptap Rust/JS bridge。
    • 已有 TiptapEditorHandleTiptapExtensionTiptapRangedelete_selectiondelete_rangeundoredo 等命令。
    • MNote 不应把这些底层 bridge 再复制一遍,而应在 spike runtime 上方建立 MNote 编辑器行为模块。

3. 与 3-19 的边界

03-rust-web 负责:

  • Rust Web SSR shell。
  • document pane host adapter。
  • resource tab / secondary pane / URL / conflict panel 生命周期。
  • filetree / resource open / local upload 的 shell 侧适配。

05-editor-mainline 负责:

  • Tiptap 编辑器内部命令。
  • 编辑器内附件链接和上传块行为。
  • undo/redo/history 安全边界。
  • 编辑器 DOM selection / overlay / toolbar / block menu。
  • Tiptap node view,例如 mindmap block。

交界规则:

  • Shell 可以告诉 editor “打开某个文档/资源/保存状态/挂载 pane”。
  • Editor 可以告诉 shell “内容变更/附件打开 intent/资源上传完成/冲突状态”。
  • Shell 不应直接改 Tiptap document 内部结构。
  • Editor 不应决定 workspace tree/resource 归属真相。

4. 目标

  • src/lib.rs 从“巨型编辑器 runtime”收敛为组件入口和模块装配层。
  • 让附件、history、命令、DOM bridge 等高频 bug 区域成为独立 Rust 模块。
  • 修 bug 时能把 worker 限定到单一模块和单一 smoke。
  • 统一编辑器命令合同:能用 Tiptap command/transaction 的行为,不再通过 set_content/HTML 整文替换实现用户编辑动作。
  • 建立 history 安全基线:删除、插入、重排、格式转换、附件操作都要明确是否进入 undo stack。

5. 非目标

  • 不替换 Tiptap 或 leptos-tiptap bridge。
  • 不把文档事实源迁回前端。
  • 不一次性重写编辑器 UI。
  • 不在本设计中实现 Phase C 流式 apply / review session。
  • 不把 Page AI panel 或 Hermes/Reasonix 工具链混入本次编辑器 runtime 拆分。

6. 目标模块布局

建议新增目录:

rust/spikes/leptos-tiptap-spike/src/
  lib.rs
  editor_runtime/
    mod.rs
    extensions.rs
    command_sync.rs
    history_safe_commands.rs
    attachment_links.rs
    attachment_upload.rs
    local_markdown_paths.rs
    dom_selection.rs
    overlays.rs
    block_menu.rs
    table_toolbar.rs
    mindmap_node_view.rs
    bridge_events.rs
    persistence.rs

第一阶段可以只拆高频问题模块,不强求一次建完所有文件。

7. 分阶段执行清单

归档说明(2026-05-25):本节保留原始分阶段路线,但不再作为当前 done/ 状态的未完成 checklist。已经满足归档条件的范围以第 13 节 Done Gate 为准;仍未实施或被明确划到 03-rust-web / 后续编辑器切片的条目改为普通状态项,后续需要另起设计或 bug/checklist 承接,不能在本 done 文档中继续以未勾选 checklist 表达。

P0:热点函数归属表和基线保护

  • 已记录:当前 src/lib.rs 中最近 bug 高频函数的归属模块:
    • selected_local_attachment_link_in_editor_state -> attachment_links.rs
    • selected_local_attachment_link_text -> attachment_links.rs / dom_selection.rs
    • sync_persisted_editor_command -> command_sync.rs
    • apply_html_update / apply_document_update -> command_sync.rs,并标记 history 风险
    • delete_top_level_block_html / reorder_top_level_block_html -> 待替换为 history_safe_commands.rs
      • 2026-05-24delete_top_level_block_html 已删除,块菜单删除改走 delete_top_level_block_with_history(...) -> Tiptap delete_range / insert_content_at
    • upload slash action / local asset path -> attachment_upload.rs / local_markdown_paths.rs
    • toolbar/block menu DOM anchor -> overlays.rs / block_menu.rs
  • 后续:建立 history 风险清单;所有用户编辑动作中仍调用 set_content 或 HTML 整文替换的路径必须登记。
  • 已保留:当前已通过的 scripts/task488-local-attachment-link-delete-undo-smoke.js 作为 RED/GREEN 基线。

P1:抽出附件链接和 history-safe delete

  • 新增 editor_runtime/attachment_links.rs
  • 迁出本地附件链接识别:
    • DOM selection fallback。
    • Tiptap editor state link attributes 判断。
    • /api/local-folder/files/open 链接识别。
  • 新增 editor_runtime/history_safe_commands.rs
  • 将“选中附件链接文本后 Delete/Backspace”收口为命名函数,例如 delete_selected_attachment_link_with_history(...)
  • 验证 task488 仍通过。

P2:抽出 command sync / persistence

  • 新增 editor_runtime/command_sync.rs
  • 新增 editor_runtime/persistence.rs
  • 迁出:
    • read_editor_snapshot
    • sync_editor_outputs
    • sync_persisted_editor_command
    • apply_document_update(当前保持零调用者现状)
  • 后续:迁出 change/status event dispatch 的编辑器侧包装。
    • 2026-05-24 复核:apply_html_update 仍依赖 ChangePayload / ChangeMetaPayloaddispatch_change_eventdispatch_runtime_stateHoveredBlockState 和 toolbar overlay state,暂留 lib.rs,等待 P4 bridge_events.rs / overlays.rs 后再迁。
  • 明确 set_content 只允许用于:
    • 初始加载
    • host 替换文档
    • 外部同步/恢复
    • 非用户编辑的兼容兜底
  • 用户编辑动作必须优先走 Tiptap command/transaction。
    • 2026-05-24:块菜单删除已从 apply_html_update / set_content(TiptapContent::html(...)) 迁到 history_safe_commands.rsdelete_top_level_block_with_history(...),并由 scripts/task489-block-menu-delete-undo-smoke.js 覆盖 Ctrl+Z/Ctrl+Y。

P3:抽出附件上传与本地 Markdown 路径

  • 新增 editor_runtime/attachment_upload.rs
  • 不纳入本设计当前归档范围:新增 editor_runtime/local_markdown_paths.rs
    • 2026-05-24 复核:local_markdown_paths.rs 不应落在 leptos-tiptap 内部;本地 Markdown 路径、rootUri/api/local-folder/files/open URL 和上传落盘归 03-rust-web 的 shell host runtime / local-upload-runtime.js
  • 迁出 slash 上传附件、图片的编辑器 intent。
    • dispatch_editor_upload_request(kind, accept) 已迁入 attachment_upload.rs
    • SlashActionKind::Image / UploadAttachment 仍留在 slash menu 基础结构中,只调用 editor intent。
  • 不纳入本设计当前归档范围:迁出 Office/PDF/code 文件的编辑器内插入逻辑。
    • 当前实际插入链路在 layout.rs / local-upload-runtime.js,不是 editor runtime;后续由 03-rust-web 继续收口。
  • 明确 editor 只负责发起上传 intent;资源保存位置和归属仍由 shell/Rust API 合同决定。
  • 后续覆盖:
    • 上传第一个/第二个附件后链接仍可点击。
    • 刷新后链接不退化为普通外链。
    • secondary pane 上传与 primary pane 隔离。

P4:抽出 DOM bridge / overlay / block menu

  • 后续:新增 editor_runtime/dom_selection.rsoverlays.rsblock_menu.rs
  • 后续:迁出 selection、toolbar anchor、slash menu anchor、block menu anchor、image/table toolbar anchor。
  • 后续:对所有 keydown/click 捕获建立“谁先处理、谁 stop propagation”的规则,避免 Tiptap 原生快捷键与外层 window listener 互相吞事件。
  • 后续覆盖:
    • Ctrl+Z/Ctrl+Y
    • slash menu
    • block menu
    • table toolbar
    • attachment context menu

P5:抽出 mindmap node view

  • 后续:新增 editor_runtime/mindmap_node_view.rs
  • 后续:迁出 mindmap block 的 node view、runtime mount、projection refresh、simplemindmap event bridge。
  • 后续:保持 mindmap 只是编辑器块视图,不成为正文事实源。

P6:收尾

  • 后续:src/lib.rs 只保留:
    • module imports
    • top-level component装配
    • 少量常量和 view glue
  • 后续:单个模块超过约 800 行时继续拆分。
  • 已完成:运行 CodeGraph sync/index,确认新增 Rust 模块被索引。
  • 后续:更新 worker skill/task template;编辑器 bug 默认先查 editor_runtime/*,再回到 lib.rs

8. History 安全规则

用户直接触发的编辑动作:

  • 必须优先使用 Tiptap command/transaction。
  • 如果临时必须使用 set_content,必须在 bug/design 中标明为什么不能走 command,并补一个 undo/redo 不支持或受限的说明。
  • 新增编辑动作的 smoke 或单测应明确检查 undo/redo,除非该动作按产品定义不可撤销。

非用户编辑动作:

  • 初始加载、host 替换文档、外部文件 watcher 重载、冲突恢复可以使用 set_content,但必须避免污染用户 undo stack。
  • 外部同步应与 working copy 冲突模型结合,不能悄悄覆盖当前用户编辑。

9. Worker 协同规则

拆分后给 ClaudeCode / Reasonix 的任务应限定为:

允许读取/修改:
- rust/spikes/leptos-tiptap-spike/src/editor_runtime/attachment_links.rs
- rust/spikes/leptos-tiptap-spike/src/editor_runtime/history_safe_commands.rs
- rust/spikes/leptos-tiptap-spike/src/editor_runtime/command_sync.rs
- rust/spikes/leptos-tiptap-spike/src/editor_runtime/persistence.rs
- scripts/task488-local-attachment-link-delete-undo-smoke.js

禁止:
- 修改 shell resource open 逻辑
- 修改 Rust tree command 语义
- 整文重写 src/lib.rs

采纳条件:

  • worker handoff 明确命中哪个模块。
  • diff 不跨越无关模块。
  • 主控能运行对应 smoke。
  • UI/浏览器行为必须有截图或 result.json 可审计证据。

10. 执行策略

本设计与 3-19 同步采用 Codex 主控、Reasonix 短跑 worker

  • Codex 负责编辑器主线边界、history 规则、最终 diff 复核和 smoke 验收。
  • Reasonix 只处理短任务,不在一个 run 里同时做架构判断、代码迁移和浏览器验收。
  • 同时最多 2 个 Reasonix worker;每批完成后必须先读取 process-handoff.md/json 和 Hindsight reasonix recall,再进入下一批。
  • Batch 0 只读,不修改文件,用来生成 src/lib.rs 热点函数归属表和 set_content / HTML 整文替换风险表。
  • 实施批次必须优先放在 editor_runtime/*,避免继续向 src/lib.rs 追加大块逻辑。

当前建议批次:

  • Batch 0 / R2:只读审计 rust/spikes/leptos-tiptap-spike/src/lib.rs 中 history、set_content、附件链接删除、上传插入相关路径,输出函数归属表和风险等级。
  • Batch 1:先抽出 attachment_links.rs 和最小 history_safe_commands.rs,保持 task488 通过;随后优先处理块菜单 delete 仍走 HTML 整文替换的 history 风险。
  • Batch 2:抽出 command_sync.rs / persistence.rs,登记仍允许 set_content 的 host 替换和外部同步路径。
  • Batch 3:抽出 attachment_upload.rs / local_markdown_paths.rs,覆盖第一个/第二个附件上传和点击打开 smoke。
  • Batch 4:抽出 DOM selection / overlay / block menu,建立 keydown/click 捕获顺序规则。

Batch 0 验收标准:

  • R2 必须列出搜索过的函数、文件位置、当前职责、建议目标模块、history 风险和 smoke 建议。
  • R2 不允许修改源码、脚本、文档或运行 git 写操作。
  • Codex 只采纳能被本地源码定位和现有 bug 证据支持的结论。
  • 若 R2 输出泛化建议但无法对应具体函数,视为不可采纳,只记录为 worker 质量问题。

10.1 Batch 0 执行记录(2026-05-24

Reasonix R2 run

  • run id: reasonix-2026-05-24T12-38-22-683Z-3826b3b6
  • handoff: /home/lix/.codex/runtime/reasonix-coding-worker/reasonix-2026-05-24T12-38-22-683Z-3826b3b6/process-handoff.md
  • 类型:只读审计,无工作区改动。
  • Codex 复核:已抽样用 rg 核对关键符号位置;Hindsight reasonix bank 可 recall 到该 handoff。

可采纳结论:

  • 最近已修复的附件链接 Delete/Backspace 路径现在使用 editor.delete_selection(),属于可迁入 history_safe_commands.rs 的正向模板。
  • selected_local_attachment_link_textselected_local_attachment_link_in_editor_state 可优先迁入 attachment_links.rs,迁移本身应保持行为不变并继续跑 task488
  • 仍需登记的高风险用户编辑路径:
    • 块菜单 deletedelete_top_level_block_html -> apply_html_update -> editor.set_content(TiptapContent::html(...)),用户删除动作绕过 history,后续应替换为 Tiptap command/transaction。
    • 块类型转换或 slash action 中经过 apply_document_update 的路径:editor.set_content(TiptapContent::json(...)) 可能绕过 history,需要逐调用点确认。
    • run_block_turn_into_page_action / run_block_turn_into_page:整文 JSON 替换,是否可撤销属于产品决策,不在第一刀默认修改。
  • 可接受的 set_content 路径:
    • apply_host_document_payload
    • 外部 block delta listener
    • 编辑器初始化恢复本地草稿 这些是非用户直接编辑动作,允许保留,但必须避免污染用户 undo stack 和覆盖当前用户编辑。

本轮发现的 worker 协同问题:

  • Reasonix 对大文件只读审计有效,但输出中的“行号和风险分级”只能作为候选事实;主控必须用本地 rg / 源码抽样复核。
  • Reasonix runner 产出 completed 结果后底层 reasonix acp 进程未自动退出;主控必须在每批结束时检查并清理本轮启动进程。

10.2 Batch 1 执行记录(2026-05-24

Reasonix run

  • run id: reasonix-2026-05-24T14-00-12-979Z-1125fb29
  • worker worktree: /mnt/Data1T/mnote-wt-5-28-attachment-history-b1
  • handoff: /home/lix/.codex/runtime/reasonix-coding-worker/reasonix-2026-05-24T14-00-12-979Z-1125fb29/process-handoff.md
  • 类型:窄范围机械迁移,允许修改 src/lib.rssrc/editor_runtime/{mod.rs,attachment_links.rs,history_safe_commands.rs}

采纳内容:

  • 新增 editor_runtime/mod.rs
  • 新增 editor_runtime/attachment_links.rs,迁出本地附件链接的 DOM selection fallback 与 Tiptap editor state link attributes 判断。
  • 新增 editor_runtime/history_safe_commands.rs,把选中本地附件链接后的 Delete/Backspace 删除收口到 delete_selected_attachment_link_with_history(...)
  • src/lib.rs 只保留模块装配和 keydown handler 调用,不再直接承载本地附件链接识别函数。

Codex 主控复核修正:

  • worker 初版把底层错误格式化为 删除附件引用失败:{e},调用处也会加同一前缀;主控已改为 error.to_string(),避免用户反馈重复。
  • worker 初版自报 cargo check 通过依赖临时 reference-code symlink;主控在 worktree 和主工作区分别重新验证。
  • Reasonix runner 写出 completed 后底层 reasonix acp 残留,主控已清理本轮进程。
  • 后续 task479 暴露 DOM range.selectNode(link) + Backspace 场景下 Tiptap selection 可能未同步,导致相邻附件删除检查失败;主控已在 history_safe_commands 删除前把 DOM 选中的本地附件链接映射回 Tiptap text selection,再调用 delete_selection()

已通过验证:

  • cargo fmt --manifest-path rust/Cargo.toml --all --check
  • cargo check --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml35 个既有 warning
  • cargo build --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml --target wasm32-unknown-unknown --release
  • wasm-bindgen rust/spikes/leptos-tiptap-spike/target/wasm32-unknown-unknown/release/mnote_leptos_tiptap_spike.wasm --target web --out-dir rust/spikes/leptos-tiptap-spike/generated/island --out-name mnote-leptos-tiptap-spike-island
  • node scripts/task488-local-attachment-link-delete-undo-smoke.js
    • 结果:ok=true
    • 证据:tmp/task488-local-attachment-link-delete-undo-smoke/result.json
  • node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js
    • 结果:ok=true
    • 证据:命令 stdout JSON,覆盖 editor-upload-to-page-resource-dirfiletree-folder-drop-to-targetbroken-link-after-real-file-deletedirect-attach-open-without-active-mdeditor-delete-one-attachment-preserves-adjacent 等检查。
  • node scripts/task488-local-attachment-link-delete-undo-smoke.js
    • 结果:ok=true
    • 步骤:uploaded -> selected -> deleted -> undo -> redo
    • 证据:tmp/task488-local-attachment-link-delete-undo-smoke/result.jsontmp/task488-local-attachment-link-delete-undo-smoke/after-redo.png
  • node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js
    • 结果:ok=true
    • 覆盖:单附件 Backspace 删除保留相邻附件、真实附件文件不被级联删除、刷新后被删引用不回来。
  • codegraph index . --force && codegraph status .
    • 结果:索引 318 个文件,状态 up to date。
    • 复核:delete_selected_attachment_link_with_historyselected_local_attachment_link_textis_local_attachment_link_selected 均可通过 CodeGraph 搜索定位到 editor_runtime/*

10.3 Batch 2 执行记录(2026-05-24

Reasonix read-only audit

  • run id: reasonix-2026-05-24T15-01-33-138Z-dc32246a
  • worker worktree: /mnt/Data1T/mnote-wt-5-28-command-sync-b2
  • handoff: /home/lix/.codex/runtime/reasonix-coding-worker/reasonix-2026-05-24T15-01-33-138Z-dc32246a/process-handoff.md
  • 类型:只读审计,主控已读取 Hindsight recall、process-handoff.mdprocess-handoff.json

采纳内容:

  • 新增 editor_runtime/command_sync.rs,迁出 read_editor_snapshotsync_editor_outputssync_persisted_editor_command
  • 新增 editor_runtime/persistence.rs,迁出 PersistedSpikeDocumentPersistedDocumentIdentity、identity normalization、storage key、localStorage load/persist。
  • apply_document_update 迁入 command_sync.rs,保持迁移前零调用者现状。
  • apply_html_update 暂留 lib.rs:它仍强依赖 host change/status event、HoveredBlockState 和 toolbar overlay state;后续等 bridge_events.rs / overlays.rs 拆出后再迁。

Codex 主控复核:

  • persist_document_state 的 payload、storage key、localStorage 写入错误文案保持不变。
  • sync_persisted_editor_command 现在委托 persist_current_editor_snapshot(...)dirty count、自身快照读取、HTML/JSON output 更新和反馈文案顺序保持不变。
  • normalize_identity_value 改为 pub(crate),用于 mindmap draft identitypersisted_document_storage_key 只在 #[cfg(test)] 下导入。

已通过验证:

  • cargo fmt --manifest-path rust/Cargo.toml --all --check
  • cargo check --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml35 个既有 warning
  • cargo build --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml --target wasm32-unknown-unknown --release35 个既有 warning
  • wasm-bindgen rust/spikes/leptos-tiptap-spike/target/wasm32-unknown-unknown/release/mnote_leptos_tiptap_spike.wasm --target web --out-dir rust/spikes/leptos-tiptap-spike/generated/island --out-name mnote-leptos-tiptap-spike-island

受阻验证:

  • cargo test --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml persisted_document_storage_key_isolated_per_document_identity
  • cargo test --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml standalone_mindmap_object_uses_isolated_draft_identity
  • 阻塞原因:lib test 编译阶段命中既有 slash_menu_css_uses_viewport_overlay_layerSTYLE 未定义;HEAD 中该测试已引用 STYLE,而实际常量名为 SPIKE_STYLE。该问题不属于本批拆分,已记录为后续单独处理。

10.4 Batch 3 第一刀执行记录(2026-05-24

Reasonix read-only audit

  • run id: reasonix-2026-05-24T15-17-16-430Z-18054487
  • worker worktree: /mnt/Data1T/mnote-wt-5-28-upload-boundary-b3
  • handoff: /home/lix/.codex/runtime/reasonix-coding-worker/reasonix-2026-05-24T15-17-16-430Z-18054487/process-handoff.md
  • 类型:只读审计,主控已读取 Hindsight recall、process-handoff.mdprocess-handoff.json

采纳结论:

  • 当前上传链路应保持 editor intent -> CustomEvent -> shell pickup
  • attachment_upload.rs 只承接 dispatch_editor_upload_request(kind, accept) 这一类编辑器 intent,不迁入真实上传、文件选择器、API POST、资源归属或插入链接主流程。
  • local_markdown_paths.rs 暂不在 leptos-tiptap 内创建;本地 Markdown 路径、rootUri/api/local-folder/files/open URL、markdownRelativePath 与落盘语义归 03-rust-web shell host runtime / local-upload-runtime.js

采纳内容:

  • 新增 editor_runtime/attachment_upload.rs,迁出 dispatch_editor_upload_request(kind, accept)
  • src/lib.rsrun_slash_action 继续保留 SlashActionKind::Image / UploadAttachment 的 slash menu 决策,只调用 attachment_upload 发出上传 intent。
  • src/lib.rs 不再直接构造 mnote:editor-upload-request

已通过验证:

  • cargo fmt --manifest-path rust/Cargo.toml --all --check
  • cargo check --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml35 个既有 warning
  • cargo build --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml --target wasm32-unknown-unknown --release35 个既有 warning
  • wasm-bindgen rust/spikes/leptos-tiptap-spike/target/wasm32-unknown-unknown/release/mnote_leptos_tiptap_spike.wasm --target web --out-dir rust/spikes/leptos-tiptap-spike/generated/island --out-name mnote-leptos-tiptap-spike-island
  • node scripts/task488-local-attachment-link-delete-undo-smoke.js
    • 结果:ok=true
    • 证据:tmp/task488-local-attachment-link-delete-undo-smoke/result.json
  • node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js
    • 结果:ok=true
    • 证据:命令 stdout JSON,覆盖 editor-upload-to-page-resource-dirfiletree-folder-drop-to-targetbroken-link-after-real-file-deletedirect-attach-open-without-active-mdeditor-delete-one-attachment-preserves-adjacent 等检查。

后续归属:

  • resolveEditorUploadContexteditorRootFromUploadOptionsopenEditorUploadFilePicker 等 shell-side DOM/context 探针适合进入 03-rust-weblocal-upload-runtime.js 后续批次。
  • uploadFileToMediaAssetuploadFilesWithResolvedTargetinsertUploadedAssetIntoEditor 仍是高耦合主流程,暂不迁入 editor runtime。

10.5 Batch 4 块菜单删除 history 收口(2026-05-24

Codex 主控直接完成:

  • 新增 scripts/task489-block-menu-delete-undo-smoke.js,先在旧实现上确认 RED:块菜单删除 Beta block 后,Ctrl+Z 未恢复目标块,失败证据写入 tmp/task489-block-menu-delete-undo-smoke/result.json
  • editor_runtime/history_safe_commands.rs 新增 delete_top_level_block_with_history(editor, index)
    • 多块文档:计算顶层块完整 ProseMirror range 后调用 Tiptap delete_range(...)
    • 单块文档:用 insert_content_at(range, paragraph, update_selection=true) 替换为空段落。
    • 删除完成后仅调用 sync_persisted_editor_command(...) 同步快照和草稿,不再走 apply_html_update
  • 删除 src/lib.rs 中旧的 delete_top_level_block_html(...),避免块菜单删除继续保留 HTML 整文替换入口。

Reasonix read-only audit

  • run id: reasonix-2026-05-24T15-45-44-865Z-bb0ede13
  • handoff: /home/lix/.codex/runtime/reasonix-coding-worker/reasonix-2026-05-24T15-45-44-865Z-bb0ede13/process-handoff.md
  • 类型:只读审计,主控已读取 Hindsight recall、process-handoff.mdprocess-handoff.json
  • 采纳结论:可采纳;块菜单删除已从 apply_html_update(...) / set_content(...) 路径迁到 Tiptap delete_range(...) / insert_content_at(...)。Reasonix 提醒 sync_persisted_editor_command(...) 与 Tiptap on_change 后续可能存在 dirty count / localStorage 冗余,作为后续优化风险登记,不阻塞本批。

已通过验证:

  • node --check scripts/task489-block-menu-delete-undo-smoke.js
  • cargo fmt --manifest-path rust/Cargo.toml --all --check
  • cargo check --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml35 个既有 warning
  • cargo build --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml --target wasm32-unknown-unknown --release35 个既有 warning
  • wasm-bindgen rust/spikes/leptos-tiptap-spike/target/wasm32-unknown-unknown/release/mnote_leptos_tiptap_spike.wasm --target web --out-dir rust/spikes/leptos-tiptap-spike/generated/island --out-name mnote-leptos-tiptap-spike-island
  • node scripts/task489-block-menu-delete-undo-smoke.js
    • 结果:ok=true
    • 证据:tmp/task489-block-menu-delete-undo-smoke/result.jsontmp/task489-block-menu-delete-undo-smoke/after-redo.png
  • node scripts/task488-local-attachment-link-delete-undo-smoke.js
    • 结果:ok=true
    • 证据:tmp/task488-local-attachment-link-delete-undo-smoke/result.json
  • node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js
    • 结果:ok=true
    • 证据:命令 stdout JSON,覆盖上传目录、文件树 drop、broken link、无 active document 附件 tab、标题来源、附件键盘/手柄删除。
  • smoke 启动方式:
    • 使用稳定入口 MNOTE_WEB_BIND=0.0.0.0:3000 MNOTE_WEB_PUBLIC_BIND=127.0.0.1:3000 cargo run --manifest-path rust/Cargo.toml -p mnote-web --bin mnote-web
    • npm run dev:hot / cargo watch 在本轮验收中发生过重编译重启,导致 task479 中段 ERR_CONNECTION_REFUSED;该结果已废弃,不作为功能失败证据。
  • codegraph search delete_top_level_block_with_history top_level_block_full_range prosemirror_node_size
    • 结果:delete_top_level_block_with_historytop_level_block_full_rangeprosemirror_node_size 可定位到 rust/spikes/leptos-tiptap-spike/src/editor_runtime/history_safe_commands.rs

剩余风险:

  • 块手柄拖拽重排和复制副本仍使用 HTML 整文替换,继续登记为后续 history-safe command 切片;本设计 done gate 只要求至少一个已登记用户编辑路径完成迁移。

10.6 后续 P4 只读审计补充(2026-05-25

Reasonix read-only audit

  • run id: reasonix-2026-05-24T16-25-22-430Z-3a9a1a82
  • worktree: /mnt/Data1T/mnote-worktrees/5-28-dom-bridge-audit
  • handoff: /home/lix/.codex/runtime/reasonix-coding-worker/reasonix-2026-05-24T16-25-22-430Z-3a9a1a82/process-handoff.md
  • 类型:只读审计,主控已读取 Hindsight recall 与 process-handoff.md

可采纳结论:

  • 后续 P4 第一刀应优先新增 editor_runtime/dom_selection.rs,而不是直接抽 block_menu.rs
  • 第一刀候选函数:
    • active_editor_text_selection
    • selection_summary
    • has_active_text_selection
    • selection_has_rich_marks
    • selection_payload / selection event payload 构造
    • send_selection_state / send_selection_state_to_target
  • block_menu.rs 暂不先做:当前 block menu / overlay 相关函数仍强依赖大量 ReadSignal / WriteSignal,在合并 overlay state 或先迁出 HoveredBlockState 前,直接抽模块只会把长参数耦合搬到新文件。
  • overlays.rs 可作为 dom_selection.rs 之后的第二刀;apply_html_update 仍等 bridge_events.rs / overlay state 收口后再迁。

建议验证:

  • cargo fmt --manifest-path rust/Cargo.toml --all --check
  • cargo check --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml
  • cargo build --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml --target wasm32-unknown-unknown --release
  • wasm-bindgen rust/spikes/leptos-tiptap-spike/target/wasm32-unknown-unknown/release/mnote_leptos_tiptap_spike.wasm --target web --out-dir rust/spikes/leptos-tiptap-spike/generated/island --out-name mnote-leptos-tiptap-spike-island
  • node scripts/task488-local-attachment-link-delete-undo-smoke.js
  • node scripts/task489-block-menu-delete-undo-smoke.js

10.7 Batch 5 DOM selection 第一刀(2026-05-25

Codex 主控基于上一批 Reasonix P4 只读审计直接完成:

  • 新增 editor_runtime/dom_selection.rs
  • 迁出纯 DOM / selection helper
    • node_is_within_root
    • active_editor_text_selection
    • has_active_text_selection
    • selection_summary
    • selection_has_rich_marks
  • src/lib.rs 保留 SelectionPayloadselection_payloadsend_selection_statesend_selection_state_to_targetselection_event_payload 和事件 dispatch;这些仍依赖 bridge event、HoveredBlockState、block id 解析和 target dispatch,后续等 bridge_events.rs / overlay state 边界更稳后再迁。
  • editor_root_element() 改为 pub(crate),供 dom_selection.rs 读取当前编辑器根节点;shell/resource 语义未进入 editor DOM 模块。

已通过验证:

  • cargo fmt --manifest-path rust/Cargo.toml --all --check
  • cargo check --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml35 个既有 warning
  • cargo build --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml --target wasm32-unknown-unknown --release35 个既有 warning
  • wasm-bindgen rust/spikes/leptos-tiptap-spike/target/wasm32-unknown-unknown/release/mnote_leptos_tiptap_spike.wasm --target web --out-dir rust/spikes/leptos-tiptap-spike/generated/island --out-name mnote-leptos-tiptap-spike-island
  • node scripts/task488-local-attachment-link-delete-undo-smoke.js
    • 结果:ok=true
    • 证据:tmp/task488-local-attachment-link-delete-undo-smoke/result.json
  • node scripts/task489-block-menu-delete-undo-smoke.js
    • 结果:ok=true
    • 证据:tmp/task489-block-menu-delete-undo-smoke/result.json
    • 备注:首次与 task488 并行执行时在 Ctrl+Z 等待上超时;单独重跑通过,后续 smoke 建议串行跑编辑器键盘类场景,避免浏览器/服务端时序争用误报。

后续建议:

  • 下一刀可迁 floating_toolbar_anchor_from_selection / sync_text_selection_overlay 等 overlay anchor 纯计算部分进入 overlays.rs
  • selection_payload / send_selection_state* 应与 bridge_events.rs 一起迁,避免为了单次拆分扩大 SelectionPayloadHoveredBlockState 和 runtime block id 的可见性。

11. 测试与验收

Rust 检查

cargo fmt --manifest-path rust/Cargo.toml --all --check
cargo check --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml
cargo build --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml --target wasm32-unknown-unknown --release
wasm-bindgen rust/spikes/leptos-tiptap-spike/target/wasm32-unknown-unknown/release/mnote_leptos_tiptap_spike.wasm --target web --out-dir rust/spikes/leptos-tiptap-spike/generated/island --out-name mnote-leptos-tiptap-spike-island

Browser smoke

node scripts/task488-local-attachment-link-delete-undo-smoke.js
node scripts/task489-block-menu-delete-undo-smoke.js
node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js
node scripts/task472-side-target-secondary-pane-smoke.js

后续拆到 block menu / table toolbar 时,应补对应 smoke,不复用附件 smoke 代替。

12. 风险与控制

  • 风险:拆模块时改变 Leptos signal 捕获和生命周期。
    • 控制:每次只迁一个行为域;迁移后立即跑 wasm build 和浏览器 smoke。
  • 风险:为了通过编译把函数参数扩得过大。
    • 控制:只在边界稳定后引入小型 context struct;不要先抽象大而全 runtime context。
  • 风险:把 shell/resource 语义搬进 editor 模块。
    • 控制:editor 只发出 intent,不决定 workspace/resource 真相。
  • 风险:继续产生新的巨型模块。
    • 控制:单模块超过约 800 行必须拆出子模块。

13. Done Gate

本设计移动到 done/ 前必须满足:

  • attachment_links.rshistory_safe_commands.rs 已落地。
  • command_sync.rs 已落地,用户编辑动作与 host 替换动作边界明确。
  • 至少一个仍使用 set_content 的用户编辑路径已迁到 Tiptap command/transaction。
  • task488 通过,且至少覆盖一次真实键盘 Ctrl+Z/Ctrl+Y
  • 新增 editor_runtime Rust 模块能被 CodeGraph 定位。
  • src/lib.rs 不再新增附件/history/DOM bridge 大块逻辑。

14. 后续问题

  • 是否把 leptos-tiptap-spike 从 spike 正式改名:不作为本设计前置,等模块拆分后再评估。
  • 是否回 upstream reference-code/leptos-tiptap 增加命令:只有当底层 bridge 缺少通用 Tiptap command 时才考虑;MNote 特有行为优先留在 editor_runtime
  • 是否引入更强类型的 editor command contractP1-P3 完成后再评估,不作为第一阶段阻塞项。