# 17 Sidex / MNote Workbench Gap Execution Checklist v1 > 状态:done > > 创建时间:2026-05-27 > > Owner:10-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 resource(Office/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 2:ObjectWorkspacePath / 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] 补 smoke:local_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 3:BufferStore / 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/stale;dirty 时必须显式确认或阻断。 - [x] 补 smoke:clean 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 state;stat 非 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 4:Page 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 5:Tree 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 6:Command 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] 补 smoke:readonly、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` + when;dirty 目标和 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] 补 smoke:create/rename/move/delete/restore/purge 在 local_folder 下不触发 legacy owner。 本轮最小实现切片: - [x] Sidex 对照:借鉴 file action 明确区分 `moveFileToTrash` / `deleteFile` 的语义分叉和回收站 fallback;MNote 保持 `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 8:AI 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] 补 smoke:clean 写入同步、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::`、`resource:mindmap::`、`resource:onlyoffice::` 等 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 . ```