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