2026-05-09 19:05:06 +08:00
|
|
|
|
# 4-22 [done] Local Folder / Convex 统一树源顺序执行清单 v1
|
2026-05-08 00:41:03 +08:00
|
|
|
|
|
|
|
|
|
|
> 更新时间:2026-05-06
|
|
|
|
|
|
>
|
|
|
|
|
|
> 来源:
|
2026-05-09 19:05:06 +08:00
|
|
|
|
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-20-vscode-explorer-file-tree-alignment-v1.md`
|
|
|
|
|
|
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-21-local-folder-convex-unified-tree-source-v1.md`
|
2026-05-08 00:41:03 +08:00
|
|
|
|
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-2-sidebar-pagetree-filetree-product-interaction-contract-v1.md`
|
|
|
|
|
|
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-4-tree-projection-protocol-contract-v1.md`
|
|
|
|
|
|
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-6-tree-command-protocol-cutover-stage2-v1.md`
|
|
|
|
|
|
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-7-tree-shell-ui-state-boundary-v1.md`
|
|
|
|
|
|
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-5-1-page-aggregate-contract-v1.md`
|
|
|
|
|
|
> - `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/vscode`
|
2026-05-09 19:05:06 +08:00
|
|
|
|
>
|
|
|
|
|
|
> 状态说明:本清单对应的 Local Folder / Convex 统一树源主链已进入已完成基线;后续 Markdown parser、Page Aggregate Rust-first、tree live stream 与设计状态漂移由后续稿继续跟踪。
|
2026-05-08 00:41:03 +08:00
|
|
|
|
|
|
|
|
|
|
## 0. 执行原则
|
|
|
|
|
|
|
|
|
|
|
|
本清单按顺序执行,不跳阶段。
|
|
|
|
|
|
|
|
|
|
|
|
总目标:
|
|
|
|
|
|
|
|
|
|
|
|
> **只维护一套 tree projection、tree command、preflight、runtime state 和 UI;Local Folder 与 Convex 只作为不同 `WorkspaceSource` executor 存在。**
|
|
|
|
|
|
|
|
|
|
|
|
硬约束:
|
|
|
|
|
|
|
|
|
|
|
|
- [x] 不让本地文件夹先导入 Convex 再渲染。
|
|
|
|
|
|
- [x] 不在 UI 层用本地 path 或 Convex record 拼第二套树。
|
|
|
|
|
|
- [x] 不新增 local 专用文件树 UI。
|
|
|
|
|
|
- [x] 不绕过 Rust kernel 写页面树、文件树、排序或权限。
|
|
|
|
|
|
- [x] 不把 `expanded/selected/focus/drag/drop/contextMenu` 写进 projection。
|
|
|
|
|
|
- [x] 所有写操作先 preflight,再 execute,再 projection resync。
|
|
|
|
|
|
|
|
|
|
|
|
**Done 条件:**
|
|
|
|
|
|
|
|
|
|
|
|
- 本清单对应实现中,Local 和 Convex 都通过同一 command envelope 进入 Rust command router。
|
|
|
|
|
|
- `file_tree/page_tree/page_aggregate` 的 UI 消费路径不因 source 分叉。
|
|
|
|
|
|
- 本地文件夹与 Convex 的差异只通过 capability / preflight / executor 表达。
|
|
|
|
|
|
|
|
|
|
|
|
## 1. Phase A:冻结 Source 与协议边界
|
|
|
|
|
|
|
|
|
|
|
|
### A1. 定义 `WorkspaceSource` 合同
|
|
|
|
|
|
|
|
|
|
|
|
- [x] 在 Rust 侧定义 `WorkspaceSource` trait / interface 草案。
|
|
|
|
|
|
- [x] 字段至少覆盖 `sourceKind`、`rootUri`、`workspaceId`、`capabilities`。
|
|
|
|
|
|
- [x] 方法至少覆盖 `loadSnapshot`、`watch`、`preflightCommand`、`executeCommand`、`resolvePageAggregate`。
|
|
|
|
|
|
- [x] 明确 `local_folder` 和 `convex_workspace` 两个 source kind。
|
|
|
|
|
|
- [x] 写 contract test,验证 source kind 和 root uri 会进入 command envelope。
|
|
|
|
|
|
|
|
|
|
|
|
**Done 条件:**
|
|
|
|
|
|
|
|
|
|
|
|
- Rust 代码中存在统一 `WorkspaceSource` 抽象。
|
|
|
|
|
|
- 至少有 mock source 能输出 `file_tree/page_tree` snapshot。
|
|
|
|
|
|
- 前端没有新增 source-specific tree component。
|
|
|
|
|
|
|
|
|
|
|
|
### A2. 扩展 projection source metadata
|
|
|
|
|
|
|
|
|
|
|
|
- [x] 为 projection item 补 source metadata 方案。
|
|
|
|
|
|
- [x] 字段候选固定为 `sourceKind`、`sourceUri`、`relativePath`、`storageIdentity`、`operationProfile`。
|
|
|
|
|
|
- [x] 优先放入 `resourceMeta.extra`,除非需要正式扩展 `core-protocol`。
|
|
|
|
|
|
- [x] 更新 TS 类型映射。
|
|
|
|
|
|
- [x] 补 fixture:local folder item、convex item 同时存在。
|
|
|
|
|
|
|
|
|
|
|
|
**Done 条件:**
|
|
|
|
|
|
|
|
|
|
|
|
- `file_tree` item 可以区分来自 local 还是 convex。
|
|
|
|
|
|
- UI 仍只读 projection 字段,不直接访问 source adapter。
|
|
|
|
|
|
- fixture 覆盖 `directory/md file/normal file/document/index/asset`。
|
|
|
|
|
|
|
|
|
|
|
|
### A3. 统一 command envelope
|
|
|
|
|
|
|
|
|
|
|
|
- [x] 定义 `TreeCommandEnvelope` 的 source 字段。
|
|
|
|
|
|
- [x] envelope 至少包含 `sourceKind`、`workspaceId`、`rootUri`、`targetNodeId`、`targetResourceMeta`、`selection`、`operation`。
|
|
|
|
|
|
- [x] 现有 `tree.node.*`、`tree.subtree.*`、`tree.resource.*` 进入统一 router。
|
|
|
|
|
|
- [x] router 按 source kind 分发到 executor。
|
|
|
|
|
|
- [x] 对 unknown source / capability missing 返回可解释错误。
|
|
|
|
|
|
|
|
|
|
|
|
**Done 条件:**
|
|
|
|
|
|
|
|
|
|
|
|
- 同一个 UI action 可以构造同一种 envelope。
|
|
|
|
|
|
- local / convex 的 executor 可以在测试中分别接收 envelope。
|
|
|
|
|
|
- 没有新增 `/api/local-filetree/*` 这类平行命令入口。
|
|
|
|
|
|
|
|
|
|
|
|
## 2. Phase B:Local Folder 只读主链
|
|
|
|
|
|
|
|
|
|
|
|
### B1. 打开本地文件夹入口
|
|
|
|
|
|
|
|
|
|
|
|
- [x] 增加选择本地文件夹的入口。
|
|
|
|
|
|
- [x] 生成 `SpaceRoot=file://...`。
|
|
|
|
|
|
- [x] 记录最近打开的 local root。
|
|
|
|
|
|
- [x] 禁止 root 外路径访问。
|
|
|
|
|
|
- [x] 对无权限目录返回明确错误。
|
|
|
|
|
|
|
|
|
|
|
|
**Done 条件:**
|
|
|
|
|
|
|
|
|
|
|
|
- 用户能选择一个本地文件夹并进入 local workspace。
|
|
|
|
|
|
- 3000 `desktop:hot` 主入口不能跳到 `/tree` debug shell,也不能落入 `legacy_next_compat_disabled`。
|
|
|
|
|
|
- Convex workspace 入口不受影响。
|
|
|
|
|
|
- smoke 能断言当前 source kind 为 `local_folder`。
|
|
|
|
|
|
|
|
|
|
|
|
### B2. LocalFolderSource 只读扫描
|
|
|
|
|
|
|
|
|
|
|
|
- [x] 扫描目录。
|
|
|
|
|
|
- [x] 识别 directory。
|
|
|
|
|
|
- [x] 识别 `.md` 文件。
|
|
|
|
|
|
- [x] 识别普通文件。
|
|
|
|
|
|
- [x] 默认忽略 `.git`、`node_modules`、`.mnote/trash` 等高噪音目录。
|
|
|
|
|
|
- [x] 处理 symlink:P0 只显示,不默认跟随写入。
|
|
|
|
|
|
|
|
|
|
|
|
**Done 条件:**
|
|
|
|
|
|
|
|
|
|
|
|
- 本地文件夹能输出 read-only snapshot。
|
|
|
|
|
|
- snapshot 不越过 root。
|
|
|
|
|
|
- 扫描失败不会让整个 app 崩溃。
|
|
|
|
|
|
|
|
|
|
|
|
### B3. 输出 local `file_tree`
|
|
|
|
|
|
|
|
|
|
|
|
- [x] directory 映射为 folder row。
|
|
|
|
|
|
- [x] `.md` 映射为 markdown file row。
|
|
|
|
|
|
- [x] 普通文件映射为 asset row。
|
|
|
|
|
|
- [x] iconHint 覆盖 markdown/image/video/audio/pdf/book/table/unknown。
|
|
|
|
|
|
- [x] childCount、depth、parentNodeId、position 由 kernel 生成。
|
|
|
|
|
|
- [x] 排序规则先固定为 VSCode default:folder first,name sort。
|
|
|
|
|
|
|
|
|
|
|
|
**Done 条件:**
|
|
|
|
|
|
|
|
|
|
|
|
- `kernel.project_view(file_tree)` 可以返回 local folder projection。
|
|
|
|
|
|
- UI 使用同一 Explorer surface 渲染 local projection。
|
|
|
|
|
|
- 浏览器 smoke 可看到真实目录、`.md`、普通文件。
|
|
|
|
|
|
|
|
|
|
|
|
### B4. 输出 md-only `page_tree`
|
|
|
|
|
|
|
|
|
|
|
|
- [x] 只把 `.md` 文件纳入 page tree。
|
|
|
|
|
|
- [x] 目录只作为页面树分组/父级,不作为正文页面打开。
|
|
|
|
|
|
- [x] 页面标题优先级固定为 frontmatter `title` > 第一行 H1 > 文件名。
|
|
|
|
|
|
- [x] parent/child 来自目录层级。
|
|
|
|
|
|
- [x] 同名 md 文件必须通过 path / page id 区分。
|
|
|
|
|
|
|
|
|
|
|
|
**Done 条件:**
|
|
|
|
|
|
|
|
|
|
|
|
- 同一个 local root 下,Explorer 显示所有文件,页面树只显示 `.md`。
|
|
|
|
|
|
- 点击页面树 md 节点能定位到同一个 `.md` resource。
|
|
|
|
|
|
- 不因过滤普通文件影响 page tree 层级。
|
|
|
|
|
|
|
|
|
|
|
|
### B5. `.md` 只读 Page Aggregate
|
|
|
|
|
|
|
|
|
|
|
|
- [x] 读取 `.md` 文件。
|
|
|
|
|
|
- [x] 解析 frontmatter。
|
|
|
|
|
|
- [x] 解析标题。
|
|
|
|
|
|
- [x] 生成只读 `page_aggregate`。
|
|
|
|
|
|
- [x] 文档页用现有 `DocumentShell / DocumentContent` 消费。
|
|
|
|
|
|
- [x] 文件读取失败显示错误页,不清空 Explorer。
|
|
|
|
|
|
|
|
|
|
|
|
**Done 条件:**
|
|
|
|
|
|
|
|
|
|
|
|
- 点击本地 `.md` 文件能打开文档阅读态。
|
|
|
|
|
|
- 文档页不关心内容来自 local 还是 Convex。
|
|
|
|
|
|
- `page_aggregate` 字段满足 `5-5-1` 最小契约。
|
|
|
|
|
|
|
|
|
|
|
|
## 3. Phase C:页面身份与本地元数据
|
|
|
|
|
|
|
|
|
|
|
|
### C1. 本地 page id 策略
|
|
|
|
|
|
|
|
|
|
|
|
- [x] 支持 frontmatter `mnote_id`。
|
|
|
|
|
|
- [x] 支持 `.mnote/page-ids.json`。
|
|
|
|
|
|
- [x] 支持 path-derived 临时 id,但仅用于未初始化只读模式。
|
|
|
|
|
|
- [x] page id 生成必须稳定且不与 Convex document id 冲突。
|
|
|
|
|
|
- [x] 写测试覆盖 rename/move 后 id 保持。
|
|
|
|
|
|
|
|
|
|
|
|
**Done 条件:**
|
|
|
|
|
|
|
|
|
|
|
|
- 重命名或移动 `.md` 后,page id 不变。
|
|
|
|
|
|
- 没有 id 的文件能被安全初始化。
|
|
|
|
|
|
- 同名不同路径 `.md` 不冲突。
|
|
|
|
|
|
|
|
|
|
|
|
### C2. 本地 `.mnote` 元数据目录
|
|
|
|
|
|
|
|
|
|
|
|
- [x] 定义 `.mnote/page-ids.json`。
|
|
|
|
|
|
- [x] 定义 `.mnote/page-options.json`。
|
|
|
|
|
|
- [x] 定义 `.mnote/trash` 或系统 trash 策略。
|
|
|
|
|
|
- [x] `.mnote` 不出现在默认 Explorer 展示中,除非开启 debug。
|
|
|
|
|
|
- [x] 元数据写入必须原子化或有失败回滚策略。
|
|
|
|
|
|
|
|
|
|
|
|
**Done 条件:**
|
|
|
|
|
|
|
|
|
|
|
|
- `.mnote` 元数据能支持 page id 和页面设置。
|
|
|
|
|
|
- 默认用户视图不被 `.mnote` 噪音污染。
|
|
|
|
|
|
- 元数据损坏时有可解释错误和恢复建议。
|
|
|
|
|
|
|
|
|
|
|
|
## 4. Phase D:统一 Explorer P0 交互
|
|
|
|
|
|
|
|
|
|
|
|
### D1. Active / Selection / Focus 分离
|
|
|
|
|
|
|
|
|
|
|
|
- [x] 统一 row state:`activeRowId`、`selectedRowIds`、`focusedRowId`。
|
|
|
|
|
|
- [x] active 表示右侧打开资源。
|
|
|
|
|
|
- [x] selection 表示 Explorer 操作集合。
|
|
|
|
|
|
- [x] focus 表示键盘焦点。
|
|
|
|
|
|
- [x] local 和 convex 共用 reducer。
|
|
|
|
|
|
- [x] 多选时 selection 视觉优先于 active。
|
|
|
|
|
|
|
|
|
|
|
|
**Done 条件:**
|
|
|
|
|
|
|
|
|
|
|
|
- 截图中的 `index.md` active/focus/selection 歧义被测试覆盖。
|
|
|
|
|
|
- local `.md` 和 Convex `index.md` 使用同一视觉规则。
|
|
|
|
|
|
- selection 不回写 projection。
|
|
|
|
|
|
|
|
|
|
|
|
### D2. 键盘导航
|
|
|
|
|
|
|
|
|
|
|
|
- [x] `ArrowUp/ArrowDown` 移动 focus。
|
|
|
|
|
|
- [x] `ArrowLeft/ArrowRight` 折叠 / 展开 / 父子导航。
|
|
|
|
|
|
- [x] `Home/End` 跳首尾。
|
|
|
|
|
|
- [x] `Enter` 打开 focused row。
|
|
|
|
|
|
- [x] `F2` 进入 inline rename。
|
|
|
|
|
|
- [x] `Delete/Backspace` 触发删除 preflight。
|
|
|
|
|
|
- [x] `Ctrl/Cmd+C` copy。
|
|
|
|
|
|
- [x] `Ctrl/Cmd+X` cut。
|
|
|
|
|
|
- [x] `Ctrl/Cmd+V` paste。
|
|
|
|
|
|
- [x] `Esc` 取消 rename / menu / drag feedback。
|
|
|
|
|
|
|
|
|
|
|
|
**Done 条件:**
|
|
|
|
|
|
|
|
|
|
|
|
- 键盘能力在 runtime contract 中测试,不只散落在 React handler。
|
|
|
|
|
|
- local 和 convex 使用同一组按键浏览器 smoke。
|
|
|
|
|
|
- `cut` 不再缺失。
|
|
|
|
|
|
|
|
|
|
|
|
### D3. 选择与右键
|
|
|
|
|
|
|
|
|
|
|
|
- [x] 单击单选并设置 anchor/focus。
|
|
|
|
|
|
- [x] `Ctrl/Cmd + Click` 切换选中。
|
|
|
|
|
|
- [x] `Shift + Click` 范围选。
|
|
|
|
|
|
- [x] 右键已选区内 row 保留选区。
|
|
|
|
|
|
- [x] 右键未选 row 切换到该 row。
|
|
|
|
|
|
- [x] 空白区点击清空 selection/focus。
|
|
|
|
|
|
|
|
|
|
|
|
**Done 条件:**
|
|
|
|
|
|
|
|
|
|
|
|
- 多选 context menu 不会被右键误清空。
|
|
|
|
|
|
- local/convex 的 selection reducer 共用。
|
|
|
|
|
|
- 过滤可见行变化后 selection normalize 正确。
|
|
|
|
|
|
- 已选区内右键、未选项右键、空白区点击三条路径都必须在真实浏览器中验证。
|
|
|
|
|
|
|
|
|
|
|
|
### D4. Row grid 与 root toolbar
|
|
|
|
|
|
|
|
|
|
|
|
- [x] 固定 indent / twistie / icon / label / decoration / actions 六列。
|
|
|
|
|
|
- [x] section header 使用 `EXPLORER`。
|
|
|
|
|
|
- [x] root row 明确显示 local folder 或 cloud workspace。
|
|
|
|
|
|
- [x] toolbar 至少有新建、上传/导入、刷新、全部折叠、更多。
|
|
|
|
|
|
- [x] toolbar action 由 capability 控制可用性。
|
|
|
|
|
|
|
|
|
|
|
|
**Done 条件:**
|
|
|
|
|
|
|
|
|
|
|
|
- hover action 不改变行高和缩进。
|
|
|
|
|
|
- local root 与 Convex root 在同一 Explorer 框架下展示。
|
|
|
|
|
|
- toolbar 不创建 source-specific UI 分支。
|
|
|
|
|
|
- hover / focus 显示 actions 时,row height、indent、twistie/icon/label 列位置必须有浏览器截图或 DOM 断言。
|
|
|
|
|
|
|
|
|
|
|
|
## 5. Phase E:统一 Preflight 与 Context Menu
|
|
|
|
|
|
|
|
|
|
|
|
### E1. Context menu profile
|
|
|
|
|
|
|
|
|
|
|
|
- [x] root 菜单:新建页面、新建文件夹、上传/导入、刷新、全部折叠。
|
|
|
|
|
|
- [x] md/page 菜单:打开、重命名、复制、剪切、粘贴、移动到、删除、Reveal。
|
|
|
|
|
|
- [x] folder 菜单:新建页面、新建文件夹、粘贴、重命名、删除。
|
|
|
|
|
|
- [x] asset 菜单:打开、重命名、复制、剪切、删除、下载、移动到。
|
|
|
|
|
|
- [x] 多选菜单:复制、剪切、删除、移动到。
|
|
|
|
|
|
- [x] share/publish 只在 Convex capability 存在时出现。
|
|
|
|
|
|
|
|
|
|
|
|
**Done 条件:**
|
|
|
|
|
|
|
|
|
|
|
|
- 菜单项由 rowKind/resourceKind/capability/selection 共同裁剪。
|
|
|
|
|
|
- 不支持的能力有禁用原因或隐藏规则。
|
|
|
|
|
|
- mindmap/table 的“不支持”状态被 capability 表达,而不是散落提示。
|
|
|
|
|
|
|
|
|
|
|
|
### E2. Preflight result UI
|
|
|
|
|
|
|
|
|
|
|
|
- [x] delete preflight 展示影响集合。
|
|
|
|
|
|
- [x] move preflight 展示目标和冲突。
|
|
|
|
|
|
- [x] paste preflight 展示命名冲突策略。
|
|
|
|
|
|
- [x] upload/drop preflight 展示目标是否可写。
|
|
|
|
|
|
- [x] readonly / permission denied / root escape 返回统一错误结构。
|
|
|
|
|
|
|
|
|
|
|
|
**Done 条件:**
|
|
|
|
|
|
|
|
|
|
|
|
- local 和 convex 的 preflight 使用同一 UI 组件展示。
|
|
|
|
|
|
- 失败时不发 execute。
|
|
|
|
|
|
- 危险动作不会静默执行。
|
|
|
|
|
|
|
|
|
|
|
|
## 6. Phase F:Local 写操作 P1
|
|
|
|
|
|
|
|
|
|
|
|
### F1. 新建
|
|
|
|
|
|
|
|
|
|
|
|
- [x] `tree.node.create` 在 local source 创建 `.md`。
|
|
|
|
|
|
- [x] `tree.folder.create` 或等价命令创建目录。
|
|
|
|
|
|
- [x] 新建后进入 inline rename。
|
|
|
|
|
|
- [x] 命名冲突走 incremental naming 或 preflight 提示。
|
|
|
|
|
|
- [x] Convex 对 `New Folder` 按 capability 禁用,除非已有 folder resource。
|
|
|
|
|
|
|
|
|
|
|
|
**Done 条件:**
|
|
|
|
|
|
|
|
|
|
|
|
- 同一个新建入口可在 local 创建文件,在 Convex 创建 document。
|
|
|
|
|
|
- Convex 不支持 folder 时不会显示假入口。
|
|
|
|
|
|
- 新建结果通过 projection resync 出现。
|
|
|
|
|
|
|
|
|
|
|
|
### F2. Inline rename
|
|
|
|
|
|
|
|
|
|
|
|
- [x] 本地文件 / 目录 rename 写入文件系统。
|
|
|
|
|
|
- [x] `.md` rename 保持 page id。
|
|
|
|
|
|
- [x] Convex document rename 继续走 `tree.node.rename`。
|
|
|
|
|
|
- [x] asset rename 收口到 `tree.resource.rename` 或等价正式命令。
|
|
|
|
|
|
- [x] `Enter` 提交,`Esc` 取消。
|
|
|
|
|
|
- [x] 文件型资源默认选中文件名 stem。
|
|
|
|
|
|
|
|
|
|
|
|
**Done 条件:**
|
|
|
|
|
|
|
|
|
|
|
|
- 不再使用 `prompt` 做重命名。
|
|
|
|
|
|
- local 和 convex 的 rename UI 完全一致。
|
|
|
|
|
|
- rename 失败不破坏 selection/focus。
|
|
|
|
|
|
|
|
|
|
|
|
### F3. Move / Copy / Cut / Paste
|
|
|
|
|
|
|
|
|
|
|
|
- [x] local move/copy 使用文件系统操作。
|
|
|
|
|
|
- [x] Convex move/copy 使用 existing tree/resource command。
|
|
|
|
|
|
- [x] cut 状态有 decoration。
|
|
|
|
|
|
- [x] paste 成功后清空 cut。
|
|
|
|
|
|
- [x] 多选时过滤父子重复选择。
|
|
|
|
|
|
- [x] 禁止 move 到自身或后代。
|
|
|
|
|
|
|
|
|
|
|
|
**Done 条件:**
|
|
|
|
|
|
|
|
|
|
|
|
- local/convex 都通过同一 clipboard payload。
|
|
|
|
|
|
- cut/copy/paste 浏览器 smoke 覆盖两个 source。
|
|
|
|
|
|
- projection resync 后 selection/focus 合理保留。
|
|
|
|
|
|
|
|
|
|
|
|
### F4. Delete / Restore / Purge
|
|
|
|
|
|
|
|
|
|
|
|
- [x] local delete 进入 `.mnote/trash` 或系统 trash,策略需冻结。
|
|
|
|
|
|
- [x] local restore 从 trash 恢复。
|
|
|
|
|
|
- [x] local purge 永久删除。
|
|
|
|
|
|
- [x] Convex delete/restore/purge 继续走 `tree.node.*` / `tree.resource.*`。
|
|
|
|
|
|
- [x] asset delete/restore 从 legacy `/api/media/batch` 收口。
|
|
|
|
|
|
|
|
|
|
|
|
**Done 条件:**
|
|
|
|
|
|
|
|
|
|
|
|
- 删除不是直接 `rm` 静默执行。
|
|
|
|
|
|
- restore 在 local 和 Convex 都有统一菜单语义。
|
|
|
|
|
|
- purge 有更强确认。
|
|
|
|
|
|
|
|
|
|
|
|
### F5. Drop 外部文件
|
|
|
|
|
|
|
|
|
|
|
|
- [x] local drop 文件复制进目标目录。
|
|
|
|
|
|
- [x] Convex drop 文件上传对象存储。
|
|
|
|
|
|
- [x] drop 到 readonly 目标被拒绝。
|
|
|
|
|
|
- [x] drop 到 md 文件时目标归属规则明确。
|
|
|
|
|
|
- [x] before/inside/after indicator 覆盖本地目录和 Convex document/asset-folder。
|
|
|
|
|
|
|
|
|
|
|
|
**Done 条件:**
|
|
|
|
|
|
|
|
|
|
|
|
- 外部拖入在 local 和 convex 使用同一 drop preflight UI。
|
|
|
|
|
|
- 非法目标不执行写入。
|
|
|
|
|
|
- 文件写入后 projection 自动刷新。
|
|
|
|
|
|
- 外部文件 drop、内部拖拽、before/inside/after indicator、hover auto expand 都必须在真实浏览器中验收,不能只靠 contract test 或 jsdom。
|
|
|
|
|
|
- 至少覆盖自拖自身、拖到后代、跨 workspace、只读目标四类非法拖拽场景。
|
|
|
|
|
|
|
|
|
|
|
|
**验收记录:**
|
|
|
|
|
|
|
|
|
|
|
|
- local 外部 drop:`scripts/task163-local-folder-unified-tree-browser-smoke.js` 使用真实 Chromium `DataTransfer/File`,覆盖取消 preflight 不写入、确认后复制进目录、drop 到 `.md` 时写入父目录、readonly 目标拒绝且不发 execute。
|
2026-05-16 08:57:40 +08:00
|
|
|
|
- Convex 外部 drop:同一 smoke 通过 Rust DOM host 监听 `tree.filetree.external-drop`,覆盖 Convex document 与 asset-folder 两类目标;宿主侧继续由 `tree-shell-dom-host.tsx -> Sidebar.handleResourcePaneDropFiles -> uploadFileTreeResourceAsset -> /api/media/upload` 上传对象存储。历史 `tree-shell-iframe-host.tsx` 已于 2026-05-16 移入 `recycle/`,不再作为 active 路径。
|
2026-05-08 00:41:03 +08:00
|
|
|
|
- 对象存储链路:`resource-command-client.test.ts` 覆盖 `uploadFileTreeResourceAsset`,`app/api/media/upload/route.test.ts` 覆盖 `tree.resource.upload` plan、Convex upload URL、`mediaAssets:createWithStorage` 与资源 delta artifact。
|
|
|
|
|
|
- indicator:同一 smoke 覆盖本地目录 before/inside/after、hover auto expand,以及 Convex document before/inside/after 与 asset-folder inside。
|
|
|
|
|
|
|
|
|
|
|
|
## 7. Phase G:Watcher / Realtime / Refresh
|
|
|
|
|
|
|
|
|
|
|
|
### G1. Local file watcher
|
|
|
|
|
|
|
|
|
|
|
|
- [x] 监听 root 下文件新增、删除、重命名、修改。
|
|
|
|
|
|
- [x] 忽略 `.git/node_modules/.mnote/trash` 等目录。
|
|
|
|
|
|
- [x] 防抖合并事件。
|
|
|
|
|
|
- [x] source 输出 tree delta 或触发 snapshot refresh。
|
|
|
|
|
|
- [x] 写操作自身产生的 watcher 事件不造成重复刷新风暴。
|
|
|
|
|
|
|
|
|
|
|
|
**Done 条件:**
|
|
|
|
|
|
|
|
|
|
|
|
- 外部编辑器新增 `.md` 后 page_tree 自动更新。
|
|
|
|
|
|
- 外部删除文件后 Explorer 自动更新。
|
|
|
|
|
|
- watcher 不越过 root。
|
|
|
|
|
|
|
|
|
|
|
|
**验收记录:**
|
|
|
|
|
|
|
|
|
|
|
|
- Rust 侧新增 `/api/tree/local-folder-watch`,由 `LocalFolderSource` 计算 root revision,只在可见文件/目录变化时变更 revision,并复用 root escape 与 ignore 规则。
|
|
|
|
|
|
- 浏览器端 local source 不再固定 2.5 秒 reload,而是轮询 revision,变化后防抖触发 snapshot refresh。
|
|
|
|
|
|
- `cargo test -p mnote-web local_ -- --nocapture` 覆盖 `.mnote` 噪音不改变 revision、可见文件内容变更会改变 revision、root escape 防护。
|
|
|
|
|
|
- `scripts/task163-local-folder-unified-tree-browser-smoke.js` 覆盖外部新增 `.md` 后 page_tree 自动更新,以及外部新增/删除普通文件后 Explorer 自动更新。
|
|
|
|
|
|
|
|
|
|
|
|
### G2. Convex realtime parity
|
|
|
|
|
|
|
|
|
|
|
|
- [x] Convex 继续使用现有 tree stream。
|
|
|
|
|
|
- [x] preferred snapshot 选择不因 local source 破坏。
|
|
|
|
|
|
- [x] local source 和 Convex source 都能进入统一 tree event consumer。
|
|
|
|
|
|
- [x] source 切换时清理上一 source 的 watcher / stream。
|
|
|
|
|
|
|
|
|
|
|
|
**Done 条件:**
|
|
|
|
|
|
|
|
|
|
|
|
- local 和 convex 的实时刷新状态都可观测。
|
|
|
|
|
|
- 切换 workspace 不出现旧 source 事件污染新 workspace。
|
|
|
|
|
|
- 当前 Convex smoke 不回退。
|
|
|
|
|
|
|
|
|
|
|
|
**验收记录:**
|
|
|
|
|
|
|
|
|
|
|
|
- Convex 侧继续由 `useSidebarTreeStream` / tree stream server 处理 realtime,`pnpm vitest run src/lib/tree-stream/use-sidebar-tree-stream.test.tsx src/components/sidebar/use-preferred-sidebar-snapshot.test.tsx src/lib/tree-stream/server.test.ts` 通过。
|
|
|
|
|
|
- local 侧通过同一 tree shell source metadata 和 revision watcher 触发 snapshot refresh,不引入第二套 UI。
|
|
|
|
|
|
- 真实浏览器 smoke 覆盖 local -> Convex source 切换,断言旧 local watcher 内容和 selection 不污染 Convex file tree。
|
|
|
|
|
|
|
|
|
|
|
|
## 8. Phase H:Markdown 编辑与写回 P2
|
|
|
|
|
|
|
|
|
|
|
|
### H1. Markdown AST 到编辑器文档
|
|
|
|
|
|
|
|
|
|
|
|
- [x] Markdown parse 成 editor document。
|
|
|
|
|
|
- [x] frontmatter 保留。
|
|
|
|
|
|
- [x] 基础块覆盖 paragraph / heading / list / quote / code / divider。
|
|
|
|
|
|
- [x] 附件链接解析为 resource reference。
|
|
|
|
|
|
- [x] 不支持块有降级显示。
|
|
|
|
|
|
|
|
|
|
|
|
**Done 条件:**
|
|
|
|
|
|
|
|
|
|
|
|
- 本地 `.md` 可进入可编辑态。
|
|
|
|
|
|
- 基础 Markdown 往返不丢主体内容。
|
|
|
|
|
|
- 不支持格式不会导致保存破坏原文件。
|
|
|
|
|
|
|
|
|
|
|
|
**验收记录:**
|
|
|
|
|
|
|
|
|
|
|
|
- `LocalFolderSource` 的 Markdown 读取侧已覆盖 heading 1-6、paragraph、bullet / numbered list、quote、fenced code、divider、相对附件链接 `media` reference;未知/不支持语法降级为 paragraph。
|
|
|
|
|
|
- `cargo test -p mnote-web local_markdown -- --nocapture` 覆盖基础块解析、附件链接解析、unsupported table 降级显示。
|
|
|
|
|
|
- 真实浏览器 smoke 打开 `docs/blocks.md`,断言 frontmatter title、列表、quote、code 与附件链接文本均可在文档页读出。
|
|
|
|
|
|
|
|
|
|
|
|
### H2. 编辑器文档写回 Markdown
|
|
|
|
|
|
|
|
|
|
|
|
- [x] 保存正文写回 `.md`。
|
|
|
|
|
|
- [x] 标题编辑规则冻结:文件名 / H1 / frontmatter 如何同步。
|
|
|
|
|
|
- [x] 页面设置写入 frontmatter 或 `.mnote/page-options.json`。
|
|
|
|
|
|
- [x] 保存失败保留编辑器状态并显示错误。
|
|
|
|
|
|
- [x] watcher 与保存事件去重。
|
|
|
|
|
|
|
|
|
|
|
|
**Done 条件:**
|
|
|
|
|
|
|
|
|
|
|
|
- 修改本地 `.md` 后刷新仍能看到新内容。
|
|
|
|
|
|
- 文件名、H1、frontmatter title 不互相打架。
|
|
|
|
|
|
- Convex 编辑链路不受本地写回影响。
|
|
|
|
|
|
|
|
|
|
|
|
**验收记录:**
|
|
|
|
|
|
|
|
|
|
|
|
- 正文保存通过 `/api/documents/save` 写回 `.md`,标题编辑通过 `/api/documents/title` 只写 frontmatter `title`,文件名变更仍由 Explorer rename 负责;三者职责分离。
|
|
|
|
|
|
- 页面设置写入 `.mnote/page-options.json`,不混入正文。
|
|
|
|
|
|
- `cargo test -p mnote-web local_ -- --nocapture` 覆盖本地正文、标题和页面设置写回;`local_markdown_save_preserves_frontmatter_and_writes_basic_blocks` 覆盖 frontmatter 保留与基础块 Markdown 输出。
|
|
|
|
|
|
- 真实浏览器 smoke 通过 document APIs 保存本地 Markdown,刷新后仍能看到新标题和正文;revision watcher 只在本地 root revision 变化后防抖刷新,避免固定刷新风暴。
|
|
|
|
|
|
|
|
|
|
|
|
## 9. Phase I:Local / Cloud 显式桥接 P3
|
|
|
|
|
|
|
|
|
|
|
|
### I1. Import / Export
|
|
|
|
|
|
|
|
|
|
|
|
- [x] local folder import 到 cloud workspace。
|
|
|
|
|
|
- [x] cloud workspace export 到 local folder。
|
|
|
|
|
|
- [x] 冲突策略明确。
|
|
|
|
|
|
- [x] page id 映射保留。
|
|
|
|
|
|
- [x] 附件路径和对象存储映射保留。
|
|
|
|
|
|
|
|
|
|
|
|
**Done 条件:**
|
|
|
|
|
|
|
|
|
|
|
|
- import/export 是显式动作,不是 local 打开时自动发生。
|
|
|
|
|
|
- 用户可预览影响集合。
|
|
|
|
|
|
- 失败可重试或回滚。
|
|
|
|
|
|
|
|
|
|
|
|
**验收记录:**
|
|
|
|
|
|
|
|
|
|
|
|
- P3 候选合同已冻结在 `4-23-local-cloud-explicit-bridge-p3-candidate-v1.md`。
|
|
|
|
|
|
- import/export 均被定义为 `preview -> execute` 显式动作,不能在打开 local folder、保存 local `.md`、watcher 或普通树操作中隐式发生。
|
|
|
|
|
|
- 冲突策略固定为 preview 输出冲突集合,execute 默认 incremental naming,不静默覆盖。
|
|
|
|
|
|
- page id 映射分别通过 `mnote_id` / `.mnote/page-ids.json`、`cloud_document_id`、`.mnote/cloud-export-map.json` 保留。
|
|
|
|
|
|
- 附件路径和对象存储 id 必须进入 export/import map;execute 失败必须返回 retry cursor 和 rollback plan。
|
|
|
|
|
|
|
|
|
|
|
|
### I2. Publish / Sync 候选
|
|
|
|
|
|
|
|
|
|
|
|
- [x] local page publish 到 Convex public page。
|
|
|
|
|
|
- [x] 手动 sync 候选设计。
|
|
|
|
|
|
- [x] 不在 P0/P1 引入隐式双向同步。
|
|
|
|
|
|
- [x] 权限模型不混淆:local OS 权限不同于 Convex ACL。
|
|
|
|
|
|
|
|
|
|
|
|
**Done 条件:**
|
|
|
|
|
|
|
|
|
|
|
|
- 本地 workspace 不依赖 Convex 才能工作。
|
|
|
|
|
|
- Cloud 功能通过显式桥接启用。
|
|
|
|
|
|
- 没有自动双真相。
|
|
|
|
|
|
|
|
|
|
|
|
**验收记录:**
|
|
|
|
|
|
|
|
|
|
|
|
- `4-23` 固定 publish 为 `tree.bridge.publish.preview/execute`,只发布显式选择的单页或子树。
|
|
|
|
|
|
- sync 只进入 `tree.bridge.sync.preview` 候选,不提供默认 `sync.execute`,避免形成自动双真相。
|
|
|
|
|
|
- local OS 权限与 Convex ACL 在 preview 中必须单独展示,不互相推导。
|
|
|
|
|
|
- 当前实现中的 local folder 读写、watcher、Markdown 保存均不依赖 Convex;Convex 能力只通过显式 bridge 候选启用。
|
|
|
|
|
|
|
|
|
|
|
|
## 10. 全局验收矩阵
|
|
|
|
|
|
|
|
|
|
|
|
### 10.1 Contract tests
|
|
|
|
|
|
|
|
|
|
|
|
- [x] `WorkspaceSource` mock local / mock convex。
|
|
|
|
|
|
- [x] command envelope source routing。
|
|
|
|
|
|
- [x] projection source metadata。
|
|
|
|
|
|
- [x] capability-driven menu profile。
|
|
|
|
|
|
- [x] preflight result schema。
|
|
|
|
|
|
- [x] active / selection / focus reducer。
|
|
|
|
|
|
- [x] cut / copy / paste reducer。
|
|
|
|
|
|
|
|
|
|
|
|
**Done 条件:**
|
|
|
|
|
|
|
|
|
|
|
|
- 不启动浏览器也能验证核心协议。
|
|
|
|
|
|
- local/convex 至少各有一套同命令不同 executor 的测试。
|
|
|
|
|
|
- Contract tests 只负责协议、schema、reducer、payload 与 executor 分发,不替代拖拽、右键、多选、外部 drop、source 切换的真实浏览器验收。
|
|
|
|
|
|
|
|
|
|
|
|
### 10.2 Rust tests
|
|
|
|
|
|
|
|
|
|
|
|
- [x] local folder scan。
|
|
|
|
|
|
- [x] md-only page_tree。
|
|
|
|
|
|
- [x] file_tree folder first sorting。
|
|
|
|
|
|
- [x] page id persistence。
|
|
|
|
|
|
- [x] root escape 防护。
|
|
|
|
|
|
- [x] local write executor。
|
|
|
|
|
|
- [x] Convex executor regression。
|
|
|
|
|
|
|
|
|
|
|
|
**Done 条件:**
|
|
|
|
|
|
|
|
|
|
|
|
- Rust 测试覆盖 source adapter 主路径。
|
|
|
|
|
|
- 安全边界有失败用例。
|
|
|
|
|
|
- Convex 旧路径没有被本地 source 破坏。
|
|
|
|
|
|
|
|
|
|
|
|
### 10.3 Browser smoke
|
|
|
|
|
|
|
|
|
|
|
|
- [x] 打开 local folder。
|
|
|
|
|
|
- [x] 3000 `desktop:hot` 主入口覆盖打开本地文件夹:不触发原生 prompt,不跳 `/tree`,不落入 `legacy_next_compat_disabled`。
|
|
|
|
|
|
- [x] Explorer 显示目录、md、普通文件。
|
|
|
|
|
|
- [x] 页面树只显示 md。
|
|
|
|
|
|
- [x] 打开 md 到文档页。
|
|
|
|
|
|
- [x] 同一套右键菜单在 local / convex 按 capability 裁剪。
|
|
|
|
|
|
- [x] 已选区内右键保留多选,未选项右键切换 selection 后开菜单,空白区点击清空 selection / focus。
|
|
|
|
|
|
- [x] `Ctrl/Cmd+Click`、`Shift+Click`、`Ctrl/Cmd+A` 多选链路。
|
|
|
|
|
|
- [x] 3000 主 Sidebar file tree 的 `Shift+Click` 范围多选由 `scripts/task164-desktop-hot-local-folder-main-entry-smoke.js` 验收。
|
|
|
|
|
|
- [x] inline rename。
|
|
|
|
|
|
- [x] `Ctrl/Cmd+C/X/V` 与菜单 Copy / Cut / Paste 都覆盖,cut decoration 出现,paste 成功后消失。
|
|
|
|
|
|
- [x] 内部拖拽覆盖单选、多选、move / copy 修饰键切换。
|
|
|
|
|
|
- [x] 内部拖拽覆盖 before / inside / after 三态 indicator。
|
|
|
|
|
|
- [x] 内部拖拽覆盖 hover auto expand。
|
|
|
|
|
|
- [x] 内部拖拽覆盖自拖自身、拖到后代、跨 workspace、只读目标四类非法场景,并断言不会发 execute。
|
|
|
|
|
|
- [x] 外部文件 drop 使用真实 `DataTransfer/File` 或等价 Playwright 文件拖入,覆盖 local folder、Convex document/asset-folder、readonly 目标、md 文件目标。
|
|
|
|
|
|
- [x] 外部文件 drop 覆盖 preflight UI、写入/上传结果、projection refresh。
|
|
|
|
|
|
- [x] watcher 外部修改刷新。
|
|
|
|
|
|
- [x] 切回 Convex workspace 后原有页面树正常。
|
|
|
|
|
|
- [x] source 切换后旧 watcher / stream 已清理,selection / focus / active 不被旧 source 污染。
|
|
|
|
|
|
- [x] local 与 Convex 上同一动作保持同一 UI 和同一命令名,但 executor 行为可不同。
|
|
|
|
|
|
- [x] 组合场景覆盖多同名 `新页面`、每页下 `index.md`、多选、右键、`F2`、`Ctrl/Cmd+X/V`、外部文件拖入、切回 Convex workspace。
|
|
|
|
|
|
|
|
|
|
|
|
**Done 条件:**
|
|
|
|
|
|
|
|
|
|
|
|
- smoke 覆盖 source 切换。
|
|
|
|
|
|
- smoke 覆盖 3000 主入口本地文件夹链路;不能只用 `/tree` debug shell 作为完成证据。
|
|
|
|
|
|
- smoke 覆盖至少一个同 UI、同命令、不同 executor 的动作。
|
|
|
|
|
|
- 截图中 Explorer root / active / selection / focus 可解释。
|
|
|
|
|
|
- 浏览器 smoke 必须是真实浏览器自动化,不接受纯 jsdom、DOM 单测或只跑 reducer 的结果。
|
|
|
|
|
|
- 拖拽、外部 drop、上下文菜单、hover 和键盘导航必须在浏览器里触发真实事件链,不接受手工只看截图的验收。
|
|
|
|
|
|
|
|
|
|
|
|
## 11. 最终 done 条件
|
|
|
|
|
|
|
|
|
|
|
|
只有同时满足以下条件,`4-20/4-21/4-22` 才能整体移动到 `done/`:
|
|
|
|
|
|
|
|
|
|
|
|
- [x] `WorkspaceSource` 抽象落地。
|
|
|
|
|
|
- [x] `LocalFolderSource` 支持本地只读 `file_tree/page_tree/page_aggregate`。
|
|
|
|
|
|
- [x] `ConvexWorkspaceSource` 继续通过同一 projection/command 消费路径工作。
|
|
|
|
|
|
- [x] Explorer P0 交互完成:active/selection/focus、keyboard、context menu、inline rename、cut/copy/paste、drag/drop。
|
|
|
|
|
|
- [x] 至少一组写操作在 local 和 convex 上共用 UI 和 command envelope。
|
|
|
|
|
|
- [x] 本地文件夹不经过 Convex 中介。
|
|
|
|
|
|
- [x] page id 不因本地 rename/move 断裂。
|
|
|
|
|
|
- [x] UI 没有 source-specific tree fork。
|
|
|
|
|
|
- [x] 所有危险写操作都有 preflight。
|
|
|
|
|
|
- [x] 浏览器 smoke 和 Rust/TS contract tests 通过。
|
|
|
|
|
|
- [x] 3000 `desktop:hot` 主入口 smoke 通过,覆盖本地文件夹入口、legacy fallback 拒绝和主 Sidebar `Shift+Click` 多选。
|
|
|
|
|
|
- [x] 拖拽、外部 drop、右键、多选、键盘、source 切换的关键路径在真实浏览器中逐项验收通过;这些 hard gate 未通过时,即使 contract/Rust 测试通过也不得移动到 `done/`。
|
|
|
|
|
|
|
|
|
|
|
|
## 12. 明确非目标
|
|
|
|
|
|
|
|
|
|
|
|
- [x] 不在 P0 实现本地和云端自动同步。
|
|
|
|
|
|
- [x] 不在 P0 承诺 Markdown 与编辑器完全无损互转。
|
|
|
|
|
|
- [x] 不用本地 per-user 文件夹替代 Convex 协作权限。
|
|
|
|
|
|
- [x] 不把 VSCode 源码作为直接复制对象;只参考 Explorer 行为模型。
|
|
|
|
|
|
- [x] 不把 Convex 虚拟空间强行伪装成真实磁盘目录。
|
|
|
|
|
|
- [x] 不把 Local Folder 模式做成必须登录才能使用。
|