docs: organize active mnote execution checklists

This commit is contained in:
lix-2026
2026-05-28 22:01:12 +08:00
parent 80d54c4c2b
commit 7354807ee9
7 changed files with 1484 additions and 67 deletions
@@ -0,0 +1,86 @@
# 全局与内容类型页面宽度设置 Checklist v1
## 背景
当前 Markdown 页面已有 `wideLayout` 布尔选项,但 Word / PDF / Excel / PPT / Mindmap 等不同内容类型没有统一的页面宽度偏好入口。用户需要在全局设置中分别调整不同类型的默认宽度,避免像思源那样必须写 CSS 才能把阅读宽度调到合适比例。
## 目标
- 增加“全局默认宽度 + 按内容类型默认宽度”的偏好模型。
- 用户可以分别配置 Markdown、Word、PDF、Excel、PPT、Mindmap 的默认宽度。
- Markdown 文档页、轻量 Office 预览页、PDF 预览页消费该偏好。
- 第一阶段不做单页面 / 单文件覆盖,避免引入每页 SQLite 记录和资源属性扩散。
## 非目标
- 不新增每个页面、每个文件或每个附件的宽度覆盖。
- 不迁移已有 `wideLayout` 历史偏好;它继续作为当前页面选项存在。
- 不替换 OnlyOffice 或现有文件预览方案。
- 不实现自定义 CSS 编辑器。
## 偏好模型
复用 `user_ui_preferences`,不新增表。固定写入 global scope
- `pageWidth.default`
- `pageWidth.markdown`
- `pageWidth.word`
- `pageWidth.pdf`
- `pageWidth.excel`
- `pageWidth.ppt`
- `pageWidth.mindmap`
偏好值统一为对象:
```json
{"mode":"wide","custom":null}
```
支持模式:
- `inherit`:内容类型继承全局默认。
- `readable`:阅读宽度,约 `760px`
- `comfortable`:舒适宽度,约 `980px`
- `wide`:宽版,约 `1180px`
- `full`:接近全屏。
内置默认:
```json
{
"default": {"mode":"comfortable","custom":null},
"markdown": {"mode":"readable","custom":null},
"word": {"mode":"wide","custom":null},
"pdf": {"mode":"wide","custom":null},
"excel": {"mode":"full","custom":null},
"ppt": {"mode":"wide","custom":null},
"mindmap": {"mode":"full","custom":null}
}
```
优先级:
`内容类型默认 > 全局默认 > 系统内置默认`
## 实施 Checklist
- [x] 后端 API`GET /api/ui/preferences/effective` 返回 `pageWidthPreferences`
- [x] 后端 API`PUT /api/ui/preferences` 接受 `pageWidth.*` key,固定写入 global scope。
- [x] 后端测试:写入 `pageWidth.word` / `pageWidth.excel` 后,effective payload 可读回归一化结果。
- [x] 后端测试:`pageWidth.*` 不随 workspace/document 进入 workspace 或 document scope。
- [x] 前端设置:页面设置弹窗的“全局选项”增加页面宽度设置区。
- [x] 前端设置:支持默认、Markdown、Word、PDF、Excel、PPT、Mindmap 的 select。
- [x] Markdown 消费:文档 shell 根据 `pageWidthPreferences.markdown` 设置最大宽度;保留 `wideLayout` 兼容。
- [x] Office 消费:`/office-preview` 根据 fileType 推导 Word / Excel 并设置宽度模式。
- [x] PDF 消费:`/pdf-preview` 使用 PDF 宽度模式。
- [x] Smoke:覆盖设置保存后 API 返回、DOCX / XLSX 预览宽度、Markdown shell 宽度属性。
- [x] 验证:Rust 定点测试、JS 语法检查、浏览器 smoke、相关文件 `git diff --check`
说明:PPT / Mindmap 第一阶段已完成偏好模型、默认值和设置入口;当前真实消费点已接入 Markdown、轻量 Office 预览和 PDF 预览。PPTX 快速预览已接入轻量 Office 预览并消费 `ppt` 宽度偏好;旧 `.ppt` 和需要高保真编辑的演示文件仍走 OnlyOffice。Mindmap 的实际渲染消费需要在对应 viewer 进入统一宽度控制时再接入。
## 验收标准
- 用户不写 CSS 即可分别设置 Markdown / Word / PDF / Excel / PPT / Mindmap 默认宽度。
- 设置存储只增加固定全局 key,不增加单页面或单文件记录。
- 默认体验保持:Markdown 偏阅读宽度,Word/PDF 偏宽版,Excel/Mindmap 偏全宽。
- 移动端不因固定宽度产生横向溢出,宽度通过 `min(100%, var(...))` 或等价约束收敛。
@@ -0,0 +1,210 @@
# 5-27 本地 Markdown Working Copy 冲突合同 v1
## 背景
`bugs/0524.md` 第 2 条暴露的问题不是单个上传入口错误,而是本地文件夹 watcher、正文 session、文件树资源事件和冲突 UI 之间缺少清晰边界:
- 上传附件或向文件树拖入文件后,非 Markdown 文件变化会进入正文外部变更链路。
- 新建页面后,旧的冲突提示可能被同 root 的事件流带到新页面,刷新后消失。
- 真实冲突仍然需要保留:当前 Markdown 有未保存编辑,同时磁盘上的同一个 Markdown 文件被外部修改时,必须进入冲突处理。
## Sidex 对照
Sidex/VSCode 的核心模型是 `StoredFileWorkingCopy`
- 每个 working copy 绑定一个具体 `resource`
- 保存时以 `lastResolvedFileStat.etag/mtime` 做 dirty write prevention。
- 只有同一个 resource 的写入出现 `FILE_MODIFIED_SINCE` 时,才进入 `inConflictMode`
- 文件系统 watcher 的目录级事件不会直接把同目录其它文件变化升级成当前 working copy 冲突。
- 自身文件操作走 `onDidRunOperation` 一类的写入通道,外部变化走 `onDidFilesChange` watcher 通道。
- dirty working copy 收到 watcher update 时不自动 reload;真正冲突主要在保存时通过 mtime/etag 前置条件产生。
- 保存成功只在保存期间 working copy 版本没有再次变化时清 dirty,否则保留 dirty 并继续下一轮保存。
MNote 不需要照搬 VSCode 的实现,但应采用同一条合同:正文冲突只属于当前 Markdown 文件,不属于同 root 下任意附件、文件树资源或元数据文件。
## 当前根因
当前 `mnote-web` 的 document event channel 以 `rootUri` 共享:
- 前端订阅 `/api/local-folder/events?rootUri=...`,没有传 `documentId`
- 后端 `build_document_events_stream` 在没有 `documentId` 时不会按 Markdown 相对路径过滤。
- watcher 对非 Markdown 资源发出的 payload 中 `documentId` 为空。
- 前端收到空 `documentId` 后,仍会对同 root 下所有 document session 设置 `externalChangePending` 并调度正文刷新。
这会把附件上传、文件树拖入、资源创建等 root 级变化误投递到正文 session,形成文件冲突误报和跨页面状态污染。
另一个独立根因是冲突 UI 的 DOM 生命周期:
- `renderSessionConflictSurface` 会把冲突面板直接插入 `.document-pane`
- 页面切换时 `unmountEditorViewBinding` 只卸载 editor runtime 和事件监听,没有清理旧 session 的冲突面板。
- 因此旧页面已出现冲突时,新建/切换到新页面可能短暂看到旧冲突面板;刷新后整页 DOM 重建,问题消失。
2026-05-28 复发根因:
- 上传附件后,`local-upload-runtime` 会先写附件文件,再把附件链接插入编辑器并保存 Markdown。
- 保存 Markdown 成功后,后端返回新的 `fileVersion/conflictDetectionKey`,但后续用户输入的 autosave 仍可能使用旧 `expectedFileVersion`
- Local watcher 会看到 MNote 自己 `fs::write()` 产生的 Markdown 文件事件,并把它标成 `external-editor`
- 如果用户在 watcher 回声到达前后继续输入,`Dirty/saveTimer/recent input` 与 watcher 事件相遇,被误判成外部冲突。
- 进入冲突态后,第二个附件上传仍可能写入附件文件和插入链接,但正文保存链停止可靠提交,导致两个附件的编辑器链接、Markdown 正文和文件树资源状态分裂。
## 合同
1. Markdown 正文 session 只订阅自己的 Markdown 文件事件。
2. 非 Markdown 资源事件只用于文件树/资源投影刷新,不触发正文 `externalChangePending`
3. 真实正文冲突只在“同一个 Markdown documentId + 当前 session dirty/saving/saveTimer/recent input”时出现。
4. 同 root 下不同 Markdown 页面必须有独立 document event channel。
5. 文件树 live stream 继续承担 root 级变化刷新,不依赖正文冲突链路。
6. 保存冲突以 CAS 为准:`expectedFileVersion` 与当前磁盘 `fileVersion` 不一致时返回 409。
7. watcher 事件不能单独制造正文冲突;它只能触发 clean reload、标记 buffer 外部变化,或在不同版本且 dirty 时把 buffer 推到 Stale。
8. MNote 自身保存成功后的同版本 watcher 回声必须被识别为 self-write echo,不得把 Clean 变 ExternalModified,也不得把 Dirty 变 Stale。
9. 保存成功必须原子更新 Rust `BufferStore`、浏览器 `DocumentSession.conflictDetectionKey/fileVersion`、页面 aggregate script 中的版本字段。
10. dirty working copy 不自动 reload;如果磁盘版本未变化,当前编辑器内容与磁盘快照不同也不是冲突。
## 已执行修复切片
- 前端 document event channel key 从 `rootUri` 收窄为 `rootUri#documentId`
- 前端订阅本地文件夹 document events 时携带 `documentId`
- 前端收到无 `documentId` 的 change payload 时直接忽略,不再设置正文 `externalChangePending`
- editor view unmount 时清理旧 session 的冲突面板,避免手动插入的 DOM 跨页面残留。
- 后端补充 document event filter 测试,确保资源文件路径不会匹配 Markdown 正文路径。
## 后续系统性收口
- [x]`DocumentBufferStore` 作为正文冲突状态底座之一,前端 session 不再仅凭 watcher/dirty 差异制造冲突。
- [x] 保存成功后的同版本 watcher 回声由 Rust `BufferStore` 忽略。
- [x] 上传附件后继续输入、再上传第二个附件的 browser smoke 覆盖。
- [x] 引入显式 `writeIntentId/saveOperationId`,让 watcher 可精确关联 MNote 自身写入,而不只依赖 fileVersion。
- [x] 为资源 tab 增加独立 resource watch 合同:按 `resourcePath` 监听资源文件,而不是复用 Markdown document event。
- [x] 为新建页面增加创建者写入抑制或 bootstrap generation,避免未来新建空文件 watcher 与首次打开时序竞争。
- [x]`task451` 的真实冲突 smoke 与上传/新建页面无冲突 smoke 合并成一组 local Markdown conflict regression。
- [x] 补 AI 写入、多浏览器 tab、外部删除/移动的冲突矩阵。
## 执行 Checklist
### Batch 1 - 保存意图与自写回声审计
- [x] `PageBodyWriteRequest` / `/api/documents/save` 接受并回传 `writeIntentId``saveOperationId`
- [x] 浏览器 `DocumentSession` 普通保存与 `local-upload-runtime` 附件保存都生成并发送 `writeIntentId/saveOperationId`
- [x] `BufferStore.mark_saved` 记录最后一次 `writeIntentId/saveOperationId`
- [x] watcher 处理 Markdown 文件事件时在 payload 中带出 `observedFileVersion``bufferFileVersion``lastWriteIntentId``lastSaveOperationId``selfWriteEcho`
- [x] 同版本 watcher 回声继续保持 Clean/Dirty 不被升级为 ExternalModified/Stale。
- [x] 定点验证:core-protocol 反序列化测试、BufferStore self-write outcome 测试、`task503` 连续上传 smoke。
验证证据:
- `cargo test --manifest-path rust/Cargo.toml -p core-protocol page_body_write_request_uses_file_version_contract -- --nocapture`
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web document_buffer_records_save_operation_and_reports_self_write_echo -- --nocapture`
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web document_buffer_ignores_watcher_echo_for_last_saved_version -- --nocapture`
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web document_buffer_marks_stale_when_dirty_buffer_sees_new_version -- --nocapture`
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web local_markdown_write_contract_returns_page_body_write_command -- --nocapture`
- `cargo check --manifest-path rust/Cargo.toml -p mnote-web`
- `MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3017 node scripts/task503-local-pptx-upload-filetree-open-smoke.js`
- `MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3017 node scripts/task491-local-md-attachment-icon-refresh-smoke.js`
- `node --check rust/crates/mnote-web/browser/document-session-runtime.js && node --check rust/crates/mnote-web/browser/local-upload-runtime.js && node --check scripts/task503-local-pptx-upload-filetree-open-smoke.js`
### Batch 2 - 真实外部编辑冲突恢复
- [x] 写 browser smokedirty 当前 Markdown 后由外部进程修改同一 `.md`,必须显示冲突面板。
- [x] `accept_disk`:接受磁盘版本后编辑器、DocumentSession、BufferStore 与 aggregate 版本一致。
- [x] `keep_editor`:保留当前编辑器内容后下一次保存使用最新磁盘版本 CAS,成功后清冲突。
- [x] `open_diff`:至少能打开稳定 diff/对照视图,不丢当前编辑器内容。
- [x] 保存过程中继续输入:只清理已保存版本,后续输入保持 Dirty。
验证证据:
- `MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3018 node scripts/task504-local-md-external-conflict-recovery-smoke.js`
- `node --check scripts/task504-local-md-external-conflict-recovery-smoke.js`
- `task504``save-while-typing` 步骤断言保存 pending 时继续输入会触发第二轮保存:`saveRequestCountBeforeRace=1``saveRequestCountAfterRace=3`,最终磁盘同时包含第一段和第二段且无冲突面板。
### Batch 3 - Resource Tab 独立 Watch 合同
- [x] 定义 resource watch 合同:`resourcePath/resourceId` 级别监听资源文件,不复用 Markdown document event。
- [x] PDF/Office/image/resource markdown tab 只响应自身 resource 事件。
- [x] resource Markdown 文件更新时刷新资源 tab 或提示冲突,不影响正文 session 冲突态。
- [x] resource 文件删除/移动进入 resource tab 自己的 missing/rekey 状态。
- [x] browser smoke 覆盖 Office/PDF resource tab 更新与删除。
验证证据:
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web resource_event_path_filters_to_its_own_relative_path -- --nocapture`
- `MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3018 node scripts/task505-resource-tab-watch-contract-smoke.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 rust/crates/mnote-web/browser/document-resource-tab-runtime.js && node --check scripts/task505-resource-tab-watch-contract-smoke.js`
- `task505` 覆盖 Markdown resource clean 更新、Markdown resource dirty 冲突、Office passive resource 更新重载与删除 missingPDF 与 Office 共用 iframe passive resource watch 路径,image 走同一 resourcePath watch 并重载 `img.src`
### Batch 4 - 创建/删除/移动 Bootstrap 与生命周期
- [x] 新建页面首写使用 bootstrap generation 或 creator write suppression,避免空文件 watcher 抢跑。
- [x] 外部删除打开 Markdown:不可 rekey 时进入 `Deleted`,保留 buffer 内容。
- [x] 外部移动/重命名打开 Markdown:可 rekey 时保留 buffer 并更新 workspace path/document id。
- [x] 文件树删除/移动与外部删除/移动走同一 resource lifecycle 合同。
验证证据:
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web document_buffer_marks_deleted_after_local_file_operation_archive -- --nocapture`
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web document_buffer_rekeys_after_local_file_operation_rename -- --nocapture`
- `MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3018 node scripts/task506-local-md-delete-move-lifecycle-smoke.js`
- `MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3018 node scripts/task458-local-create-page-no-conflict-smoke.js`
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web document_shell_renders_local_markdown_with_same_sidebar_surfaces -- --nocapture`
- `node --check scripts/task506-local-md-delete-move-lifecycle-smoke.js`
- 说明:内部 filetree rename/move 通过 tree command execution 的 previous/next resource 调 `BufferStore.rekey_local_folder_markdown`;外部原始 `fs.rename` 在 watcher 层没有稳定 previous/next 配对,当前按冲突/删除态保留 buffer,不静默重定向到猜测路径。
### Batch 5 - AI 与多 Tab 并发矩阵
- [x] AI 写入 clean 文档:当前 tab 自动同步,BufferStore 保持 Clean。
- [x] AI 写入 dirty 文档:显示 agent 来源冲突,冲突 envelope 带 actor/source。
- [x] 两个浏览器 tab 并发编辑同一 Markdownclean tab 自动同步,dirty tab 保存 409。
- [x] 附件文件已落盘但 Markdown 引用保存 409:提示重试/保留孤儿/清理策略明确,不静默丢失。
- [x] 合并 `task451` 真实冲突 smoke、上传无冲突 smoke、新建页面无冲突 smoke 为 local Markdown conflict regression 组。
验证证据:
- `MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3018 node scripts/task436-local-markdown-open-document-external-change-smoke.js`
- `MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3018 node scripts/task451-local-markdown-conflict-resolution-ui-smoke.js`
- `MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3018 node scripts/task508-local-md-multitab-conflict-smoke.js`
- `MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3018 node scripts/task509-local-upload-save-409-orphan-smoke.js`
- `MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3018 node scripts/task510-local-markdown-conflict-regression-group.js`
- `task508` 断言 clean tab 自动同步,dirty tab 延迟保存经 A tab 写入后返回 `gateStatus=409`,B tab 保留未保存内容并显示冲突面板。
- `task509` 断言 local-upload save 409 后附件文件保留、编辑器引用保留、`data-mnote-last-upload-orphaned-policy=asset-kept-reference-unsaved-retry-required`;session 已冲突时第二次附件上传在写文件前阻断。
## 分阶段 Checklist
### Phase A - Working Copy 合同冻结
- [x] Page Aggregate body 输出 `fileVersion/conflictDetectionKey`
- [x] DocumentSession 保存时携带 `expectedFileVersion`
- [x] BufferStore 持有 `Clean/Dirty/Stale/ExternalModified/Deleted`
- [x] 文档化 `fileVersion` 生成规则和 `baseContentHash/currentContentHash` 语义。
合同说明:
- `fileVersion/conflictDetectionKey` 是本地 Markdown 的 CAS 令牌,由当前 `documentId + mtime_ms + file_size + content_hash_prefix` 生成;它只用于“这次保存基于哪个磁盘版本”比较,不作为页面 ID、附件 ID 或排序真相。
- `baseContentHash` 表示 buffer 上一次确认的磁盘/保存内容 hash;`currentContentHash` 表示当前 working copy 内容 hash。两者不同是 dirty/stale 的依据,但只有保存 CAS 失败或删除态才进入用户可见冲突。
### Phase B - Self-write 回声消除
- [x] `write_local_markdown_page_body()` 保存成功后调用 `BufferStore.mark_saved()`
- [x] watcher 看到同版本 Markdown 事件时不调用外部冲突转换。
- [x] 保存请求分配 `writeIntentId`watcher payload 回传并落审计。
- [x] `local-upload-runtime` 在 session 已冲突时阻止“看似成功”的第二次正文保存。
### Phase C - 真实冲突闭环
- [x]`expectedFileVersion` 保存返回 409 conflict envelope。
- [x] dirty 当前 Markdown 后外部编辑同一 `.md` 的 browser smoke。
- [x] 接受磁盘版本、保留当前编辑器版本、打开 diff 后的版本恢复 smoke。
- [x] 保存过程中用户继续输入时,只清理已保存版本,不丢后续 dirty。
### Phase D - 附件/文件树
- [x] 编辑器附件上传后继续输入不触发冲突。
- [x] 连续上传同名 pptx 后两个编辑器附件链接可打开。
- [x] 文件树拖拽上传后投影刷新并可打开。
- [x] 正文保存 409 时,附件文件存在但引用未落盘的 UI 提示/重试/孤儿清理策略。
### Phase E - AI / 多 Tab / 外部文件
- [x] AI 写入 clean 文档时自动同步。
- [x] AI 写入 dirty 文档时显示 agent 来源冲突。
- [x] 两个浏览器 tab 并发编辑同一 Markdownclean tab 自动同步,dirty tab 保存 409。
- [x] 外部删除/移动打开文件:可 rekey 时保留 buffer,不可 rekey 时进入 Deleted。
## 验收
- 上传第一个/第二个附件后,主编辑区不显示 `mnote-editor-conflict-panel`
- 新建页面后,旧页面冲突 UI 不带入新页面。
- 新建页面后直接向文件树拖入文件,不显示文件冲突。
- dirty 当前 Markdown 后由外部修改同一个 `.md` 文件,仍显示冲突并保留 accept disk / keep current / diff 流程。
@@ -1,67 +0,0 @@
# 5-27 本地 Markdown Working Copy 冲突合同 v1
## 背景
`bugs/0524.md` 第 2 条暴露的问题不是单个上传入口错误,而是本地文件夹 watcher、正文 session、文件树资源事件和冲突 UI 之间缺少清晰边界:
- 上传附件或向文件树拖入文件后,非 Markdown 文件变化会进入正文外部变更链路。
- 新建页面后,旧的冲突提示可能被同 root 的事件流带到新页面,刷新后消失。
- 真实冲突仍然需要保留:当前 Markdown 有未保存编辑,同时磁盘上的同一个 Markdown 文件被外部修改时,必须进入冲突处理。
## Sidex 对照
Sidex/VSCode 的核心模型是 `StoredFileWorkingCopy`
- 每个 working copy 绑定一个具体 `resource`
- 保存时以 `lastResolvedFileStat.etag/mtime` 做 dirty write prevention。
- 只有同一个 resource 的写入出现 `FILE_MODIFIED_SINCE` 时,才进入 `inConflictMode`
- 文件系统 watcher 的目录级事件不会直接把同目录其它文件变化升级成当前 working copy 冲突。
MNote 不需要照搬 VSCode 的实现,但应采用同一条合同:正文冲突只属于当前 Markdown 文件,不属于同 root 下任意附件、文件树资源或元数据文件。
## 当前根因
当前 `mnote-web` 的 document event channel 以 `rootUri` 共享:
- 前端订阅 `/api/local-folder/events?rootUri=...`,没有传 `documentId`
- 后端 `build_document_events_stream` 在没有 `documentId` 时不会按 Markdown 相对路径过滤。
- watcher 对非 Markdown 资源发出的 payload 中 `documentId` 为空。
- 前端收到空 `documentId` 后,仍会对同 root 下所有 document session 设置 `externalChangePending` 并调度正文刷新。
这会把附件上传、文件树拖入、资源创建等 root 级变化误投递到正文 session,形成文件冲突误报和跨页面状态污染。
另一个独立根因是冲突 UI 的 DOM 生命周期:
- `renderSessionConflictSurface` 会把冲突面板直接插入 `.document-pane`
- 页面切换时 `unmountEditorViewBinding` 只卸载 editor runtime 和事件监听,没有清理旧 session 的冲突面板。
- 因此旧页面已出现冲突时,新建/切换到新页面可能短暂看到旧冲突面板;刷新后整页 DOM 重建,问题消失。
## 合同
1. Markdown 正文 session 只订阅自己的 Markdown 文件事件。
2. 非 Markdown 资源事件只用于文件树/资源投影刷新,不触发正文 `externalChangePending`
3. 真实正文冲突只在“同一个 Markdown documentId + 当前 session dirty/saving/saveTimer/recent input”时出现。
4. 同 root 下不同 Markdown 页面必须有独立 document event channel。
5. 文件树 live stream 继续承担 root 级变化刷新,不依赖正文冲突链路。
## 已执行修复切片
- 前端 document event channel key 从 `rootUri` 收窄为 `rootUri#documentId`
- 前端订阅本地文件夹 document events 时携带 `documentId`
- 前端收到无 `documentId` 的 change payload 时直接忽略,不再设置正文 `externalChangePending`
- editor view unmount 时清理旧 session 的冲突面板,避免手动插入的 DOM 跨页面残留。
- 后端补充 document event filter 测试,确保资源文件路径不会匹配 Markdown 正文路径。
## 后续系统性收口
-`DocumentBufferStore` 作为正文冲突唯一状态底座,前端 session 只展示 buffer state,不自行拼第二套冲突事实。
- 为资源 tab 增加独立 resource watch 合同:按 `resourcePath` 监听资源文件,而不是复用 Markdown document event。
- 为新建页面增加创建者写入抑制或 bootstrap generation,避免未来新建空文件 watcher 与首次打开时序竞争。
-`task451` 的真实冲突 smoke 与上传/新建页面无冲突 smoke 合并成一组 local Markdown conflict regression。
## 验收
- 上传第一个/第二个附件后,主编辑区不显示 `mnote-editor-conflict-panel`
- 新建页面后,旧页面冲突 UI 不带入新页面。
- 新建页面后直接向文件树拖入文件,不显示文件冲突。
- dirty 当前 Markdown 后由外部修改同一个 `.md` 文件,仍显示冲突并保留 accept disk / keep current / diff 流程。
@@ -0,0 +1,235 @@
# 5-33 导航页与路由守卫执行清单 v1
> 状态:process
>
> Owner05-editor-mainline / 03-rust-web / control-plane
>
> 创建时间:2026-05-27
>
> 背景:当前本地文件夹入口在没有明确页面时容易回退打开默认页面;未登录、删除页、失效页等路径也缺少统一页面级恢复策略。需要把“导航页作为主页”与“路由守卫”合并设计,避免用户停留在错误页或误打开无关页面。
## 0. 当前落地状态
- [x] Phase A 核心语义已落地:`/` 与 local folder / folder scope 无明确页面时渲染导航页,不再自动打开 recent / first page。
- [x] Phase B recent 闭环已落地:control-plane SQLite recent 表、读写 API、SSR recent 展示、打开 folder/page 写入 recent。
- [x] Phase C 主路径已落地:导航页/星标文件夹进入 scoped 导航页;FileTree 普通文件夹点击只展开;Markdown 普通点击记录 recent 并打开文档页。
- [x] Phase D 验证已落地:补 Rust route/API 测试与 `task500-navigation-page-route-guard-smoke.js` browser smoke,覆盖未登录、删除页回退、recent 分组和导航页首屏无 editor bootstrap。
- [ ] 后续增强:文档页 breadcrumb 文件夹段逐级回到导航页、所有 fetch 404/410 的全局浏览器兜底可继续拆独立 follow-up;当前服务端 HTML shell 已覆盖 canonical route guard。
## 1. 目标
- 本地文件夹和文件夹 scope 没有明确页面时显示导航页,不再自动打开 recent / first page 作为默认正文。
- 导航页是主编辑区的主页状态;点击 Markdown 页面后,文档页覆盖该主页。
- 点击文件夹后,行为对齐星标文件夹:切到 scoped Explorer,并以该文件夹为导航页上下文。
- 导航页展示最近访问的文件夹和最近访问的页面,两组分开显示,并可直接访问。
- 页面级路由守卫统一处理未登录、无权限、页面不存在、页面已删除等场景。
- 首屏性能不因导航页退化:不构建 Page Aggregate,不启动 tiptap island,不额外扫描完整大目录。
## 2. 非目标
- 不新增第二套树真相;导航页只消费 Rust projection / control-plane recent。
- 不把 recent 写入 Markdown frontmatter 或本地文件夹 `.mnote` 作为长期事实。
- 不把文件夹导航页做成真实 Markdown 页面。
- 不改变 local-first Markdown 正文事实源。
- 不在本阶段实现复杂最近访问排序模型,例如全文索引热度、跨设备文件内容同步或多设备路径重绑定。
## 3. 产品语义
### 3.1 导航页状态
- 工作区导航页:`/` 或默认 local workspace 入口,展示工作区级最近访问、打开本地文件夹入口和当前工作区概览。
- 文件夹导航页:`/?sourceKind=local_folder&rootUri=...&fileTreeScope=docs&treeView=filetree`,展示 `docs/` scope 的子文件夹、Markdown 页面和资源入口。
- 导航页不是错误页;它是没有明确文档目标时的正常主页。
### 3.2 点击行为
- 点击最近文件夹:进入该文件夹 scope,主区域显示文件夹导航页。
- 点击最近页面:打开文档页并覆盖导航页。
- 在导航页或 Explorer 中点击 Markdown:打开文档页并覆盖导航页。
- 在文档页点击 breadcrumb 的文件夹段:回到对应文件夹导航页。
- Ctrl / Meta / 中键点击页面:允许浏览器新标签页打开;普通点击优先走 MNote 当前主编辑区。
### 3.3 失效恢复
- 未登录访问受保护页面:跳转 `/auth?next=<encoded-current-url>`
- 登录后有 `next`:返回原始目标;如果目标已失效,则进入对应导航页。
- Markdown 文件不存在、已删除或 document id 无法解析:回到当前 `rootUri + fileTreeScope` 的导航页,并显示轻量提示。
- 文件夹 scope 不存在或无权限:回到 root 导航页或授权页,不停留在空白错误态。
## 4. 架构判断
- 导航页属于 `05-editor-mainline` 的工作区/文档壳体验,数据入口由 `03-rust-web` 的 route 和 projection 提供。
- Recent 持久化属于 control-plane 用户状态,不属于文件系统事实。
- FileTree / PageTree 内容仍由 Rust projection 生成;前端只负责导航、局部渲染和交互。
- 路由守卫 canonical 逻辑放在 Rust HTML shell route;前端 fetch guard 只做浏览器交互恢复。
## 5. 数据与协议
### 5.1 Recent 记录
- [ ] 在 control-plane 增加用户 recent 记录表或复用已有偏好表能力,字段至少包含:
- `id`
- `user_id`
- `kind`: `folder | page`
- `source_kind`: MVP 先支持 `local_folder`
- `root_uri`
- `relative_path`
- `document_id`
- `title`
- `visited_at`
- `status`
- [ ] Recent folder 使用 `root_uri + relative_path` 定位;root 文件夹 `relative_path=""`
- [ ] Recent page 使用 `root_uri + document_id` 定位,并冗余保存 `relative_path/title` 方便展示。
- [ ] 每个用户每类 recent 保留有限条数,MVP 建议 folder 10 条、page 20 条。
- [ ] list recent 时做授权过滤;无权限、root 不可用、路径不存在的记录不作为可点击主项。
- [ ] localStorage 里已有 recent local roots 只作为迁移/兜底读取,不作为长期 canonical。
### 5.2 API
- [ ] 新增或扩展读取 API:返回当前用户 recent folders / recent pages。
- [ ] 新增写入 API:打开 folder scope 或 Markdown page 后记录 recent。
- [ ] API 写入只记录用户行为,不触碰 Markdown 或 `.mnote` 文件。
- [ ] API 返回项包含可直接用于导航的 URL 参数:`sourceKind/rootUri/fileTreeScope/documentId/workspaceId`
## 6. Rust HTML Shell
### 6.1 导航页渲染
- [ ] 新增导航页 view model,区分 `workspace``folder` 两种 scope。
- [ ] 导航页首屏只加载 shallow file projection 和 recent 列表。
- [ ] 导航页不调用 `build_page_aggregate_snapshot`
- [ ] 导航页不注入 `__MNOTE_PAGE_AGGREGATE__` / `__MNOTE_EDITOR_BOOTSTRAP__`,除非后续明确需要编辑器。
- [ ] 导航页继续使用 `PageLayout` 和现有 workspace sidebar。
- [ ] 文件夹导航页主区域展示:
- 当前文件夹标题和路径面包屑
- 子文件夹列表
- 当前层 Markdown 页面列表
- 当前层资源列表
- 最近访问页面和最近访问文件夹
### 6.2 `/` 入口选择规则
- [ ] `/` 未登录继续跳 `/auth?next=/`
- [ ] `/``pageId` 且无 `fileTreeScope`:显示工作区导航页。
- [ ] `/``sourceKind=local_folder&rootUri=...` 但无 `pageId`:显示对应 root 导航页。
- [ ] `/``fileTreeScope`:显示该 scope 文件夹导航页。
- [ ] `/``pageId`:尝试打开文档页;失败时回退导航页。
- [ ] 移除或限制 `recent_page_id -> first_page` 的自动正文打开行为,避免文件夹入口误开默认页面。
### 6.3 `/documents/{document_id}` 守卫
- [ ] 未登录访问 `/documents/{document_id}``303 /auth?next=<current-url>`
- [ ] local_folder 缺少 `rootUri`:跳 root/workspace 导航页,并保留错误提示参数。
- [ ] `local_markdown_not_found`:跳 `/?sourceKind=local_folder&rootUri=...&fileTreeScope=<parent>&missingPage=<document_id>`
- [ ] `local_workspace_access_denied`:跳授权说明页或 root 导航页提示无权限。
- [ ] 其它不可恢复错误保留错误页,但必须带 trace id。
## 7. Browser Runtime
- [ ] 抽出 `openNavigationPageForFolder(rootUri, relativePath, workspaceId)` helper,供最近文件夹、星标文件夹、breadcrumb 文件夹段复用。
- [ ] 点击文件夹时不只展开侧栏;如果是导航动作,应更新 URL `fileTreeScope` 并渲染文件夹导航页。
- [ ] 点击 Markdown 时记录 recent page,然后打开文档页覆盖导航页。
- [ ] 点击导航页或星标 folder scope 时记录 recent folder,然后显示导航页;FileTree 普通文件夹点击只展开子项。
- [ ] fetch 返回 `401` 且当前是浏览器交互:跳 `/auth?next=<current-url>`
- [ ] fetch 返回 `404/410` 且目标是当前页面:跳当前文件夹导航页。
- [ ] 保留普通业务错误 toast,不把所有 API 错误都变成导航跳转。
## 8. 性能约束
- [ ] 导航页首屏不得扫描完整 workspace root 下所有 Markdown。
- [ ] 文件夹导航页使用 scoped shallow FileTree projection。
- [ ] PageTree scope 可继续给 sidebar 使用,但主导航页首屏不依赖完整 PageTree。
- [ ] Recent list 查询必须限制条数。
- [ ] 大目录 smoke 要验证打开 `design/` scope 不显示 root 兄弟目录,不触发完整 root 渲染。
- [ ] 导航页首屏不启动 tiptap island,避免无文档目标时加载编辑器 bundle。
- [ ] 若需要最近页面标题刷新,只在列表展示时对有限条记录做存在性校验,不批量读取正文。
## 9. 验收标准
- [ ] 登录后访问 `/` 显示导航页,而不是自动打开某个默认页面。
- [ ] 打开本地文件夹 root 后显示该文件夹导航页。
- [ ] 点击文件夹进入 scoped 导航页,Sidebar Explorer 与主区域 scope 一致。
- [ ] 点击 Markdown 后打开文档页并覆盖导航页。
- [ ] 最近文件夹和最近页面分组展示;点击最近文件夹进入导航页,点击最近页面打开文档。
- [ ] 删除当前 Markdown 后刷新旧 `/documents/...`,进入父文件夹导航页并显示提示。
- [ ] 未登录访问 `/documents/...``/auth?next=...`,登录后能回到目标或导航页兜底。
- [ ] 无权限访问本地 folder 不显示空白文档页。
- [ ] 导航页没有注入页面 aggregate 和 editor bootstrap。
- [ ] 大目录 scope 打开不退化为完整 root 扫描。
## 10. 测试计划
### 10.1 Rust 单元 / route 测试
- [ ] `root_entry_without_page_renders_navigation_home`
- [ ] `root_entry_local_folder_scope_renders_folder_navigation`
- [ ] `root_entry_does_not_auto_open_recent_page_for_folder_scope`
- [ ] `document_shell_unauthenticated_redirects_to_auth_with_next`
- [ ] `document_shell_missing_local_markdown_redirects_to_folder_navigation`
- [ ] `recent_navigation_records_are_user_scoped`
- [ ] `recent_navigation_list_filters_inaccessible_local_roots`
### 10.2 Browser smoke
- [ ] 新增 `task5xx-navigation-home-local-folder-smoke.js`
- 登录测试账号
- 打开本地 folder root
- 断言主区域是导航页
- 断言未打开默认 Markdown
- [ ] 新增 `task5xx-navigation-folder-scope-smoke.js`
- 点击文件夹
- 断言 URL 含 `fileTreeScope`
- 断言主区域标题和 Explorer scope 一致
- [ ] 新增 `task5xx-navigation-recent-smoke.js`
- 打开两个文件夹和一个 Markdown
- 回到导航页
- 断言 recent folders / recent pages 分组可见并可点击
- [ ] 新增 `task5xx-route-guard-auth-and-deleted-page-smoke.js`
- 未登录访问文档 URL 跳 auth
- 登录后打开文档
- 删除对应 `.md`
- 刷新旧文档 URL 后回到文件夹导航页
### 10.3 回归测试
- [ ] `cargo test -p mnote-web root_entry -- --nocapture`
- [ ] `cargo test -p mnote-web local_folder -- --nocapture`
- [ ] `node scripts/task492-sidebar-starred-shortcuts-smoke.js`
- [ ] `node scripts/task497-local-page-tree-filetree-open-performance-smoke.js`
- [ ] `git diff --check`
- [ ] 修改代码后运行 `codegraph sync .`,提交前确认 CodeGraph 无 pending。
## 11. 分阶段执行建议
### Phase A:守住语义
- [ ] 新增导航页 SSR 壳和 route 测试。
- [ ]`/` 入口选择规则,不再在 folder scope 下自动打开默认页面。
- [ ] `/documents/{document_id}` 增加未登录和 missing local markdown 路由恢复。
### Phase BRecent 闭环
- [ ] control-plane 增加 recent 记录。
- [ ] 导航页 SSR 展示 recent folders/pages。
- [ ] 打开 folder/page 时写入 recent。
### Phase C:浏览器交互对齐
- [ ] 文件夹点击进入导航页 scope。
- [ ] Markdown 点击覆盖导航页。
- [ ] breadcrumb 文件夹段返回导航页。
- [ ] fetch guard 处理 `401/404/410`
### Phase D:性能与 smoke
- [ ] 确认导航页首屏不构建 editor / aggregate。
- [ ] 大目录 scoped smoke 验证不加载完整 root。
- [ ] 删除页、未登录、recent 点击 smoke 通过。
## 12. 风险与边界
- Recent 写入如果只做前端 localStorage,会和登录用户、授权过滤、SSR 首屏冲突;MVP 应优先 SQLite。
- 文件夹导航页如果复用完整 PageTree 递归扫描,可能在大目录退化;首屏应以 shallow FileTree projection 为主。
- `mnote_recent_page_id` 旧 cookie 不能继续作为文件夹入口自动打开正文的依据;最多用于工作区级“继续编辑”入口展示。
- 路由守卫不能把所有错误都吞成导航页;不可恢复错误仍要保留 trace,方便排障。
- Ctrl / Meta / 中键打开浏览器新标签页不能破坏普通点击覆盖主页的主路径。
@@ -0,0 +1,259 @@
# 7-39 Page AI agent selector / context authorization settings v1
> 创建时间:2026-05-28
> 状态:`done`
> 来源:用户反馈:旧 Page AI UI 信息过载;“编辑当前文件 / 工作区任务”模式难以区分;应参考 Cline 在聊天输入区选择 agent,并在设置里按 agent 拆分配置;共性设置包含授权区域,且授权区域应按用户保存到 SQLite。
## 1. 背景
当前 Page AI sidebar 已能接 Hermes / Reasonix ACP、run target snapshot、changed files 审计和 SQLite directory grant。但 UI 仍把 agent、profile、runtime、skills、history、gateway、工具参数和错误解释混在一个抽屉中。
这导致两个问题:
- 用户真正要做的只是“选择 agent + 勾选要发给 AI 的上下文地址”,但现有 UI 暴露了太多 Hermes/ACP 运行细节。
- “编辑当前文件”和“工作区任务”作为主模式不稳定:本地 Markdown、打开资源、选区、文件夹和 changed files 都可能参与同一轮请求,按模式分流反而会误导。
因此后续 Page AI 主线应从“模式切换”改为“agent 选择 + contextRefs 勾选 + SQLite 授权区域”的结构。
## 2. 参考对照
### 2.1 Cline
可借鉴模型:
- 聊天输入区附近直接选择当前 agent / provider,让用户在提问前完成路由决策。
- 设置页按 agent/provider 拆分,只把模型、profile、API、能力开关等差异配置放在各自页面。
- 共性配置只放所有 agent 都需要理解的内容,例如上下文、授权、审计、历史保留。
只作参考实现:
- Cline 的 VSCode extension 状态和 provider 配置模型不能直接搬到 MNote;MNote 的授权区域必须继续走 SQLite control-plane,而不是浏览器 localStorage 或扩展全局状态。
不适合 MNote
- 不把“工作区 root”当作天然可读写范围。MNote 必须基于 `directory_grants` / `AiAccessScope` / allowed roots 显式授权。
### 2.2 Sidex / VSCode workbench
可借鉴模型:
- 资源、编辑器、工作区和最近修改项应以可选引用表达,而不是把 UI 分成互斥大模式。
- Explorer / editor / panel 可以共享同一份打开资源快照,但不把 UI 层选择变成新的树或页面真相。
只作参考实现:
- VSCode / Sidex 的工作台布局、tab model、context key 可以作为交互参考;MNote 的真实对象边界仍由 Rust kernel projection、local-first source 和 control-plane 决定。
不适合 MNote
- 不把 VSCode extension host 或工作台状态当成 MNote 的资源生命周期真相。
## 3. 产品决策
### 3.1 主聊天面
主聊天面只保留四类默认可见元素:
- 当前 agent 选择器:位于输入框下方或紧邻输入框,默认可快速切换 `Hermes``Reasonix``Chat-only`,未来可扩展其他 agent。
- ContextRefs 选择器:用勾选 chip / popover 选择要发送的地址,例如当前页、当前选区、当前打开资源、指定文件、指定文件夹、最近 changed files。
- 输入框和发送/停止按钮。
- 简短状态:只显示“可用 / 需要授权 / 正在运行 / 失败”,不默认显示 gateway、profile、tool trace、参数 JSON。
“编辑当前文件 / 工作区任务”不再作为主模式。它们退化为 contextRefs 的预设组合:
- 编辑当前文件:勾选当前页或当前文件 + 必要的写权限 allowed root。
- 工作区任务:勾选一个或多个文件夹 / changed files / 打开资源 + 必要的读写授权。
### 3.2 共性设置
共性设置只保留所有 agent 共享的配置:
- 授权区域:来自 SQLite `directory_grants`,按当前用户隔离;展示 root、权限、递归、来源、状态。
- 默认上下文勾选:保存到 SQLite `user_ui_preferences`,按用户、workspace、source 隔离。
- changed files 展示策略:默认收起,只显示摘要;需要时展开路径/hash/mtime/actor。
- 隐私与审计:是否记录 local agent audit、是否展示工具细节、是否允许发送选区/页面摘要。
共性设置不放 Hermes profile、Reasonix skill、provider API key、模型 preset 等 agent 差异项。
### 3.3 Agent 设置
设置页按 agent 拆分:
- Hermesprofile、Hermes 设置入口、Hermes/ACP runtime 兼容提示、Hermes 专属 tool 开关。
- ReasonixACP runtime、Reasonix skill 只读/可用状态、Reasonix 专属能力说明。
- Chat-only:模型或 provider 选择、是否允许读取 contextRefs;默认不请求文件写权限。
- 未来 agent:通过 registry 增加 `agentId`、显示名、能力、设置 schema、运行入口。
每个 agent 只能保存自身差异设置。默认 agent、默认 contextRefs 和授权区域仍属于共性设置。
## 4. 数据与合同
### 4.1 SQLite 使用边界
优先复用现有 control-plane
- `directory_grants`:每用户授权区域真相。用于生成 allowed roots,不能由前端本地状态代替。
- `user_ui_preferences`:每用户 UI 偏好。用于默认 agent、contextRefs 默认勾选、授权区域排序/最近使用、UI 密度、changed files 默认折叠状态。
建议 preference scope / key
- `scope = "ai.common"``key = "default_agent_id"`
- `scope = "ai.common"``key = "context_refs.default_selected"`
- `scope = "ai.common"``key = "allowed_roots.default_selected"`
- `scope = "ai.common"``key = "allowed_roots.order"`
- `scope = "ai.common"``key = "changed_files.visibility"`
- `scope = "ai.agent.hermes"``key = "profile_id"`
- `scope = "ai.agent.reasonix"``key = "skill_panel_filter"`
- `scope = "ai.agent.chat_only"``key = "model_id"`
除非现有表无法表达这些键值,否则第一阶段不新增 SQLite 表。
### 4.2 请求合同草案
```json
{
"agentId": "reasonix",
"message": "把选中的 Markdown 小节整理成 checklist",
"contextRefs": [
{
"kind": "current_page",
"documentId": "local:page:example",
"rootUri": "file:///mnt/Data1T/mnote"
},
{
"kind": "folder",
"rootUri": "file:///mnt/Data1T/mnote",
"relativePath": "design/07-ai"
}
],
"allowedRoots": [
{
"rootUri": "file:///mnt/Data1T/mnote",
"permission": "write",
"source": "sqlite_directory_grant"
}
],
"runTargetSnapshot": {
"version": "mnote.page_ai_run_target_snapshot.v1",
"source": "open_editors_snapshot"
},
"audit": true
}
```
合同要求:
- `agentId` 是主路由字段,不能再隐含在 Hermes profile 或 ACP runtime 里。
- `contextRefs` 是用户勾选的上下文地址,不等同于授权。
- `allowedRoots` 必须由 SQLite 授权区域和当前用户权限解析得到。
- `runTargetSnapshot` 继续从 `OpenEditorsSnapshot` 冻结,防止 run 过程中 UI 切换导致目标漂移。
## 5. 旧 UI 精简规则
默认聊天面应移除或收敛:
- 顶部同时展示 Hermes/profile/model/gateway 的多重状态。
- 空态长解释、示例段落和“后端 AI 会话”等实现细节文案。
- Runtime 页默认暴露 gateway health、queue、tools、trace、audit JSON。
- Skills 页默认展示大量参数、来源和技术标签。
- 工具调用卡默认展示 args/result/raw trace。
- 输入框 placeholder 固定写“问 Hermes...”。
替代方式:
- 默认只显示 agent 名称、上下文 chips、运行状态和最终回答。
- 工具调用、changed files、audit、gateway、queue 进入“高级 / 调试”折叠区。
- 错误文案先给用户动作,例如“需要授权该文件夹”,再提供可展开技术详情。
- Hermes / Reasonix 的差异通过 agent selector 和 agent 设置页表达,不在聊天主面堆说明。
## 6. Checklist
### Batch A - 现状冻结与视觉基线
- [x] 截图记录当前 Page AI sidebar 默认聊天面、Agent、Runtime、Skills、History 页。
- [x] 标注必须删除/折叠的冗余信息:profile/model/gateway、runtime queue、tool trace、长空态说明、Hermes 固定文案。
- [x] 对照 Cline agent selector 和 Sidex/VSCode context/resource 模型,形成最小信息架构图。
- [x] 验证:浏览器截图 + 本文档补充截图路径或 smoke 证据。
证据:旧 UI 基线截图在 `tmp/task502-page-ai-baseline/01-chat.png``02-agent.png``03-skills.png``04-runtime.png``05-history.png`;收敛后截图在 `tmp/task502-page-ai-agent-selector-context-smoke/01-agent-selector-context.png`
### Batch B - Agent registry 与主选择器
- [x] 定义 `agentId` registry`hermes``reasonix``chat_only`,包含显示名、能力、是否可写文件、设置页入口。
- [x] 在聊天输入区附近增加 agent selector,替代“Hermes 固定入口”。
- [x] 发送请求时 payload 显式包含 `agentId`,旧 Hermes/Reasonix ACP 路径保持兼容。
- [x] 验证:node smoke 断言切换 agent 后 payload 和 UI badge 正确。
证据:`rust/crates/mnote-web/browser/sidebar-page-ai-runtime.js``PAGE_AI_AGENT_REGISTRY``scripts/task502-page-ai-agent-selector-context-smoke.js` 断言 `hermes/reasonix/chat_only` 和 run payload `agentId=reasonix`
### Batch C - ContextRefs 勾选器
- [x] 定义 `contextRefs` 类型:当前页、选区、当前打开资源、文件、文件夹、changed files。
- [x] ContextRefs 从 `OpenEditorsSnapshot`、当前 selection 和授权目录解析,不新增第二套页面/树真相。
- [x] “编辑当前文件 / 工作区任务”改为预设组合,不再作为主模式。
- [x] 验证:浏览器 smoke 勾选/取消 contextRefs 后 payload 稳定。
证据:`PAGE_AI_CONTEXT_REF_REGISTRY` 覆盖 `current_page/selection/active_editor/file/folder/changed_files``task502` 断言取消 `current_page` 后 payload 不含 `current_page/pageText/pageXml/contextBlocks`,并保留 `active_editor/folder/changed_files`
### Batch D - SQLite per-user 授权区域
- [x] 共性设置页展示当前用户的 `directory_grants`,支持按 read/write、recursive、source、status 展示。
- [x] allowedRoots 只从当前用户 SQLite grant 解析;未授权目录必须提示授权,不允许前端绕过。
- [x] 修复/补齐 Page AI 与 ACP session create 的授权路径一致性,确保已授权本地文件夹不会误报无权访问。
- [x] 验证:Rust 定点测试覆盖不同用户 grant 隔离;浏览器 smoke 覆盖授权目录可用、未授权目录阻断。
证据:`hermes_client.rs::enforce_local_ai_run_access` 服务端按当前 actor 调 `resolve_access` 重算 `allowedRoots``sqlite.rs::resolve_access_does_not_match_sibling_uri_prefix` 修复 URI 前缀串扰;`local_ai_run_access_*` 覆盖 Alice/Bob 隔离与 `chat_only` read-only 降级;`task502` 验证授权 chip 来源为 `sqlite_directory_grant`
### Batch E - UI preference per-user 持久化
- [x]`user_ui_preferences` 保存默认 agent、默认 contextRefs、授权区域排序/默认勾选、changed files 折叠策略。
- [x] preference key 必须带 user/workspace/source/scope 边界。
- [x] 不用 localStorage 作为长期真相;最多作为临时 UI cache。
- [x] 验证:Rust/JS smoke 覆盖 Alice/Bob 设置隔离。
证据:`ui_preferences.rs` 支持 `ai.common.*``ai.agent.*`,并返回 `aiPreferences``ui_preferences_api_persists_ai_preferences_per_user_and_workspace` 验证 Alice/Bob 隔离;`task502` 捕获 PUT `ai.common.default_agent_id``ai.common.context_refs.default_selected`
### Batch F - Agent 分页设置
- [x] 设置页拆为 Common / Hermes / Reasonix / Chat-only。
- [x] Common 只放授权、默认 contextRefs、隐私/审计、changed files 显示策略。
- [x] Hermes 只放 Hermes profile/upstream/tool 相关设置。
- [x] Reasonix 只放 Reasonix ACP/skill 相关设置。
- [x] Chat-only 默认不申请文件写权限。
- [x] 验证:浏览器 smoke 检查不同 agent 设置页不串项。
证据:`ensurePageAiDrawer` 拆出 Common/Hermes/Reasonix/Chat-only/高级面板;`task502` 用真实浏览器检查 Common 不含 `Hermes profile/ACP runtime`Hermes 不混 ReasonixReasonix 不混 HermesChat-only 不混 ACP runtime。
### Batch G - 旧 UI 冗余收敛
- [x] 默认聊天面隐藏 gateway/model/profile 技术细节,只保留 agent badge 和简短状态。
- [x] Runtime/Skills/History 收敛为高级/调试入口,默认不进入主聊天流。
- [x] 工具调用卡默认只展示摘要;args/result/raw trace 需展开。
- [x] changed files 默认摘要,展开后才显示路径/hash/mtime/actor。
- [x] 验证:浏览器截图对比,确保主面没有冗余解释和技术噪音。
证据:`sidebar-page-ai-runtime.js` 默认标题状态改为 agent/授权/AI session,旧 `Hermes session` 与固定 `问 Hermes` 文案退出默认聊天面;`task502` 断言默认聊天面不含 `问 Hermes/model.default/gateway:/Hermes profile`changed files 仍由 `task453-local-folder-page-ai-changed-files-smoke.js` 验证摘要与 dirty 阻断。
### Batch H - 合同与回归
- [x] 补最小 JS/Rust 合同注释或文档,说明 `agentId + contextRefs + allowedRoots + runTargetSnapshot` 的边界。
- [x] 保持 Hermes / Reasonix 旧 payload 兼容,必要时在 route 层做窄 adapter。
- [x] 更新 Page AI smoke,覆盖 agent 切换、contextRefs、SQLite 授权、per-user preference、旧路径兼容。
- [x] 运行 `git diff --check`、相关 Rust 定点测试、node smoke;涉及代码图修改后运行 `codegraph sync .`
证据:JS 合同注释位于 `PAGE_AI_AGENT_REGISTRY` 前;服务端兼容仍走 `/api/hermes/client/runs` 和 ACP `profile/acpRuntime` adapter;最终验证矩阵见本次执行汇报。
## 7. 非目标
- 不实现 Phase C 流式 apply / suggest review。
- 不把 local-first 普通 Markdown 编辑改回粗粒度 `mnote.page.save`
- 不新增第二套树真相、页面真相、资源生命周期真相或授权真相。
- 不把 agent 设置写入浏览器 localStorage 作为长期存储。
- 不在本批次重构 Hermes / Reasonix agent 内部运行时。
## 8. 待确认产品点
- Agent selector 的精确位置:输入框下方一行,还是输入框左侧/上方 compact row。
- ContextRefs 默认勾选:是否默认包含当前页和当前选区,还是只默认当前页。
- Chat-only 是否允许读取文件夹摘要,或严格只允许聊天文本和显式勾选的页面摘要。
- 技术调试入口默认是否仅管理员可见,还是普通用户也可展开。
@@ -0,0 +1,694 @@
# 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 .
```