Files
mnote/design/10-review/done/17-sidex-mnote-workbench-gap-execution-checklist-v1.md
T
lix-2026 1882db7681 收口 MNote P0 P1 P2 审查尾项
- 归档 OnlyOffice live bridge、Page AI、mindmap、design governance 与相关 bug 条目
- 补齐 MinerU OCR 后端 runtime 合同与 smoke/test 基线
- 收口 ChatOnly/Doubao、ObjectIdentity、Page Aggregate compat 与 runtime owner 文档口径

验证:
- cargo test --manifest-path rust/Cargo.toml -p mnote-web local_ocr -- --test-threads=1
- cargo test --manifest-path rust/Cargo.toml -p mnote-web onlyoffice_bridge -- --test-threads=1
- git diff --check
- git diff --cached --check
- codegraph index . --force && codegraph status .
- codegraph sync . && codegraph status .
2026-06-01 09:29:12 +08:00

695 lines
47 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 17 Sidex / MNote Workbench Gap Execution Checklist v1
> 状态:done
>
> 创建时间:2026-05-27
>
> Owner10-review / 05-editor-mainline / 03-rust-web / 04-tree-domain / 07-ai
>
> 目标:把本轮 Sidex / VSCode 对照审查转成可执行 checklist,优先补齐 MNote 当前最缺的统一工作台服务层,而不是继续扩散 UI 分支或照搬 VSCode 全量复杂度。
## 1. 背景结论
MNote 当前已经具备知识库型 VSCode-like workspace 骨架:
- Rust Web 3000 主壳、SSR 文档页和独立 browser runtime asset。
- Resource Tree / File Tree / Page Tree 三层模型。
- local-first Markdown、Page Aggregate、leptos-tiptap island。
- resource tab、secondary pane、DocumentBuffer、冲突面板。
- tree live WS/SSE、本地文件夹 watcher、本地搜索索引。
- Hermes / Reasonix local-first 文件编辑控制面。
相比 Sidex / VSCode,当前最大差距不是某个控件,而是统一服务层仍分散:
- `ObjectWorkspacePath` / `KernelObjectIdentity` 没有成为 browser runtime 的统一身份消费入口。
- `BufferStore` 已存在,但 tiptap、watcher、AI、resource tab 仍有各自 session / dirty / conflict 状态。
- resource tab / open editors 仍是页面内 Map,不是可恢复、可被 Sidebar / 快捷键 / AI target resolver 共同消费的轻量 workbench editor model。
- command context / when / keybinding / menu enablement 在 Rust core-protocol 与 browser JS 中各有简化版本。
- tree live 仍有 local-folder SSE、watch batch、projection refresh、polling fallback 多链并存。
- `tree.*` 已是 preferred command,但 compat 路径仍保留 `documents.*` 映射。
## 2. Sidex 对照边界
### 2.1 值得借鉴
| Sidex / VSCode 模型 | MNote 对应改进 |
| --- | --- |
| `ExplorerModel -> ExplorerService -> ExplorerView` 三层分工 | FileTree / PageTree 继续拆成 projection data source、runtime service、DOM view state,不让 DOM 自己拼真相 |
| `IEditorService.openEditor` + `IEditorGroup` | 建立轻量 `MNoteEditorInput` / `MNoteEditorGroupState` / `OpenEditorsSnapshot`,统一 page、resource tab、secondary pane |
| `EditorGroupModel` 管 active / preview / sticky / transient / MRU / selection | resource tab 补 preview / pinned / dirty close veto / MRU / keyboard action 的模型层 |
| ContextKeyService + CommandsRegistry + KeybindingsRegistry | MNote 建立统一 command context,用 Rust command contract 驱动 browser menu / shortcut enablement |
| Working Copy dirty / save / backup / conflict | BufferStore 成为 tiptap、watcher、AI、resource tab 的唯一打开态和版本仲裁面 |
| Explorer watcher 节流、编辑中抑制、局部 parent refresh | local-folder watcher / filetree projection 只刷新受影响 parent,避免整树替换与编辑冲突 |
### 2.2 不照搬
- 不引入完整 VSCode DI / service container。
- 不把文件系统 Explorer 当成 MNote 树真相;MNote 真相仍是 Rust kernel projection。
- 不引入完整 Extension Host、Editor Override、Auxiliary Editor、多窗口 grid。
- 不把 Monaco TextModel 当成 Markdown 事实源。
- 不把 Sidex 的 `ResourceFileEdit` 直接替代 `tree.*` / `tree.resource.*` 命令面。
- terminal / git / debug / tasks / extensions 是否进入 MNote,单独做产品边界决策,不混入本 checklist 的 P0。
## 3. 执行原则
- 优先做服务层统一,不做大规模视觉重写。
- 每一批必须有 smoke 或 Rust/JS 定点测试;浏览器可见能力必须有真实用户路径验证。
- 不新增第二套树真相、页面真相或资源生命周期真相。
- 不碰与本 checklist 无关的历史清理、回收区删除或用户已有改动。
- 对旧 compat 只做可证明的瘦身;不能为了“干净”破坏 cloud / remote / legacy 边界。
## 4. Checklist
### Batch 0:基线取证与任务拆分
状态:done
- [x] 对照 Sidex Explorer / EditorGroup / command / working copy 模型。
- [x] 对照 MNote Rust Web / browser runtime 当前能力。
- [x] 对照现有 design / smoke / process backlog。
- [x] 确认本轮不做代码清理、不改用户已有脏改动。
- [x] 输出本执行 checklist,并把后续工作拆成 P0 / P1 / P2。
验收证据:
- `reference-code/sidex-main/src/vs/workbench/contrib/files/common/explorerModel.ts`
- `reference-code/sidex-main/src/vs/workbench/contrib/files/browser/explorerService.ts`
- `reference-code/sidex-main/src/vs/workbench/common/editor/editorGroupModel.ts`
- `rust/crates/core-protocol/src/kernel.rs`
- `rust/crates/mnote-web/src/document_buffer_store.rs`
- `rust/crates/mnote-web/browser/document-resource-tab-runtime.js`
- `rust/crates/mnote-web/browser/tree-live-controller.js`
### Batch 1:轻量 Workbench Editor Model
状态:done
目标:
- 定义 MNote 轻量 `EditorInput` / `EditorGroupState` / `OpenEditorsSnapshot` 合同。
- 统一 page、resource tab、secondary pane 的打开态。
- 让 Sidebar、快捷键、AI target resolver 和 smoke 都能消费同一个 open editors snapshot。
Sidex 对照:
- Sidex `EditorGroupModel` 持有 active editor、preview editor、sticky count、MRU、selection。
- Sidex Explorer 打开资源统一走 `IEditorService.openEditor({ resource, options })`
- MNote 不照搬多 group grid,只保留 primary / secondary pane 和 resource tab 的轻量模型。
待办:
- [x] 盘点当前 open editor 数据源:page tab、resource tab registry、secondary pane、URL query、`window.__mnoteOpenEditorsSnapshot`
- [x] 写一个最小合同文档或 Rust/JS 类型注释,明确字段:
- `objectIdentity`
- `workspacePath`
- `paneRole`
- `editorKind`
- `active`
- `dirtyState`
- `preview`
- `pinned`
- `lastActiveAt`
- [x]`document-resource-tab-runtime.js` 的 open editors snapshot 增加 primary/secondary 分组语义。
- [x] 补 smoke:打开页面、打开 mindmap/office/code/text 资源、切换 tab、刷新后至少能恢复 active page/resource 的可见状态。
- [x] 补 AI target resolver 输入:last focused editor / resource tab 应来自 open editors snapshot,不再从 DOM 临时猜。
本轮最小实现切片:
- [x] `OpenEditorsSnapshot` 保持 `mnote.open_editors_snapshot.v1`,新增 `groups.primary` / `groups.secondary`
- [x] page editor entry 与 resource editor entry 均输出 `paneRole``editorKind``preview``pinned``lastActiveAt`
- [x] resource tab entry 补 `documentId` / `workspaceId`,为后续 `ObjectWorkspacePath` 消费统一预留字段。
- [x] page / resource editor entry 补 `workspacePath`,合同以 `mnote.workspace_path.v1` 表达 workspace/source/root/relativePath/documentId/objectIdentity。
- [x] passive resourceOffice/PDF/image 等无 session tab)激活后也写入 `lastActiveAt`,避免 MRU / AI target resolver 后续只能看到 0。
- [x] `task457-main-editor-resource-tab-smoke` 覆盖 primary markdown resource active、primary pinned page、secondary page editor。
- [x] `task457-main-editor-resource-tab-smoke` 覆盖 Office passive resource active snapshot 的 `documentId` / `workspaceId` / `editorKind` / `lastActiveAt`
- [x] `task457-main-editor-resource-tab-smoke` 覆盖 canonical `resourceTab` URL 刷新后恢复 active markdown resource tab,并验证 `workspacePath.relativePath`
- [x] `sidebar-page-ai-runtime.js``OpenEditorsSnapshot` 生成 `mnote.ai_editor_target.v1`,写入 `pageContext.aiContext.activeEditorTarget``openEditorsSnapshot``/api/hermes/client/runs``editorTarget`
- [x] `hermes_client.rs` 在 local source instructions 中保留 `editorTarget`,避免 agent 只靠 URL / DOM 猜当前编辑目标。
- [x] 同步 `task457` 中已过期的 Office 默认 iframe 断言:默认主编辑区是 `/office-preview` 轻量预览,显式 `new-window` 仍走 `/onlyoffice?mode=edit`
本轮验证证据:
```bash
node --check rust/crates/mnote-web/browser/document-resource-tab-runtime.js
node --check scripts/task457-main-editor-resource-tab-smoke.js
git diff --check -- design/10-review/process/17-sidex-mnote-workbench-gap-execution-checklist-v1.md rust/crates/mnote-web/browser/document-resource-tab-runtime.js scripts/task457-main-editor-resource-tab-smoke.js
node scripts/task457-main-editor-resource-tab-smoke.js
node --check scripts/task501-office-preview-light-viewer-smoke.js
node scripts/task501-office-preview-light-viewer-smoke.js
node scripts/task490-runtime-surfaces-smoke.js
cargo test --manifest-path rust/Cargo.toml -p mnote-web office_preview_page_serves_lightweight_viewer_shell -- --test-threads=1
cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_client_run_body_local_source_uses_file_scope_not_full_page_context -- --test-threads=1
node --check rust/crates/mnote-web/browser/document-editor-adapter-runtime.js
node --check rust/crates/mnote-web/browser/sidebar-page-ai-runtime.js
node --check scripts/task453-local-folder-page-ai-changed-files-smoke.js
node scripts/task453-local-folder-page-ai-changed-files-smoke.js
```
建议验证:
```bash
node --check rust/crates/mnote-web/browser/document-resource-tab-runtime.js
node scripts/task496-editor-open-parallel-runtime-aggregate-smoke.js
node scripts/task490-runtime-surfaces-smoke.js
```
### Batch 2ObjectWorkspacePath / Resource Identity runtime 消费统一
状态:done
目标:
- browser runtime 不再主要依赖 `documentId + rootUri + relativePath + assetId` 临时拼接身份。
- FileTree、resource open、copy-id、AI scope、tree command 参数统一消费 `ObjectWorkspacePath` / `KernelObjectIdentity`
Sidex 对照:
- Sidex ExplorerItem 的 identity 基于 root resource + item resource。
- MNote identity 必须多带 `workspaceId/sourceKind/rootUri/relativePath/objectIdentity`,不能照搬纯文件 URI。
待办:
- [x] 盘点 browser runtime 中手拼 identity 的位置:
- `filetree-runtime.js`
- `sidebar-filetree-command-runtime.js`
- `document-resource-tab-runtime.js`
- `resource-open-runtime.js`
- `sidebar-filetree-open-runtime.js`
- [x] 为 FileTree row 输出 / 读取建立统一 helper:`readWorkspacePathFromRow(row)`
- [x] `copy-id` 和 resource tab key 优先使用 workspacePath / objectIdentity。
- [x] local_folder 下绝对路径还原继续从 `rootUri + relativePath` 得出,但只作为展示/复制结果,不作为内部 identity。
- [x] 补 smokelocal_folder Markdown、folder、asset、mindmap、office row 的 identity 一致且复制路径不暴露 `local:*` projection id。
本轮最小实现切片:
- [x] `filetree-runtime.js` 新增 `readWorkspacePathFromRow(row, deps)`,统一读取 `workspaceId/sourceKind/rootUri/relativePath/objectIdentity/rowId/documentId/assetId/title`
- [x] `fileTreeAssetDownloadDetail` 与 filetree context menu detail 开始携带 `workspacePath`,为后续 copy/open/command context 统一消费铺底。
- [x] `task495-filetree-copy-id-absolute-path-smoke` 增加 `readWorkspacePathFromRow` 定点断言,同时保持 local_folder 复制 ID 返回绝对路径。
- [x] `sidebar-tree-runtime.js` 的 filetree open / asset open event detail 携带 `workspacePath`resource open 不再只能从 DOM 临时拼身份。
- [x] `sidebar-filetree-open-runtime.js``workspacePath.objectIdentity/rootUri/relativePath/assetId` 生成 resource tab object identity,覆盖 local file、mindmap、office、pdf 和侧栏打开。
- [x] `document-resource-tab-runtime.js` 在 resource tab entry 中保留 `workspacePath`snapshot 输出优先沿同一 workspacePath 合同。
- [x] `local_folder_source.rs` 的真实 local_folder projection 为 mindmap/office/asset row 写入 `workspacePath.objectIdentity.assetId`,避免资源行 identity 碰撞。
- [x] `filetree-runtime.js` 收窄 `isLocalFolder` 判定:只信 `sourceKind=local_folder`,或 sourceKind 缺失时的明确 local row/doc 前缀,避免误伤 cloud/compat id。
- [x] 恢复 legacy `luckysheet` / `.luckysheet` table 分类,避免 table row 被误归为普通 file asset。
本轮验证证据:
```bash
node --check rust/crates/mnote-web/browser/document-resource-tab-runtime.js
node --check rust/crates/mnote-web/browser/filetree-runtime.js
node --check rust/crates/mnote-web/browser/sidebar-filetree-command-runtime.js
node --check rust/crates/mnote-web/browser/sidebar-filetree-open-runtime.js
node --check rust/crates/mnote-web/browser/sidebar-tree-runtime.js
node --check scripts/task495-filetree-copy-id-absolute-path-smoke.js
node scripts/task495-filetree-copy-id-absolute-path-smoke.js
cargo test --manifest-path rust/Cargo.toml -p mnote-web local_file_tree_classifies_mindmap_and_office_assets -- --test-threads=1
node scripts/task457-main-editor-resource-tab-smoke.js
node scripts/task453-local-folder-page-ai-changed-files-smoke.js
node scripts/task490-runtime-surfaces-smoke.js
node scripts/task501-office-preview-light-viewer-smoke.js
git diff --check -- rust/crates/mnote-web/browser/document-resource-tab-runtime.js rust/crates/mnote-web/browser/filetree-runtime.js rust/crates/mnote-web/browser/sidebar-filetree-command-runtime.js rust/crates/mnote-web/browser/sidebar-filetree-open-runtime.js rust/crates/mnote-web/browser/sidebar-tree-runtime.js rust/crates/mnote-web/src/routes/local_folder_source.rs scripts/task495-filetree-copy-id-absolute-path-smoke.js design/10-review/process/17-sidex-mnote-workbench-gap-execution-checklist-v1.md
```
边界记录:
- `node scripts/task456-resource-object-shell-sync-smoke.js` 当前走已退役 Convex compat,失败为 `503 convex_retired`,不作为 local-first Batch 2 验收入口。
- `node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js` 暴露上传后 dirty conflict 与删除附件后等待超时,归入 Batch 3/资源生命周期后续处理,不在本 identity 切片扩大修复面。
建议验证:
```bash
node --check rust/crates/mnote-web/browser/filetree-runtime.js
node --check rust/crates/mnote-web/browser/sidebar-filetree-command-runtime.js
node --check scripts/task495-filetree-copy-id-absolute-path-smoke.js
node scripts/task495-filetree-copy-id-absolute-path-smoke.js
node scripts/task456-resource-object-shell-sync-smoke.js
node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js
```
### Batch 3BufferStore / Working Copy 唯一打开态
状态:done
目标:
- `BufferStore` 成为 tiptap、watcher、AI、resource tab 的统一 dirty / stale / external modified / deleted 仲裁面。
- browser session 的 dirty/conflict 状态不再独立形成第二套事实。
Sidex 对照:
- VSCode WorkingCopyService 统一 dirty、save、backup、conflict。
- MNote 不引入完整 backup service;先把 local-first Markdown 的 dirty/stale/deleted 状态统一到 BufferStore。
待办:
- [x] 盘点 `documentSessionRegistry`、localStorage draft、`conflictDetectionKey``expectedFileVersion` 当前写读链。
- [x] 让 watcher 标记外部修改后,前端 session 从 BufferStore / aggregate 读取状态,而不是只靠本地 session flag。
- [x] 资源 tab close guard 读取统一 dirty state。
- [x] AI 写入前检查目标 buffer dirty/staledirty 时必须显式确认或阻断。
- [x] 补 smokeclean buffer 外部写入可同步;dirty buffer 外部写入弹冲突;删除当前文件后 tab 不静默保留旧正文。
本轮最小实现切片:
- [x] Sidex 对照:借鉴 `WorkingCopyService` 的统一 dirty/event 仲裁;不引入 VSCode 完整 backup/hot-exit/URI provider。
- [x] `documents.rs` 增加 `POST /api/documents/buffer-state/dirty`,浏览器编辑变 dirty 时写入 Rust `BufferStore::mark_dirty`,保存成功仍由 Rust 写链 `mark_saved` 清理。
- [x] `document-session-runtime.js` 增加 `fetchSessionBufferState()` / `applyBufferStateToSession()`,外部刷新前先读取 `/api/documents/buffer-state`,对 `Dirty/Stale/Deleted` 进入冲突/删除保护,不再只靠本地 session flag。
- [x] `queueSessionSave()` 在 autosave 排队时同步标记 BufferStore dirty,形成 watcher / AI / save 共享的打开态。
- [x] clean 外部写入、dirty 外部写入、保存失败保留编辑器内容、AI dirty 写入冲突路径均已通过 smoke。
- [x] `sidebar-page-ai-runtime.js` 在发起 Hermes run 前读取 OpenEditorsSnapshot 与 `/api/documents/buffer-state`,当目标 buffer 为 `Dirty` / `Stale` / `Deleted` / `ExternalModified` 时阻断 AI 写入,不再让 dirty buffer 进入 agent 写盘后冲突。
- [x] `document-resource-tab-runtime.js` 的 page entry 从当前 `documentSessionRegistry` 输出 dirty state,避免页面刚输入但 `/buffer-state/dirty` 仍未完成时 Page AI 被放行。
- [x] `document-resource-tab-runtime.js` 的 resource tab close 在关闭前 await 读取 BufferStore dirty state,并把 `Dirty` / `Stale` / `Deleted` / `ExternalModified` 纳入 close veto;本地 resource session 的小写 `dirty` 同时被 Page AI 阻断逻辑规范化识别。
- [x] `local-upload-runtime.js` 上传保存成功后广播 `mnote:local-upload-editor-save-completed``document-session-runtime.js` 同步 session fileVersion / conflict key,避免编辑器自己写盘被 watcher 误判为外部冲突。
- [x] `sidebar-attachment-open-runtime.js` 为 local_folder 附件链接增加存在性刷新、missing 状态缓存、stat 请求序号和幂等 DOM 写入;删除真实附件后 Markdown 链接保留,点击进入 resource tab missing/error statestat 非 OK 不再伪装成 missing,而是写入诊断属性。
本轮验证证据:
```bash
node --check rust/crates/mnote-web/browser/document-session-runtime.js
cargo test --manifest-path rust/Cargo.toml -p mnote-web documents_buffer_state_falls_back_to_document_id_without_relative_path -- --test-threads=1
cargo test --manifest-path rust/Cargo.toml -p mnote-web document_buffer_mark_dirty_and_saved_round_trip -- --test-threads=1
node scripts/task436-local-markdown-open-document-external-change-smoke.js
node scripts/task451-local-markdown-conflict-resolution-ui-smoke.js
node scripts/task486-local-markdown-save-error-editor-preserves-content-smoke.js
node scripts/task453-local-folder-page-ai-changed-files-smoke.js
node scripts/task457-main-editor-resource-tab-smoke.js
node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js
```
补充验证证据(2026-05-28):
```bash
node --check rust/crates/mnote-web/browser/document-session-runtime.js
node --check rust/crates/mnote-web/browser/local-upload-runtime.js
node --check rust/crates/mnote-web/browser/sidebar-attachment-open-runtime.js
node --check rust/crates/mnote-web/browser/sidebar-page-ai-runtime.js
node --check rust/crates/mnote-web/browser/document-resource-tab-runtime.js
node --check scripts/task460-resource-tab-close-dirty-smoke.js
node --check scripts/task453-local-folder-page-ai-changed-files-smoke.js
node --check scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js
git diff --check -- rust/crates/mnote-web/browser/document-resource-tab-runtime.js rust/crates/mnote-web/browser/document-session-runtime.js rust/crates/mnote-web/browser/local-upload-runtime.js rust/crates/mnote-web/browser/sidebar-attachment-open-runtime.js rust/crates/mnote-web/browser/sidebar-page-ai-runtime.js scripts/task453-local-folder-page-ai-changed-files-smoke.js scripts/task460-resource-tab-close-dirty-smoke.js scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js design/10-review/process/17-sidex-mnote-workbench-gap-execution-checklist-v1.md
cargo test --manifest-path rust/Cargo.toml -p mnote-web documents_buffer_state_falls_back_to_document_id_without_relative_path -- --test-threads=1
cargo test --manifest-path rust/Cargo.toml -p mnote-web document_buffer_mark_dirty_and_saved_round_trip -- --test-threads=1
node scripts/task436-local-markdown-open-document-external-change-smoke.js
node scripts/task451-local-markdown-conflict-resolution-ui-smoke.js
node scripts/task486-local-markdown-save-error-editor-preserves-content-smoke.js
node scripts/task453-local-folder-page-ai-changed-files-smoke.js
node scripts/task460-resource-tab-close-dirty-smoke.js
node scripts/task457-main-editor-resource-tab-smoke.js
node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js
```
边界记录:
- Sidex / VSCode WorkingCopyService 的统一 dirty / conflict / backup 模型只借鉴为 BufferStore 仲裁面;MNote 暂不引入完整 hot-exit / backup service。
- Page AI dirty buffer 策略本批选择阻断,不做确认弹窗;后续若需要“显式确认后继续”,应进入 07-ai 单独交互设计。
- `task479` 的 broken link 检查在 headless 中显式调用现有 attachment refresh hook,避免浏览器定时器调度导致误判;真实 runtime 仍保留 tree event 与定时刷新。该项只证明 refresh/runtime 行为正确,不单独证明定时器或 tree event 调度可靠,后续 Batch 5 需要补真实 event 链路覆盖。
### Batch 4Page Aggregate legacy fallback 退役
状态:done
目标:
- 继续减少 `documents.content` / legacy body payload / compat projection 在 runtime 主链中的占比。
- Page Aggregate 成为页面读取和编辑刷新事实入口。
Sidex 对照:
- Sidex editor model 有明确输入和持久化模型。
- MNote 的主模型不是 Monaco TextModel,而是 local Markdown -> Page Aggregate -> tiptap projection。
待办:
- [x] 盘点 `document-tiptap-conversion-runtime.js` 中 legacy conversion 的 runtime 调用面。
- [x] 区分 local-first `.md` projection、cloud compat projection、fixture/test helper。
- [x] 为 Page Aggregate 输出增加或验证 source 标记:避免 UI 偏好 / 页面事实 / compat fallback 混淆。
- [x] 逐步把可控 runtime fallback 改成显式 degraded / compat owner。
- [x] 补 smoke:刷新后 body/blockDocument/options/source 一致,不能静默读旧 legacy 内容。
本轮最小实现切片:
- [x] Sidex 对照:借鉴 `EditorInput` 显式 input/resource/capabilities 边界;MNote 不照搬 EditorResolver,只把页面正文 source 分类写入 runtime session。
- [x] `document-tiptap-conversion-runtime.js` 增加 `pageBodyTiptapDocumentSource()`,把 `local_markdown.content``page_aggregate.block_document``compat.legacy_content``degraded.fallback_text``empty` 显式分类,避免无标记地从 legacy `body.content` 猜正文来源。
- [x] `document-session-runtime.js` 保存 `pageBodySource/projectionSource/blockProjectionVersion``document-editor-adapter-runtime.js` 在编辑器 root 上写入 `data-mnote-page-body-source` / `data-mnote-projection-source` / `data-mnote-block-projection-version`
- [x] `task167-local-markdown-title-body-options-no-convex-smoke.js` 断言 local-first Page Aggregate 的 `projectionSource=local_markdown.content``blockDocument` 存在,保存后不退回 legacy `documents.content`
本轮验证证据:
```bash
node --check rust/crates/mnote-web/browser/document-tiptap-conversion-runtime.js
node --check rust/crates/mnote-web/browser/document-session-runtime.js
node --check rust/crates/mnote-web/browser/document-editor-adapter-runtime.js
node --check scripts/task167-local-markdown-title-body-options-no-convex-smoke.js
git diff --check -- rust/crates/mnote-web/browser/document-tiptap-conversion-runtime.js rust/crates/mnote-web/browser/document-session-runtime.js rust/crates/mnote-web/browser/document-editor-adapter-runtime.js scripts/task167-local-markdown-title-body-options-no-convex-smoke.js design/10-review/process/17-sidex-mnote-workbench-gap-execution-checklist-v1.md
node scripts/task167-local-markdown-title-body-options-no-convex-smoke.js
node scripts/task484-local-folder-page-body-refresh-readback-smoke.js
node scripts/task493-page-settings-sqlite-preferences-smoke.js
cargo test --manifest-path rust/Cargo.toml -p mnote-web page_aggregate_endpoint_returns_local_markdown_readonly_snapshot -- --test-threads=1
cargo test --manifest-path rust/Cargo.toml -p mnote-web local_page_options_metadata_flows_into_page_aggregate -- --test-threads=1
```
边界记录:
- `node scripts/task-page-aggregate-body-sync-smoke.js``node scripts/task-page-aggregate-refresh-persistence-smoke.js` 当前通过旧 `/api/tree/commands` 入口创建文档,失败为 `503 convex_retired`;它们属于 legacy/cloud helper,不作为 local-first Batch 4 验收入口。
- 本批只把 fallback 分类显式化并锁住 local-first source,不删除 legacy conversion 函数;cloud compat / fixture 仍可显式走 `compat.legacy_content`
建议验证:
```bash
node scripts/task167-local-markdown-title-body-options-no-convex-smoke.js
node scripts/task-page-aggregate-body-sync-smoke.js
node scripts/task-page-aggregate-refresh-persistence-smoke.js
```
### Batch 5Tree live cache / FileTree data source 收敛
状态:done
目标:
- 减少 watcher/SSE/projection refresh/polling 多链并存。
- FileTree refresh 按受影响 parent 局部刷新,保留展开、选择和焦点。
Sidex 对照:
- Sidex AsyncDataTree 按 node resolve children,有 in-flight 去重、slow state、view state。
- MNote 已有 lazy/cache/view state 设计,继续把它固化为 data source 而不是整树 DOM 替换。
待办:
- [x] 盘点 `/api/local-folder/events?treeLive=true``tree:delta``watch_batch`、projection refresh、polling fallback 的调用关系。
- [x] 定义统一 local-folder tree event payload 到 filetree parent refresh 的映射。
- [x] 整树替换只保留在 root scope 切换或 resync 场景。
- [x] view state 保存只存 expanded/selected/focused 等用户状态,不存 rows/cache 派生数据。
- [x] 补大目录真实用户路径 smoke:从首页进入本地 workspace,点击星标/Explorer 逐级打开目标 Markdown,记录 longtask 和 editor ready。
本轮最小实现切片:
- [x] Sidex 对照:借鉴 `ExplorerModel` / `ExplorerService` / `AsyncDataTree` 的节点身份、局部 children resolve、in-flight 去重和 view state 思路;MNote 不照搬 VSCode 完整树控件,仍以 Rust projection / watcher revision 为事实源。
- [x] `sidebar-tree-live-apply-runtime.js``watch_batch``fallbackResync/requiresResync` 下显式走 `refreshLocalFolderSidebarSnapshot()`,非 resync 情况只刷新受影响 parent。
- [x] `watch_batch` 缺少 `affectedParents` 但包含 `changedPaths` 时,从 changed path 派生 parent scope,并写入 `data-mnote-local-folder-watch-batch-derived-parents` 作为诊断标记。
- [x] 修复 scoped navigation shell 缺 primary editor host 导致 `openPrimaryDocument()``pane_root_missing_primary` 并回退整页 reload 的问题:`HomePage` navigation 分支保留导航 placeholder,同时渲染隐藏 primary `DocumentPane` 供 pane runtime 原地替换。
- [x] `document-editor-adapter-runtime.js` 在 primary 文档成功激活后隐藏 navigation placeholder,保持点击 scoped FileTree Markdown 后不重建侧栏、不丢 JS marker。
本轮验证证据:
```bash
node --check rust/crates/mnote-web/browser/sidebar-tree-live-apply-runtime.js
node --check rust/crates/mnote-web/browser/document-editor-adapter-runtime.js
rustfmt --edition 2021 --check rust/crates/mnote-web/src/ssr/pages/home.rs
git diff --check -- rust/crates/mnote-web/browser/sidebar-tree-live-apply-runtime.js rust/crates/mnote-web/browser/document-editor-adapter-runtime.js rust/crates/mnote-web/src/ssr/pages/home.rs scripts/task494-filetree-lazy-loading-dedup-smoke.js design/10-review/process/17-sidex-mnote-workbench-gap-execution-checklist-v1.md
node scripts/task494-filetree-lazy-loading-dedup-smoke.js
node scripts/task492-sidebar-starred-shortcuts-smoke.js
node scripts/task497-local-page-tree-filetree-open-performance-smoke.js
node scripts/task499-sidebar-tree-view-state-smoke.js
cargo test --manifest-path rust/Cargo.toml -p mnote-web root_entry_local_folder_without_page_renders_navigation_page_without_editor_bootstrap -- --test-threads=1
cargo test --manifest-path rust/Cargo.toml -p mnote-web root_entry_local_folder_scope_renders_navigation_without_root_sibling_document -- --test-threads=1
cargo test --manifest-path rust/Cargo.toml -p mnote-web root_entry_active_page_includes_document_panes_bootstrap -- --test-threads=1
```
边界记录:
- `watch_batch` 到 FileTree parent refresh 只消费 Rust local-folder event payload;前端不新增 rows/cache 事实源。
- `view-state` smoke 证明持久化内容仍是 expanded / selected / focused / active / scroll 等用户状态,不把 rows 或 projection cache 写入 view state。
- scoped navigation shell 的 primary `DocumentPane` 是 editor host 能力补齐,不改变 local_folder navigation page 的 Page Aggregate / editor bootstrap 初始加载边界;导航页仍不输出 `__MNOTE_PAGE_AGGREGATE__``__MNOTE_EDITOR_BOOTSTRAP__`
建议验证:
```bash
node scripts/task492-sidebar-starred-shortcuts-smoke.js
node scripts/task494-filetree-lazy-loading-dedup-smoke.js
node scripts/task497-local-page-tree-filetree-open-performance-smoke.js
node scripts/task499-sidebar-tree-view-state-smoke.js
```
### Batch 6Command Context / Keybinding / Menu Enablement 统一
状态:done
目标:
- 统一 readonly、selection、resource kind、dirty、AI write capability、source kind 等上下文。
- browser menu、快捷键、tree command enablement 消费同一 context,而不是多处硬编码。
Sidex 对照:
- Sidex 由 ContextKeyService + CommandsRegistry + KeybindingsRegistry 统一驱动。
- MNote 只做轻量实现,最终执行仍落到 Rust kernel / `tree.*` command。
待办:
- [x] 对齐 Rust `CommandContext` 和 browser `when` parser 的字段名。
- [x] 建立 `buildFileTreeCommandContext(row, openEditorsSnapshot, bufferState)`
- [x] 右键菜单、键盘动作、toolbar action 统一从 command context 判断 enable/disabled/reason。
- [x] 为危险动作补 destructive / requiresApproval 元数据。
- [x] 补 smokereadonly、dirty、folder、asset、markdown、多选下的菜单与快捷键状态一致。
本轮最小实现切片:
- [x] Sidex 对照:借鉴 `ContextKeyService` + `CommandsRegistry` + `KeybindingsRegistry` 的“同一 command id / 同一 context / 同一 when”模型;MNote 不照搬 VSCode DI、Extension Host、完整 menu contribution 或复杂 when 语法。
- [x] `filetree-context-menu-runtime.js` 增加 `buildFileTreeCommandContext(row, openEditorsSnapshot, bufferState, deps)`,统一输出 `workspace.sourceKind``workspace.readonly``tree.focusKind``tree.selectionCount``tree.selectionResourceKind``tree.targetResourceKind``tree.targetIsFolder``tree.targetIsAsset``editor.dirty``editor.dirtyState``editor.hasSelection``ai.canWrite`
- [x] `core-protocol::CommandContext` 对齐新增 target / dirtyState keys,并补单测锁住 Rust when evaluator 语义。
- [x] `sidebar-filetree-command-runtime.js` 菜单打开时从 open editors snapshot / bufferState / row selection 生成统一 command context,并把 context、`when``data-disabled-reason``data-destructive``data-requires-approval` 暴露到菜单 DOM。
- [x] `filetree-keyboard-runtime.js``sidebar-tree-runtime.js` inline fallback 的 F2 / Delete / Ctrl+V 使用同一 `buildSidebarFileTreeContext` + whendirty 目标和 readonly workspace 下不执行写动作。
- [x] `task495-filetree-copy-id-absolute-path-smoke.js` 增加 command context 定点断言:markdown dirty target 禁用 rename/delete 类写操作,readonly target 禁用写操作,同时保持 local_folder copy-id 返回绝对路径。
本轮验证证据:
```bash
node --check rust/crates/mnote-web/browser/filetree-context-menu-runtime.js
node --check rust/crates/mnote-web/browser/sidebar-filetree-command-runtime.js
node --check rust/crates/mnote-web/browser/filetree-keyboard-runtime.js
node --check rust/crates/mnote-web/browser/sidebar-tree-runtime.js
node --check scripts/task495-filetree-copy-id-absolute-path-smoke.js
rustfmt --edition 2021 --check rust/crates/core-protocol/src/command.rs rust/crates/mnote-web/src/ssr/pages/layout.rs
git diff --check -- rust/crates/mnote-web/browser/filetree-context-menu-runtime.js rust/crates/mnote-web/browser/sidebar-filetree-command-runtime.js rust/crates/mnote-web/browser/filetree-keyboard-runtime.js rust/crates/mnote-web/browser/sidebar-tree-runtime.js rust/crates/core-protocol/src/command.rs rust/crates/mnote-web/src/ssr/pages/layout.rs scripts/task495-filetree-copy-id-absolute-path-smoke.js
cargo test --manifest-path rust/Cargo.toml -p core-protocol command_context -- --test-threads=1
cargo test --manifest-path rust/Cargo.toml -p mnote-web sidebar_filetree_command_context_helper_functions_exist -- --test-threads=1
cargo test --manifest-path rust/Cargo.toml -p mnote-web filetree_context_menu_runtime_contains_command_context_helpers -- --test-threads=1
cargo test --manifest-path rust/Cargo.toml -p mnote-web sidebar_filetree_context_menu_items_have_when_for_readonly -- --test-threads=1
cargo test --manifest-path rust/Cargo.toml -p mnote-web sidebar_filetree_context_evaluator_uses_readonly_and_selection_count_for_delete_key -- --test-threads=1
node scripts/task490-runtime-surfaces-smoke.js
node scripts/task495-filetree-copy-id-absolute-path-smoke.js
```
边界记录:
- 本批只统一 enablement / reason / metadata / keyboard gating,不改变最终执行面;写动作仍落到现有 Rust `tree.*` / `tree.resource.*` command 路径。
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web sidebar_filetree -- --test-threads=1` 宽过滤额外暴露两个历史/相邻断言失败:`sidebar_filetree_blocks_readonly_paste_and_drop_with_action_status` 仍期待旧 `ensureFileTreeWritableTarget('paste'` 字符串,`sidebar_filetree_runtime_does_not_keep_retired_table_engine_branches` 命中 legacy table 分支;两者未进入本批定点验收,后续如要清理应单独归入 tree/filetree runtime 测试债。
- `node scripts/task476-filetree-editor-context-menu-download-smoke.js` 在菜单验证前等待 `report-a.pdf` asset row 超时;`node scripts/task473-local-folder-trash-restore-no-refresh-focus-gap-smoke.js` 在 restore 后等待 `docs/report.txt` row 超时。两者失败点早于 Batch 6 command context enablement,记录为 FileTree projection / restore 可见性后续风险,不作为本批 command context 验收入口。
建议验证:
```bash
node scripts/task490-runtime-surfaces-smoke.js
node scripts/task476-filetree-editor-context-menu-download-smoke.js
node scripts/task473-local-folder-trash-restore-no-refresh-focus-gap-smoke.js
```
### Batch 7`tree.*` / `tree.resource.*` compat 瘦身
状态:done
目标:
- `tree.*` / `tree.resource.*` 成为正式命令面。
- `documents.*` 只保留为显式 compat alias,不继续承载长期语义。
Sidex 对照:
- Sidex Explorer file actions 统一进入 file operation / bulk edit。
- MNote 的统一入口应是 Rust kernel tree/resource command,不是直接文件 bulk edit。
待办:
- [x] 盘点 `routes/tree.rs``documents.*` legacy mapping。
- [x] 标注每个 compat mapping 的 source kind、保留原因和退役条件。
- [x] local-first 路径禁止静默走 legacy cloud command。
- [x] resource trash / restore / purge 统一走 `tree.resource.*`
- [x] 补 smokecreate/rename/move/delete/restore/purge 在 local_folder 下不触发 legacy owner。
本轮最小实现切片:
- [x] Sidex 对照:借鉴 file action 明确区分 `moveFileToTrash` / `deleteFile` 的语义分叉和回收站 fallbackMNote 保持 `archive/restore/purge` 三段语义,不照搬 `ResourceFileEdit` 或 file system-only undo 模型。
- [x] `routes/tree.rs``/api/tree/commands` 中对缺失 `sourceKind``rootUri``file://` 的请求推断为 `local_folder`,避免 local-first 请求静默走 legacy cloud executor。
- [x] `routes/tree.rs` 增加 `TREE_DOCUMENT_COMPAT_ALIAS_CATALOG`,把 `documents.*` / `tree.*` 兼容边界、保留原因与退役条件显式化。
- [x] local_folder 资源 archive / restore / purge smoke 断言 canonicalCommand 分别为 `tree.resource.archive` / `tree.resource.restore` / `tree.resource.purge`
- [x] 本轮不改 `bridge-runtime` 旧 compat executor 的退役状态;`documents.*` 相关 legacy 兼容桥测试属于已退役 cloud 验证面,失败原因是旧 Convex compat bridge 已退役,不作为 local-first Batch 7 主验收入口。
本轮验证证据:
```bash
cargo test --manifest-path rust/Cargo.toml -p mnote-web tree_command_local_folder_create_rename_copy_trash_restore_and_purge_use_same_endpoint -- --test-threads=1
cargo test --manifest-path rust/Cargo.toml -p mnote-web tree_command_local_folder_asset_trash_restore_and_purge_use_trash_index -- --test-threads=1
cargo test --manifest-path rust/Cargo.toml -p mnote-web tree_documents_compat_alias_catalog_marks_cloud_retirement_boundary -- --test-threads=1
cargo test --manifest-path rust/Cargo.toml -p mnote-web root_entry_local_folder -- --test-threads=1
```
边界记录:
- `cargo test --manifest-path rust/Cargo.toml -p bridge-runtime documents_lifecycle_aliases_are_marked_as_deprecated_tree_protocol_aliases -- --test-threads=1``tree_lifecycle_command_aliases_keep_tree_command_names` 当前失败,错误为“旧 Convex 兼容桥已退役;请改用 local-first Rust/SQLite control-plane 路径”。这说明旧 bridge-runtime 兼容桥已不再是 Batch 7 的主线验收目标,不应回拉为当前实现依赖。
- local-first 请求的 `sourceKind` 推断只在 `rootUri``file://` 时触发,不会把非本地请求静默改成本地。
建议验证:
```bash
cargo test --manifest-path rust/Cargo.toml -p mnote-web --lib routes::tree -- --test-threads=1
node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js
node scripts/task471-local-folder-bulk-resource-trash-smoke.js
```
### Batch 8AI target / changed_files / diff 审计
状态:done
目标:
- local-first AI 默认走授权文件引用 + agent 原生 patch/diff。
- MNote 负责 target resolver、allowed roots/files、dirty conflict、changed_files/diff 审计和前台同步。
Sidex 对照:
- VSCode agent/workspace 模式的价值是目标、权限、working copy、diff 和审计统一。
- MNote 不继续扩普通 Markdown 的专用 `mnote.doc.markdown_edit` 主路径。
待办:
- [x] target chip / picker 使用 open editors snapshot。
- [x] run 开始后冻结 target 摘要,避免执行中 DOM 焦点变化改目标。
- [x] changed_files 回收并展示:路径、版本、diff 摘要、actor。
- [x] dirty buffer 下 agent 写入必须阻断或进入冲突合并。
- [x] 补 smokeclean 写入同步、dirty 写入阻断、跨 workspace target 不串、changed_files 可见。
本轮最小实现切片:
- [x] Sidex 对照:借鉴 VSCode/Sidex 的 working copy / SCM / diff 审计分层,把 target、权限、dirty、diff 和 changed files 放在统一 run/audit 边界;MNote 不照搬 SCM provider、QuickDiff provider 或完整 VSCode agent workspace 模型。
- [x] `sidebar-page-ai-runtime.js` 的 Page AI target resolver 继续从 `OpenEditorsSnapshot` 生成 `mnote.ai_editor_target.v1`;发 run 前新增 workspace/root/source guard,阻断跨 workspace 或陈旧 target。
- [x] `sidebar-page-ai-runtime.js` 新增 `mnote.page_ai_run_target_snapshot.v1`,在 run 发起时冻结 `editorTarget/openEditorsSnapshot/contextScope/rootUri/workspaceId/documentId`,并写入 `/api/hermes/client/runs` payload 与 `pageContext.aiContext.runTargetSnapshot`
- [x] `hermes_client.rs` 对 local source 的 `runTargetSnapshot` 做白名单 sanitization,只保留 target/workspace/open editor 摘要,不把 `pageText/pageXml/contextBlocks` 等正文塞进本地 agent instructions。
- [x] `agent.changed_files` 工具卡显示路径、版本/hash/mtime 摘要、diff 摘要和 actor 信息;前台仍只消费 local agent before/after snapshot,不新增第二套 diff 真相。
- [x] `task453-local-folder-page-ai-changed-files-smoke.js` 覆盖 clean 写入同步、dirty buffer 阻断、跨 workspace target 不发 run、changed_files 卡片可见、runTargetSnapshot payload 可见。
本轮验证证据:
```bash
node --check rust/crates/mnote-web/browser/sidebar-page-ai-runtime.js
node --check scripts/task453-local-folder-page-ai-changed-files-smoke.js
rustfmt --edition 2021 --check rust/crates/mnote-web/src/routes/hermes_client.rs
cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_client_run_body_local_source_uses_file_scope_not_full_page_context -- --test-threads=1
cargo test --manifest-path rust/Cargo.toml -p mnote-web local_agent_audit_snapshot_detects_changed_files -- --test-threads=1
node scripts/task453-local-folder-page-ai-changed-files-smoke.js
git diff --check
codegraph sync .
```
边界记录:
- `node scripts/task-hermes-page-ai-audit-smoke.js``node scripts/task488-acp-multi-session-stability-smoke.js` 当前通过旧 helper 调 `/api/tree/commands` 创建 Convex 文档,失败为 `503 convex_retired`;两者属于 legacy/cloud 验证入口,需要后续改造为 local-first fixture 后再纳入默认 Batch 8 回归。
- Batch 8 不把普通 Markdown 编辑主路径扩回 `mnote.doc.markdown_edit`local-first 默认仍是授权文件引用 + agent 原生 patch/diff + watcher / Page Aggregate 同步。
- `codegraph sync .``codegraph index . --force` 均已成功执行;`codegraph status .` 仍持续显示 `Pending Changes: Added 3 files`,属于本轮前后均存在的 CodeGraph pending 残留,已记录但不清理用户工作区。
建议验证:
```bash
node scripts/task453-local-folder-page-ai-changed-files-smoke.js
node scripts/task-hermes-page-ai-audit-smoke.js
node scripts/task488-acp-multi-session-stability-smoke.js
```
### Batch 9:是否扩展 IDE 面的产品决策
状态:done
目标:
- 明确 MNote 是否需要 Sidex 式 terminal / git / debug / tasks / extension host。
- 若需要,单独写 `design/10-review/reference` 或对应 owner 的 process checklist;不混进当前 P0 服务层收口。
待办:
- [x] 列出 MNote 用户工作流里真实需要 terminal/git/tasks 的场景。
- [x] 区分“知识库 workspace 内嵌工具”与“完整 IDE”。
- [x] 给出引入、不引入、延后引入三种方案。
- [x] 由用户确认产品边界后再进入实现。
本轮产品边界结论:
- [x] Sidex 对照:可借鉴 Sidex terminal 的 PTY/session/event-stream 模型、git porcelain status/diff/log 解析、tasks 的 detect/parse/run/output-stream 模型,以及 extension host 的贡献点/隔离 runtime 概念。
- [x] 不引入完整 VSCode/Sidex workbench、DAP debug、VSCode extension host 或通用 Git 写操作;MNote 继续以 Rust kernel / tree-first projection 作为 workspace 真相。
- [x] Terminal 若后续引入,只作为 workspace-root-scoped、allowed-roots 约束下的受控 terminal session,不默认开放全局 shell。
- [x] Git 若后续引入,优先只做 changed_files / diff / log / status 审计;commit / push / pull / restore / clean / checkout 等 destructive 操作不进 MVP。
- [x] Tasks 若后续引入,只支持白名单知识库任务:导出、索引、链接检查、附件整理、smoke;不做完整 `.vscode/tasks.json` 兼容。
- [x] Debug 能力收口为 agent runtime / projection / session log inspector;不引入 DAP、breakpoints 或 debug adapter manager。
- [x] Extension host 延后,仅保留 importer / exporter / renderer / action provider 的 MNote-native 插件设想,不接 VSCode marketplace 生态。
三方案:
- 引入:只引入 MNote-native `Workspace Command Center`,包含受控 terminal session、只读 git status/diff、白名单 tasks、流式输出、changed_files/diff 审计;不引入 VSCode extension host,不暴露 destructive Git。
- 不引入:继续让 Hermes/Reasonix 用自身文件与 shell 能力,MNote 只做 target / allowed roots / audit / watcher;这是当前 local-first AI 主线成本最低的方案。
- 延后:先把 terminal/git/tasks 作为 Phase B/C 设计冻结项,只落 checklist,不实现;等 changed_files/diff 审计、BufferStore 冲突、workspace readonly 策略稳定后,再开最小 MVP。
当前建议:
- 选择“延后 + 小范围引入只读 Git/diff 审计”的产品边界;Terminal/tasks 等 agent runtime 审计闭环稳定后再单独立项,Debug/DAP/VSCode extension host 不进入 MVP。
## 5. 当前运行状态
历史运行记录:Batch 1 轻量 Workbench Editor Model。
第一步只做只读核验和现有状态盘点,不改代码:
- [x] 读取 `document-resource-tab-runtime.js` 的 open editors snapshot 构建逻辑。
- [x] 读取 `document-editor-adapter-runtime.js` 的 page / secondary pane 打开入口。
- [x] 读取 `resource-open-runtime.js` 与 FileTree open runtime。
- [x] 输出 Batch 1 的具体文件边界与最小 smoke 切片。
只读核验结论(2026-05-27):
- `document-resource-tab-runtime.js` 已有 `resourceTabRegistry``resourceTabMru``buildOpenEditorsSnapshot()``window.__mnoteOpenEditorsSnapshot`
- 当前 snapshot 已包含 `schema/generatedAt/activeObjectIdentity/editors/resourceEditors`,但没有显式 `groups`,调用方需要自己按 `paneRole` 过滤。
- Page entry 已按 `primary/secondary` 生成;secondary entry 仅在有 documentId 或 pane visible 时进入 snapshot。
- Resource entry 已有 `objectIdentity/title/kind/badgeKind/active/dirtyGuard/assetId/path`,但缺少 `paneRole/documentId/workspaceId/lastActiveAt/preview/pinned` 等轻量 editor group 字段。
- `document-editor-adapter-runtime.js` 的公开入口集中在 `window.__mnoteDocumentPaneRuntime`,包括 `openPrimaryDocument``openSecondaryDocument``openResourceInActiveTab``openResourceAsSideTarget`
- `sidebar-filetree-open-runtime.js` 仍在多处手拼 `resource:file:<rootUri>:<relativePath>``resource:mindmap:<documentId>:<assetId>``resource:onlyoffice:<documentId>:<assetId>` 等 identity;这应进入 Batch 2,而不是阻塞 Batch 1。
Batch 1 最小文件边界:
- 修改:`rust/crates/mnote-web/browser/document-resource-tab-runtime.js`
- 可选测试:复用或扩展 `scripts/task496-editor-open-parallel-runtime-aggregate-smoke.js`
- 暂不修改:`document-editor-adapter-runtime.js``sidebar-filetree-open-runtime.js``resource-open-runtime.js`
Batch 1 最小实现切片:
- [x] `buildOpenEditorsSnapshot()` 增加 `groups` 字段,按 `primary/secondary` 输出每个 pane 的 active object 与 editors。
- [x] `openEditorsSnapshotEntry()``paneRole``documentId``workspaceId``lastActiveAt``preview:false``pinned:false`
- [x] Page entry 补 `preview:false``pinned:true``lastActiveAt` 占位,保持页面 tab 是固定 tab。
- [x] 更新 smoke 断言:`window.__mnoteOpenEditorsSnapshot.groups.primary` 存在,secondary 打开后 `groups.secondary` 存在。
## 6. 最小成功标准
本 checklist 完成到 P0 时,MNote 至少应满足:
- 打开页面 / 资源 / secondary pane 的状态有统一 snapshot,可恢复、可被 AI target resolver 消费。
- local-first Markdown dirty/stale/conflict 只由 BufferStore / Page Aggregate 主链表达。
- FileTree row identity、resource tab identity、AI target identity 不再多处临时拼接。
- tree live / local-folder watcher 不再常规整树替换,view state 不丢。
- 菜单、快捷键、dangerous action 的 enabled/disabled/reason 有统一 context。
- local-first tree/resource command 不静默退回 legacy `documents.*` 主链。
## 7. 验证总入口
按改动范围选择运行:
```bash
node scripts/task114-rust-web-gateway-entry-smoke.js
node scripts/task159-auth-entry-smoke.js
node scripts/task164-desktop-hot-local-folder-main-entry-smoke.js
node scripts/task166-local-first-managed-workspace-no-convex-smoke.js
node scripts/task167-local-markdown-title-body-options-no-convex-smoke.js
node scripts/task490-runtime-surfaces-smoke.js
cargo test --manifest-path rust/Cargo.toml -p mnote-web --lib -- --test-threads=1
git diff --check
codegraph sync .
codegraph status .
```