47 KiB
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
- 对照 Sidex Explorer / EditorGroup / command / working copy 模型。
- 对照 MNote Rust Web / browser runtime 当前能力。
- 对照现有 design / smoke / process backlog。
- 确认本轮不做代码清理、不改用户已有脏改动。
- 输出本执行 checklist,并把后续工作拆成 P0 / P1 / P2。
验收证据:
reference-code/sidex-main/src/vs/workbench/contrib/files/common/explorerModel.tsreference-code/sidex-main/src/vs/workbench/contrib/files/browser/explorerService.tsreference-code/sidex-main/src/vs/workbench/common/editor/editorGroupModel.tsrust/crates/core-protocol/src/kernel.rsrust/crates/mnote-web/src/document_buffer_store.rsrust/crates/mnote-web/browser/document-resource-tab-runtime.jsrust/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 的轻量模型。
待办:
- 盘点当前 open editor 数据源:page tab、resource tab registry、secondary pane、URL query、
window.__mnoteOpenEditorsSnapshot。 - 写一个最小合同文档或 Rust/JS 类型注释,明确字段:
objectIdentityworkspacePathpaneRoleeditorKindactivedirtyStatepreviewpinnedlastActiveAt
- 为
document-resource-tab-runtime.js的 open editors snapshot 增加 primary/secondary 分组语义。 - 补 smoke:打开页面、打开 mindmap/office/code/text 资源、切换 tab、刷新后至少能恢复 active page/resource 的可见状态。
- 补 AI target resolver 输入:last focused editor / resource tab 应来自 open editors snapshot,不再从 DOM 临时猜。
本轮最小实现切片:
OpenEditorsSnapshot保持mnote.open_editors_snapshot.v1,新增groups.primary/groups.secondary。- page editor entry 与 resource editor entry 均输出
paneRole、editorKind、preview、pinned、lastActiveAt。 - resource tab entry 补
documentId/workspaceId,为后续ObjectWorkspacePath消费统一预留字段。 - page / resource editor entry 补
workspacePath,合同以mnote.workspace_path.v1表达 workspace/source/root/relativePath/documentId/objectIdentity。 - passive resource(Office/PDF/image 等无 session tab)激活后也写入
lastActiveAt,避免 MRU / AI target resolver 后续只能看到 0。 task457-main-editor-resource-tab-smoke覆盖 primary markdown resource active、primary pinned page、secondary page editor。task457-main-editor-resource-tab-smoke覆盖 Office passive resource active snapshot 的documentId/workspaceId/editorKind/lastActiveAt。task457-main-editor-resource-tab-smoke覆盖 canonicalresourceTabURL 刷新后恢复 active markdown resource tab,并验证workspacePath.relativePath。sidebar-page-ai-runtime.js从OpenEditorsSnapshot生成mnote.ai_editor_target.v1,写入pageContext.aiContext.activeEditorTarget、openEditorsSnapshot与/api/hermes/client/runs的editorTarget。hermes_client.rs在 local source instructions 中保留editorTarget,避免 agent 只靠 URL / DOM 猜当前编辑目标。- 同步
task457中已过期的 Office 默认 iframe 断言:默认主编辑区是/office-preview轻量预览,显式new-window仍走/onlyoffice?mode=edit。
本轮验证证据:
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
建议验证:
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。
待办:
- 盘点 browser runtime 中手拼 identity 的位置:
filetree-runtime.jssidebar-filetree-command-runtime.jsdocument-resource-tab-runtime.jsresource-open-runtime.jssidebar-filetree-open-runtime.js
- 为 FileTree row 输出 / 读取建立统一 helper:
readWorkspacePathFromRow(row)。 copy-id和 resource tab key 优先使用 workspacePath / objectIdentity。- local_folder 下绝对路径还原继续从
rootUri + relativePath得出,但只作为展示/复制结果,不作为内部 identity。 - 补 smoke:local_folder Markdown、folder、asset、mindmap、office row 的 identity 一致且复制路径不暴露
local:*projection id。
本轮最小实现切片:
filetree-runtime.js新增readWorkspacePathFromRow(row, deps),统一读取workspaceId/sourceKind/rootUri/relativePath/objectIdentity/rowId/documentId/assetId/title。fileTreeAssetDownloadDetail与 filetree context menu detail 开始携带workspacePath,为后续 copy/open/command context 统一消费铺底。task495-filetree-copy-id-absolute-path-smoke增加readWorkspacePathFromRow定点断言,同时保持 local_folder 复制 ID 返回绝对路径。sidebar-tree-runtime.js的 filetree open / asset open event detail 携带workspacePath,resource open 不再只能从 DOM 临时拼身份。sidebar-filetree-open-runtime.js用workspacePath.objectIdentity/rootUri/relativePath/assetId生成 resource tab object identity,覆盖 local file、mindmap、office、pdf 和侧栏打开。document-resource-tab-runtime.js在 resource tab entry 中保留workspacePath,snapshot 输出优先沿同一 workspacePath 合同。local_folder_source.rs的真实 local_folder projection 为 mindmap/office/asset row 写入workspacePath.objectIdentity.assetId,避免资源行 identity 碰撞。filetree-runtime.js收窄isLocalFolder判定:只信sourceKind=local_folder,或 sourceKind 缺失时的明确 local row/doc 前缀,避免误伤 cloud/compat id。- 恢复 legacy
luckysheet/.luckysheettable 分类,避免 table row 被误归为普通 file asset。
本轮验证证据:
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 切片扩大修复面。
建议验证:
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。
待办:
- 盘点
documentSessionRegistry、localStorage draft、conflictDetectionKey、expectedFileVersion当前写读链。 - 让 watcher 标记外部修改后,前端 session 从 BufferStore / aggregate 读取状态,而不是只靠本地 session flag。
- 资源 tab close guard 读取统一 dirty state。
- AI 写入前检查目标 buffer dirty/stale;dirty 时必须显式确认或阻断。
- 补 smoke:clean buffer 外部写入可同步;dirty buffer 外部写入弹冲突;删除当前文件后 tab 不静默保留旧正文。
本轮最小实现切片:
- Sidex 对照:借鉴
WorkingCopyService的统一 dirty/event 仲裁;不引入 VSCode 完整 backup/hot-exit/URI provider。 documents.rs增加POST /api/documents/buffer-state/dirty,浏览器编辑变 dirty 时写入 RustBufferStore::mark_dirty,保存成功仍由 Rust 写链mark_saved清理。document-session-runtime.js增加fetchSessionBufferState()/applyBufferStateToSession(),外部刷新前先读取/api/documents/buffer-state,对Dirty/Stale/Deleted进入冲突/删除保护,不再只靠本地 session flag。queueSessionSave()在 autosave 排队时同步标记 BufferStore dirty,形成 watcher / AI / save 共享的打开态。- clean 外部写入、dirty 外部写入、保存失败保留编辑器内容、AI dirty 写入冲突路径均已通过 smoke。
sidebar-page-ai-runtime.js在发起 Hermes run 前读取 OpenEditorsSnapshot 与/api/documents/buffer-state,当目标 buffer 为Dirty/Stale/Deleted/ExternalModified时阻断 AI 写入,不再让 dirty buffer 进入 agent 写盘后冲突。document-resource-tab-runtime.js的 page entry 从当前documentSessionRegistry输出 dirty state,避免页面刚输入但/buffer-state/dirty仍未完成时 Page AI 被放行。document-resource-tab-runtime.js的 resource tab close 在关闭前 await 读取 BufferStore dirty state,并把Dirty/Stale/Deleted/ExternalModified纳入 close veto;本地 resource session 的小写dirty同时被 Page AI 阻断逻辑规范化识别。local-upload-runtime.js上传保存成功后广播mnote:local-upload-editor-save-completed,document-session-runtime.js同步 session fileVersion / conflict key,避免编辑器自己写盘被 watcher 误判为外部冲突。sidebar-attachment-open-runtime.js为 local_folder 附件链接增加存在性刷新、missing 状态缓存、stat 请求序号和幂等 DOM 写入;删除真实附件后 Markdown 链接保留,点击进入 resource tab missing/error state;stat 非 OK 不再伪装成 missing,而是写入诊断属性。
本轮验证证据:
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):
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。
待办:
- 盘点
document-tiptap-conversion-runtime.js中 legacy conversion 的 runtime 调用面。 - 区分 local-first
.mdprojection、cloud compat projection、fixture/test helper。 - 为 Page Aggregate 输出增加或验证 source 标记:避免 UI 偏好 / 页面事实 / compat fallback 混淆。
- 逐步把可控 runtime fallback 改成显式 degraded / compat owner。
- 补 smoke:刷新后 body/blockDocument/options/source 一致,不能静默读旧 legacy 内容。
本轮最小实现切片:
- Sidex 对照:借鉴
EditorInput显式 input/resource/capabilities 边界;MNote 不照搬 EditorResolver,只把页面正文 source 分类写入 runtime session。 document-tiptap-conversion-runtime.js增加pageBodyTiptapDocumentSource(),把local_markdown.content、page_aggregate.block_document、compat.legacy_content、degraded.fallback_text、empty显式分类,避免无标记地从 legacybody.content猜正文来源。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。task167-local-markdown-title-body-options-no-convex-smoke.js断言 local-first Page Aggregate 的projectionSource=local_markdown.content与blockDocument存在,保存后不退回 legacydocuments.content。
本轮验证证据:
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。
建议验证:
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 替换。
待办:
- 盘点
/api/local-folder/events?treeLive=true、tree:delta、watch_batch、projection refresh、polling fallback 的调用关系。 - 定义统一 local-folder tree event payload 到 filetree parent refresh 的映射。
- 整树替换只保留在 root scope 切换或 resync 场景。
- view state 保存只存 expanded/selected/focused 等用户状态,不存 rows/cache 派生数据。
- 补大目录真实用户路径 smoke:从首页进入本地 workspace,点击星标/Explorer 逐级打开目标 Markdown,记录 longtask 和 editor ready。
本轮最小实现切片:
- Sidex 对照:借鉴
ExplorerModel/ExplorerService/AsyncDataTree的节点身份、局部 children resolve、in-flight 去重和 view state 思路;MNote 不照搬 VSCode 完整树控件,仍以 Rust projection / watcher revision 为事实源。 sidebar-tree-live-apply-runtime.js在watch_batch的fallbackResync/requiresResync下显式走refreshLocalFolderSidebarSnapshot(),非 resync 情况只刷新受影响 parent。watch_batch缺少affectedParents但包含changedPaths时,从 changed path 派生 parent scope,并写入data-mnote-local-folder-watch-batch-derived-parents作为诊断标记。- 修复 scoped navigation shell 缺 primary editor host 导致
openPrimaryDocument()抛pane_root_missing_primary并回退整页 reload 的问题:HomePagenavigation 分支保留导航 placeholder,同时渲染隐藏 primaryDocumentPane供 pane runtime 原地替换。 document-editor-adapter-runtime.js在 primary 文档成功激活后隐藏 navigation placeholder,保持点击 scoped FileTree Markdown 后不重建侧栏、不丢 JS marker。
本轮验证证据:
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-statesmoke 证明持久化内容仍是 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__。
建议验证:
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。
待办:
- 对齐 Rust
CommandContext和 browserwhenparser 的字段名。 - 建立
buildFileTreeCommandContext(row, openEditorsSnapshot, bufferState)。 - 右键菜单、键盘动作、toolbar action 统一从 command context 判断 enable/disabled/reason。
- 为危险动作补 destructive / requiresApproval 元数据。
- 补 smoke:readonly、dirty、folder、asset、markdown、多选下的菜单与快捷键状态一致。
本轮最小实现切片:
- Sidex 对照:借鉴
ContextKeyService+CommandsRegistry+KeybindingsRegistry的“同一 command id / 同一 context / 同一 when”模型;MNote 不照搬 VSCode DI、Extension Host、完整 menu contribution 或复杂 when 语法。 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。core-protocol::CommandContext对齐新增 target / dirtyState keys,并补单测锁住 Rust when evaluator 语义。sidebar-filetree-command-runtime.js菜单打开时从 open editors snapshot / bufferState / row selection 生成统一 command context,并把 context、when、data-disabled-reason、data-destructive、data-requires-approval暴露到菜单 DOM。filetree-keyboard-runtime.js与sidebar-tree-runtime.jsinline fallback 的 F2 / Delete / Ctrl+V 使用同一buildSidebarFileTreeContext+ when;dirty 目标和 readonly workspace 下不执行写动作。task495-filetree-copy-id-absolute-path-smoke.js增加 command context 定点断言:markdown dirty target 禁用 rename/delete 类写操作,readonly target 禁用写操作,同时保持 local_folder copy-id 返回绝对路径。
本轮验证证据:
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.pdfasset row 超时;node scripts/task473-local-folder-trash-restore-no-refresh-focus-gap-smoke.js在 restore 后等待docs/report.txtrow 超时。两者失败点早于 Batch 6 command context enablement,记录为 FileTree projection / restore 可见性后续风险,不作为本批 command context 验收入口。
建议验证:
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。
待办:
- 盘点
routes/tree.rs中documents.*legacy mapping。 - 标注每个 compat mapping 的 source kind、保留原因和退役条件。
- local-first 路径禁止静默走 legacy cloud command。
- resource trash / restore / purge 统一走
tree.resource.*。 - 补 smoke:create/rename/move/delete/restore/purge 在 local_folder 下不触发 legacy owner。
本轮最小实现切片:
- Sidex 对照:借鉴 file action 明确区分
moveFileToTrash/deleteFile的语义分叉和回收站 fallback;MNote 保持archive/restore/purge三段语义,不照搬ResourceFileEdit或 file system-only undo 模型。 routes/tree.rs在/api/tree/commands中对缺失sourceKind但rootUri为file://的请求推断为local_folder,避免 local-first 请求静默走 legacy cloud executor。routes/tree.rs增加TREE_DOCUMENT_COMPAT_ALIAS_CATALOG,把documents.*/tree.*兼容边界、保留原因与退役条件显式化。- local_folder 资源 archive / restore / purge smoke 断言 canonicalCommand 分别为
tree.resource.archive/tree.resource.restore/tree.resource.purge。 - 本轮不改
bridge-runtime旧 compat executor 的退役状态;documents.*相关 legacy 兼容桥测试属于已退役 cloud 验证面,失败原因是旧 Convex compat bridge 已退役,不作为 local-first Batch 7 主验收入口。
本轮验证证据:
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://时触发,不会把非本地请求静默改成本地。
建议验证:
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主路径。
待办:
- target chip / picker 使用 open editors snapshot。
- run 开始后冻结 target 摘要,避免执行中 DOM 焦点变化改目标。
- changed_files 回收并展示:路径、版本、diff 摘要、actor。
- dirty buffer 下 agent 写入必须阻断或进入冲突合并。
- 补 smoke:clean 写入同步、dirty 写入阻断、跨 workspace target 不串、changed_files 可见。
本轮最小实现切片:
- Sidex 对照:借鉴 VSCode/Sidex 的 working copy / SCM / diff 审计分层,把 target、权限、dirty、diff 和 changed files 放在统一 run/audit 边界;MNote 不照搬 SCM provider、QuickDiff provider 或完整 VSCode agent workspace 模型。
sidebar-page-ai-runtime.js的 Page AI target resolver 继续从OpenEditorsSnapshot生成mnote.ai_editor_target.v1;发 run 前新增 workspace/root/source guard,阻断跨 workspace 或陈旧 target。sidebar-page-ai-runtime.js新增mnote.page_ai_run_target_snapshot.v1,在 run 发起时冻结editorTarget/openEditorsSnapshot/contextScope/rootUri/workspaceId/documentId,并写入/api/hermes/client/runspayload 与pageContext.aiContext.runTargetSnapshot。hermes_client.rs对 local source 的runTargetSnapshot做白名单 sanitization,只保留 target/workspace/open editor 摘要,不把pageText/pageXml/contextBlocks等正文塞进本地 agent instructions。agent.changed_files工具卡显示路径、版本/hash/mtime 摘要、diff 摘要和 actor 信息;前台仍只消费 local agent before/after snapshot,不新增第二套 diff 真相。task453-local-folder-page-ai-changed-files-smoke.js覆盖 clean 写入同步、dirty buffer 阻断、跨 workspace target 不发 run、changed_files 卡片可见、runTargetSnapshot payload 可见。
本轮验证证据:
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 残留,已记录但不清理用户工作区。
建议验证:
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 服务层收口。
待办:
- 列出 MNote 用户工作流里真实需要 terminal/git/tasks 的场景。
- 区分“知识库 workspace 内嵌工具”与“完整 IDE”。
- 给出引入、不引入、延后引入三种方案。
- 由用户确认产品边界后再进入实现。
本轮产品边界结论:
- Sidex 对照:可借鉴 Sidex terminal 的 PTY/session/event-stream 模型、git porcelain status/diff/log 解析、tasks 的 detect/parse/run/output-stream 模型,以及 extension host 的贡献点/隔离 runtime 概念。
- 不引入完整 VSCode/Sidex workbench、DAP debug、VSCode extension host 或通用 Git 写操作;MNote 继续以 Rust kernel / tree-first projection 作为 workspace 真相。
- Terminal 若后续引入,只作为 workspace-root-scoped、allowed-roots 约束下的受控 terminal session,不默认开放全局 shell。
- Git 若后续引入,优先只做 changed_files / diff / log / status 审计;commit / push / pull / restore / clean / checkout 等 destructive 操作不进 MVP。
- Tasks 若后续引入,只支持白名单知识库任务:导出、索引、链接检查、附件整理、smoke;不做完整
.vscode/tasks.json兼容。 - Debug 能力收口为 agent runtime / projection / session log inspector;不引入 DAP、breakpoints 或 debug adapter manager。
- 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。
第一步只做只读核验和现有状态盘点,不改代码:
- 读取
document-resource-tab-runtime.js的 open editors snapshot 构建逻辑。 - 读取
document-editor-adapter-runtime.js的 page / secondary pane 打开入口。 - 读取
resource-open-runtime.js与 FileTree open runtime。 - 输出 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 最小实现切片:
buildOpenEditorsSnapshot()增加groups字段,按primary/secondary输出每个 pane 的 active object 与 editors。openEditorsSnapshotEntry()补paneRole、documentId、workspaceId、lastActiveAt、preview:false、pinned:false。- Page entry 补
preview:false、pinned:true、lastActiveAt占位,保持页面 tab 是固定 tab。 - 更新 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. 验证总入口
按改动范围选择运行:
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 .