diff --git a/design/03-rust-web/process/3-24-global-content-type-page-width-settings-v1.md b/design/03-rust-web/process/3-24-global-content-type-page-width-settings-v1.md new file mode 100644 index 00000000..8f0dca5c --- /dev/null +++ b/design/03-rust-web/process/3-24-global-content-type-page-width-settings-v1.md @@ -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(...))` 或等价约束收敛。 diff --git a/design/05-editor-mainline/done/5-27-local-markdown-working-copy-conflict-contract-v1.md b/design/05-editor-mainline/done/5-27-local-markdown-working-copy-conflict-contract-v1.md new file mode 100644 index 00000000..fd6104c2 --- /dev/null +++ b/design/05-editor-mainline/done/5-27-local-markdown-working-copy-conflict-contract-v1.md @@ -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 流程。 diff --git a/design/05-editor-mainline/process/5-27-local-markdown-working-copy-conflict-contract-v1.md b/design/05-editor-mainline/process/5-27-local-markdown-working-copy-conflict-contract-v1.md deleted file mode 100644 index 0f76887e..00000000 --- a/design/05-editor-mainline/process/5-27-local-markdown-working-copy-conflict-contract-v1.md +++ /dev/null @@ -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 流程。 diff --git a/design/05-editor-mainline/process/5-33-navigation-page-route-guard-checklist-v1.md b/design/05-editor-mainline/process/5-33-navigation-page-route-guard-checklist-v1.md new file mode 100644 index 00000000..2249f765 --- /dev/null +++ b/design/05-editor-mainline/process/5-33-navigation-page-route-guard-checklist-v1.md @@ -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=`。 +- 登录后有 `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=`。 +- [ ] local_folder 缺少 `rootUri`:跳 root/workspace 导航页,并保留错误提示参数。 +- [ ] `local_markdown_not_found`:跳 `/?sourceKind=local_folder&rootUri=...&fileTreeScope=&missingPage=`。 +- [ ] `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=`。 +- [ ] 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 / 中键打开浏览器新标签页不能破坏普通点击覆盖主页的主路径。 diff --git a/design/07-ai/done/7-39-page-ai-agent-selector-context-authorization-settings-v1.md b/design/07-ai/done/7-39-page-ai-agent-selector-context-authorization-settings-v1.md new file mode 100644 index 00000000..177640a7 --- /dev/null +++ b/design/07-ai/done/7-39-page-ai-agent-selector-context-authorization-settings-v1.md @@ -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 是否允许读取文件夹摘要,或严格只允许聊天文本和显式勾选的页面摘要。 +- 技术调试入口默认是否仅管理员可见,还是普通用户也可展开。 diff --git a/design/10-review/process/17-sidex-mnote-workbench-gap-execution-checklist-v1.md b/design/10-review/process/17-sidex-mnote-workbench-gap-execution-checklist-v1.md new file mode 100644 index 00000000..f83a0699 --- /dev/null +++ b/design/10-review/process/17-sidex-mnote-workbench-gap-execution-checklist-v1.md @@ -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::`、`resource:mindmap::`、`resource:onlyoffice::` 等 identity;这应进入 Batch 2,而不是阻塞 Batch 1。 + +Batch 1 最小文件边界: + +- 修改:`rust/crates/mnote-web/browser/document-resource-tab-runtime.js` +- 可选测试:复用或扩展 `scripts/task496-editor-open-parallel-runtime-aggregate-smoke.js` +- 暂不修改:`document-editor-adapter-runtime.js`、`sidebar-filetree-open-runtime.js`、`resource-open-runtime.js` + +Batch 1 最小实现切片: + +- [x] `buildOpenEditorsSnapshot()` 增加 `groups` 字段,按 `primary/secondary` 输出每个 pane 的 active object 与 editors。 +- [x] `openEditorsSnapshotEntry()` 补 `paneRole`、`documentId`、`workspaceId`、`lastActiveAt`、`preview:false`、`pinned:false`。 +- [x] Page entry 补 `preview:false`、`pinned:true`、`lastActiveAt` 占位,保持页面 tab 是固定 tab。 +- [x] 更新 smoke 断言:`window.__mnoteOpenEditorsSnapshot.groups.primary` 存在,secondary 打开后 `groups.secondary` 存在。 + +## 6. 最小成功标准 + +本 checklist 完成到 P0 时,MNote 至少应满足: + +- 打开页面 / 资源 / secondary pane 的状态有统一 snapshot,可恢复、可被 AI target resolver 消费。 +- local-first Markdown dirty/stale/conflict 只由 BufferStore / Page Aggregate 主链表达。 +- FileTree row identity、resource tab identity、AI target identity 不再多处临时拼接。 +- tree live / local-folder watcher 不再常规整树替换,view state 不丢。 +- 菜单、快捷键、dangerous action 的 enabled/disabled/reason 有统一 context。 +- local-first tree/resource command 不静默退回 legacy `documents.*` 主链。 + +## 7. 验证总入口 + +按改动范围选择运行: + +```bash +node scripts/task114-rust-web-gateway-entry-smoke.js +node scripts/task159-auth-entry-smoke.js +node scripts/task164-desktop-hot-local-folder-main-entry-smoke.js +node scripts/task166-local-first-managed-workspace-no-convex-smoke.js +node scripts/task167-local-markdown-title-body-options-no-convex-smoke.js +node scripts/task490-runtime-surfaces-smoke.js +cargo test --manifest-path rust/Cargo.toml -p mnote-web --lib -- --test-threads=1 +git diff --check +codegraph sync . +codegraph status . +``` diff --git a/design/01-05-current-priority-overview.md b/design/old/01-05-current-priority-overview.md similarity index 100% rename from design/01-05-current-priority-overview.md rename to design/old/01-05-current-priority-overview.md