# 07 VSCode Explorer 文件树与垃圾箱闭环增量审查

> 更新时间：2026-05-15
> 状态：done
> 范围：`04-tree-domain` 中 File Tree / Page Tree / Resource Tree 与垃圾箱、删除、恢复、永久删除、VSCode Explorer 对标相关缺口。
> 结论：本审查 checklist 已闭合。当前可表述为文件树多选、删除进入垃圾箱、资源生命周期、恢复位置、双浏览器 no-refresh、本地 folder watcher 与本地非 md 资源 trash baseline 已完成最低可用闭环；但 VSCode Explorer 对标仍应按“最低可用完成 / parity backlog”分层，不应把禁用态或后续增强项描述为完整 parity。

## 1. 本轮判断

- `design/04-tree-domain/done/4-12-*` 与 `4-14-*` 的 done 口径是 Page Tree / File Tree initial DOM、hydration、row contract、selection state family 与能力保留完成，不等于 final DOM shell、VSCode Explorer 体验或垃圾箱产品闭环完成。
- `design/04-tree-domain/done/4-20-vscode-explorer-file-tree-alignment-v1.md` 已经把 VSCode Explorer 对标列为 done，但文档自身仍明确写着资源 `rename/delete/restore` 不能长期停留在 legacy `/api/media/batch`，需要 `tree.resource.rename/archive/restore` 或等价正式命令。
- 当前 `wolai-frontend/src/components/sidebar/sidebar.tsx` 已经存在文件树多选删除和垃圾桶 Drawer，但仍以页面、附件、mindmap、table 的分散 route 组合闭环；这更像“可用基础”，不是统一 tree-first command 产品面。
- 初始审查时 `rust/crates/mnote-web/src/ssr/pages/layout.rs` 中“删除到垃圾桶”上下文菜单实际 dispatch `action: 'purge'`，这是已在阶段 1 止血的最高优先级语义风险；后续仍需防回归。

## 2. 主要缺口

### F-01 P0：Rust Web 主壳“删除到垃圾桶”实际触发永久删除

初始证据：

- `rust/crates/mnote-web/src/ssr/pages/layout.rs` 的 `delete-trash` 分支显示确认文案“删除到垃圾桶”，但 dispatch `action: 'purge'`。
- `wolai-frontend/src/app/api/tree/commands/route.ts` 中 `purge` 对应 `tree.node.purge`，语义是永久删除。

当前状态：

- 阶段 1 已改为 dispatch `archive`，并由 `sidebar_tree_delete_to_trash_dispatches_archive_not_purge` 与真实浏览器 smoke 防回归。

影响：

- 用户以为页面进入垃圾桶，实际可能走永久删除。
- 这会破坏“默认 Delete = archive / trash，永久删除只在 Trash 工作台强确认”的 VSCode Explorer 基线。

应执行：

- `delete-trash` 必须改为 `archive` / `tree.node.archive`。
- `purge` 只能从垃圾箱中的“彻底删除 / 清空垃圾箱”入口触发，并保留更强确认。

### F-02 P0：垃圾箱产品面存在，但 `/trash` 与 3000 主壳口径未闭合

证据：

- `wolai-frontend/src/components/sidebar/sidebar.tsx` 已有 `Drawer` 垃圾桶，支持页面 / 附件 tab、搜索、恢复、彻底删除、清空。
- `rust/crates/mnote-web/src/workspace_shell.rs` 暴露 footer entry：`href: "/trash"`。
- 当前只检索到 `/api/*/empty-trash`，未看到完整 `wolai-frontend/src/app/trash` 或 Rust `/trash` 工作台页面。

影响：

- Sidebar Drawer 可用不等于全局 Trash 工作台可用。
- workspace shell 的“垃圾箱”链接可能成为空入口或体验断点。

应执行：

- 明确产品口径：垃圾箱是 Sidebar Drawer、独立 `/trash` 页面，还是两者都有。
- 若保留 `/trash`，必须有真实路由、真实数据、恢复、彻底删除、清空和浏览器验收。
- 若只保留 Drawer，应移除或改写 `/trash` 链接，避免入口误导。

### F-03 P0：删除默认语义必须先进入垃圾箱，永久删除必须只从垃圾箱触发

证据：

- 页面删除在 `deleteDocumentCommand` 主链中已经走 `archive`，Convex `documents.softDelete` 会写 `deleted_at`。
- 但 Rust Web 主壳存在 `delete-trash -> purge` 反向危险。
- 附件、mindmap、table 删除链分别走 `/api/media/batch`、`/api/mindmap/:doc/:id`、`/api/tables/:id` 等分散 route。

影响：

- 同一个“删除”动作在不同入口、不同资源类型下语义不一致。
- 用户无法建立稳定心智：删除后是否能恢复，永久删除是否真的同步清理 Convex。

应执行：

- 统一命名：默认删除 = `archive` / `trash`；彻底删除 = `purge`。
- 所有入口的确认文案、commandName、route、Convex mutation 必须同义。
- 删除后必须能在垃圾箱中看到对象，恢复后回到 projection，彻底删除后 Convex 查询不到对象。

### F-04 P1：文件树多选基础已存在，但页面树与批量命令闭环不足

证据：

- `wolai-frontend/src/lib/file-tree/selection.ts` 与 `tree_shell/filetree_selection.rs` 已支持普通点击、Ctrl/Cmd 多选、Shift 范围、多选 toggle、右键保留选区。
- `wolai-frontend/src/components/sidebar/tree-shell-dom-host.tsx` 会把 Delete / Backspace 转给 `onFileTreeDeleteSelection`。
- `wolai-frontend/src/components/sidebar/sidebar.tsx` 的批量页面删除是逐个 `deleteDocumentCommand`，不是一个原子批量 tree command。
- 页面树没有同等级多选模型，主要是 focus / active / 单节点 context menu。

影响：

- 用户指出的“多选删除”在文件树有基础，但没有形成 VSCode Explorer 式跨入口一致能力。
- 批量删除部分失败时会出现部分已删、部分未删，缺少统一 plan / rollback / result summary。

应执行：

- 文件树先补真实浏览器 smoke：Ctrl/Cmd 多选、Shift 范围、右键已选项保持 selection、Delete 批量删除。
- 批量删除应进入统一 preflight plan，至少对父子重复选择去重，并返回可解释的 partial failure。
- 页面树是否需要多选要明确产品口径；若 page tree 也承担 Explorer 行为，应补 selection state family。

### F-05 P1：资源级 `tree.resource.*` 未收口

证据：

- `4-20` 明确写到资源 `copy/move/upload` 已有 `tree.resource.copy/move/upload`，但 `rename/delete/restore` 仍经过 legacy `/api/media/batch`，需要 `tree.resource.rename/archive/restore` 或等价 bridge。
- 当前前端 `handleDeleteAssets` 对 mindmap、table、file asset 分别调用不同 route，并在删除后 `refreshTree()`。

影响：

- File Tree / Resource Tree 仍有第二套资源命令真相。
- 后续 07-ai 若依赖资源树状态，容易再次绕过 tree-first command。

应执行：

- 定义并落地 `tree.resource.archive`、`tree.resource.restore`、`tree.resource.purge`、`tree.resource.rename`，或明确等价 bridge 命名。
- legacy `/api/media/batch` 只能作为兼容 alias，不应是主路径。
- command event 与 projection resync 必须覆盖资源恢复、永久删除、重命名。

### F-06 P1：垃圾箱清空需要跨 documents / media / mindmap / table 一致验收

证据：

- 当前存在 `/api/documents/empty-trash`、`/api/media/empty-trash`、`/api/mindmap-trash/empty`、`/api/tables/empty-trash`。
- Sidebar 中“页面垃圾桶”和“附件垃圾桶”分别清空：页面只调 documents；附件 tab 并发清空 media、mindmap、table。

影响：

- 用户预期的“垃圾箱删除后 Convex 同步删除”必须跨对象类型验收，而不是只验收页面。
- 如果多个 API 中任一失败，可能出现计数、projection 与 Convex 数据不一致。

应执行：

- 清空垃圾箱需要明确是否是一键全量清空，还是按页面 / 附件 tab 分区清空。
- 验收必须覆盖 Convex `documents`、`media_assets`、`mindmaps`、`document_tables` 以及页面关联数据清理。
- 对 Storage / OCR / LightRAG / 引用 / 最近访问 / 收藏等关联清理给出确认边界。

### F-07 P1：恢复位置、排序与子树还原规则不足

证据：

- 子代理审查指出 Convex 页面 restore 会把根节点 `parent_id` 置为 `null`，子节点按同一 `deleted_at` 级联恢复；原父位置 / 排序不一定完整还原。

影响：

- “恢复”只能证明对象复活，不等于恢复到删除前位置。
- 对文件树体验来说，恢复后 reveal/select/focus 也属于闭环的一部分。

应执行：

- 删除前记录原 parent / order / source view / expanded path，恢复后尽量还原。
- 如果父节点已不存在，应有明确 fallback：恢复到根、恢复到最近有效父、或提示用户选择位置。
- 恢复后必须 reveal 并选中对象，避免用户以为恢复失败。

### F-08 P1：VSCode Explorer 的 cut / inline rename / context menu 命令面未完全对齐

证据：

- 文件树键盘层会产生 `cutSelection` reducer 事件，但全局粘贴链主要读取 `action: "copy"` 的剪贴板写入，cut-move paste 未完整确认。
- F2 目前更像触发 `beginRenameFocused`，实际重命名仍大量依赖右键菜单 / prompt，不是 VSCode 式 inline rename。
- 右键菜单缺少或未统一：Cut、Paste Into、New File、New Folder、Refresh 单节点、Collapse All、Reveal、Copy Path / Relative Path 等 Explorer 常见项。

影响：

- 当前“对标 VSCode Explorer”容易被误读成完成全部 Explorer 产品能力。
- 07-ai 若把这些能力当成已有，会在自动操作、上下文菜单和 keyboard workflow 中遇到断点。

应执行：

- P1 先收口 Cut / Paste move、F2 inline rename、右键命令可用性。
- P2 再补 Reveal、Collapse All、Copy Path、Refresh 单节点等效率项。
- 不应照搬 VSCode 文件系统 provider 复杂度，但应照搬 command context 和 selection/focus 规则。

### F-09 P1：本地文件夹与 Convex 不是同一 executor，watch 范围也不一致

证据：

- Rust Web `tree.rs` 支持 `/api/tree/commands`，但 `local_folder` 分支走独立 `execute_local_tree_command`。
- 本地 watcher 当前主要监听 markdown 路径事件，未覆盖非 md 资源文件的完整资源树变更。
- `4-22` 已把 Local Folder / Convex 统一 source 大范围标为完成，但 `4-23` import/export/publish/sync 候选合同仍在 `process/`。

影响：

- “Local Folder 与 Convex 共用一套 projection / command / preflight / runtime / UI”仍需要拆成已完成和待核验两层。
- 普通附件、OnlyOffice、mindmap、本地资源文件的 Trash / restore / purge 不能默认等同于 markdown page trash。

应执行：

- 10-review 口径中明确：local markdown/page trash baseline 已有，完整 Resource Tree / File Tree Trash 待核验。
- 本地资源文件 watch、trash、restore、purge 需要单独验收。

### F-10 P2：实时刷新仍混合 refetch，缺少 no-refresh 级回归验收

证据：

- Sidebar 删除资产后仍显式 `refreshTree()`。
- 当前同时存在 Convex live/query、tree SSE、手动 refetch、本地 watcher 多条刷新链。

影响：

- “刷新浏览器才能看到变化”类问题容易复发。
- 局部 optimistic update、server delta、manual refetch 之间没有统一验收矩阵。

应执行：

- 保留手动 refetch 作为兜底，但主验收必须是无浏览器刷新可见。
- 覆盖双浏览器：A 删除 / 恢复 / 清空，B 通过 Convex live 或 tree stream 自动更新。

### F-11 P1：F2 inline rename 仍缺 VSCode 式行内校验体验

证据：

- VSCode Explorer rename 输入框会做空名、非法名、同级重名等 validation，并在输入框上下文展示错误。
- 当前 filetree 已有 F2 inline rename，但主要是 trim 后提交；失败路径仍可能落到 alert / 后端错误提示，不是行内 validation。
- 2026-05-15 二次修正后，`wolai-frontend` DOM host 已补空名、非法路径字符、同级重名行内阻断和普通附件扩展名保留；Rust SSR srcdoc 路径仍需后续 smoke 核验是否同等级。

影响：

- “能 F2 重命名”不等于 VSCode Explorer rename 体验闭环。
- 07-ai 或自动化操作若只看 input 存在，可能无法在非法名、重名、扩展名策略下获得稳定可解释反馈。

应执行：

- 页面、普通附件、本地 Markdown、本地非 md 资源统一行内校验：空名、非法字符、同级重名、扩展名保留 / 改写策略。
- Enter 不提交非法值；Escape 取消；blur 行为必须明确。
- 错误提示必须出现在 rename input 附近，不能只依赖 alert。

### F-12 P2：右键菜单仍是最低项可见，不是完整 VSCode Explorer command surface

证据：

- 当前已补 `New File / New Folder / Paste Into / Refresh / Collapse All / Copy Path / Reveal` 的最低可见或禁用态。
- `New Folder` 在当前页面树下仍是禁用态；`Paste Into` 已在主 Sidebar 节点右键菜单接入 action target，但空白区右键菜单与 Rust SSR srcdoc 路径仍需后续 smoke / 实现时继续沿同一 target 规则验收。

影响：

- 当前只能说“最低菜单项有口径”，不能说“VSCode Explorer 菜单面已完整对齐”。
- 右键 action target 与键盘 paste target 若不统一，批量选择和右键未选项切换目标会再次出现歧义。

应执行：

- 明确哪些菜单项是长期不做、哪些进入 backlog。
- 至少补 `Copy Relative Path` 的 workspace-relative / local-root-relative 语义和真实复制验收。
- `Paste Into` 应使用右键 action target，并与 Ctrl/Cmd+V 的 focused target 规则写清。

### F-13 P2：`resync_required` 事件合同需要防回归

证据：

- `applyTreeStreamDelta` 对 `resync_required` 本身不直接替换 tree 数据，必须依赖后续 snapshot / resync envelope。
- 阶段 8 smoke 已证明本轮页面、附件、mindmap、table 链路能触发 no-refresh 更新，但这不能单独证明所有未来 `resync_required` 都会配套 resync。

影响：

- 如果后续 route 只写 `resync_required` delta、不写 snapshot / resync，B 浏览器会停留在旧数据。
- 这类问题表面上会再次表现为“必须刷新浏览器才能看到变化”。

应执行：

- 给 tree event stream 增加合同级回归：每个 `resync_required` 必须伴随后续 snapshot / resync，或客户端必须主动拉取新 snapshot。
- 验收断言包括 cursor 顺序、断线重连、重复 resync 去重。
- smoke 结果中继续记录实际事件来源，不把本地 `refreshTree()` 误标为实时同步。

## 3. 按顺序执行的 checklist

### 阶段 1：P0 删除语义止血

- [x] 修正 `rust/crates/mnote-web/src/ssr/pages/layout.rs`：`delete-trash` 必须 dispatch `archive`，不能 dispatch `purge`。
- [x] 补单元测试或字符串级回归：菜单文案“删除到垃圾桶”不得与 `tree.node.purge` 绑定。
- [x] 真实浏览器验证：Rust 3000 主壳右键删除页面后，页面进入垃圾桶，Convex `deleted_at` 非空，未被永久删除。

验收标准：

- Network / bridge log 中默认删除命令为 `tree.node.archive`。
- `tree.node.purge` 只出现在垃圾箱“彻底删除 / 清空”路径。

执行记录：

- 2026-05-15：`delete-trash` 已改为 `archive`；新增 `sidebar_tree_delete_to_trash_dispatches_archive_not_purge` 回归测试。
- 2026-05-15：浏览器 smoke 使用 `TEST-10REVIEW-07-P0-*` 新建页面后右键删除，捕获请求 `action: "archive"`、bridge command `tree.node.archive`、响应 `deleted_at` 非空、同页面 `purge` 请求数为 0。

### 阶段 2：统一垃圾箱入口与产品口径

- [x] 决定 `/trash` 是独立页面还是移除入口；如果保留，补真实 Trash 工作台。
- [x] Sidebar Drawer 与 `/trash` 使用同一 dataset：`trashed_documents`、`trashed_media_assets`、`trashed_mindmap_assets`、`trashed_table_assets`。
- [x] 页面、附件、mindmap、table 均支持恢复、彻底删除、清空。

验收标准：

- 不存在点开“垃圾箱”后空路由或 404。
- 垃圾箱计数与列表一致；恢复 / 彻底删除后计数和列表实时更新。

执行记录：

- 2026-05-15：Rust 3000 主壳已挂载 `GET /trash`，复用 `sidebar.datasetList` 的 `trashed_*` 数据渲染垃圾箱工作台；页面条目提供恢复与彻底删除按钮。
- 2026-05-15：补 `trash_entry_renders_real_workspace_trash_workbench` 路由级回归；浏览器 smoke 访问 `/trash?workspaceId=<active>` 返回 200，出现 `mnote-trash-workbench`、页面区、资源区、恢复按钮和彻底删除按钮。
- 2026-05-15：Rust 3000 主壳已挂载 `POST /api/documents/empty-trash`，补 `documents_empty_trash_route_executes_workspace_trash_purge` 回归；`/trash` 页面已出现“清空页面垃圾箱”按钮，并只读浏览器 smoke 验证按钮存在，未点击清空以避免删除既有垃圾箱数据。
- 2026-05-15：Rust 3000 主壳已新增资源垃圾箱兼容入口：`POST /api/media/batch` restore、`POST /api/media/purge`、`POST /api/media/empty-trash`、`PATCH /api/mindmap/{docId}/{mindmapId}` restore / purge、`POST /api/mindmap-trash/empty`、`POST /api/tables/restore`、`POST /api/tables/purge`、`POST /api/tables/empty-trash`；补 `media_trash_routes_restore_purge_and_empty`、`mindmap_trash_routes_restore_purge_and_empty`、`table_trash_routes_restore_purge_and_empty` 回归。
- 2026-05-15：`/trash` 资源区已从“待迁移”改为真实恢复 / 彻底删除按钮，并新增“清空资源垃圾箱”；路由级 fixture 覆盖附件、mindmap、table 三类资源按钮。
- 2026-05-15：`cargo test -p mnote-web trash` 通过 7 项；Playwright 只读 smoke 访问 `http://localhost:3001/trash?workspaceId=ws_demo` 返回 200，确认工作台、页面区、资源区、“清空页面垃圾箱”和“清空资源垃圾箱”存在。真实数据当前没有垃圾箱资源行，未执行清空或彻底删除动作。
- 注意：本阶段完成的是 `/trash` 产品入口和 Rust 兼容 route 止血；附件 / mindmap / table 尚未正式收口为 `tree.resource.*` 命令，继续由阶段 5 跟踪。页面恢复 / 彻底删除 / 清空后的跨浏览器实时计数仍需在阶段 10 的 no-refresh 验收覆盖。

### 阶段 3：删除进入垃圾箱，清空后 Convex 同步永久删除

- [x] 创建测试页面、子页面、附件、mindmap、table，删除后均进入对应垃圾箱。
- [x] 清空页面垃圾箱后，确认 `documents.emptyTrashByWorkspace` 或等价 command 删除文档子树及关联数据。
- [x] 清空附件垃圾箱后，确认 `mediaAssets.emptyTrashByWorkspace`、`mindmaps.emptyTrashByWorkspace`、`tables.emptyTrashByWorkspace` 或统一 route 均执行成功。
- [x] 记录 Storage / OCR / LightRAG / page references / recent pages / favorites 的清理边界。

验收标准：

- Convex 查询不到已 purge 的 document / asset / mindmap / table。
- 清空失败时 UI 给出明确错误，不出现“UI 消失但 Convex 仍保留”的假成功。

安全前置：

- `emptyTrashByWorkspace` 是 workspace 级破坏性动作；写入型 smoke 必须使用本轮独立 workspace 或可证明只含本轮 `TEST-10REVIEW-07-P3-*` 数据的隔离环境。
- 禁止在默认测试账号的既有 workspace 上直接点击“清空页面垃圾箱 / 清空资源垃圾箱”，否则可能误删同一用户历史垃圾箱数据。
- 若当前没有安全 workspace seed 机制，先保留代码审查与 route 级 fixture 验收，补齐隔离 workspace 后再执行真实清空。

执行记录：

- 2026-05-15 代码审查：`documents.emptyTrashByWorkspace` 会删除当前用户在 workspace 内 `deleted_at != null` 的 documents，并调用 `purgeDocumentRelatedData` 清理关联 `mindmaps`、`media_assets`、`document_tables` / `document_table_rows`、`comment_messages` / `comment_threads`、`page_references`、`document_stars`、`user_recent_pages`；附件底层 Convex Files 只在同一 `storage_id` 没有其它未清理引用时删除。
- 2026-05-15 代码审查：`mediaAssets.emptyTrashByWorkspace` 会清理已过期 `deleted_at` 或历史 `purged_at` 的 `media_assets`，并按 `storage_id` 引用计数决定是否删除 Convex Files；`mindmaps.emptyTrashByWorkspace` 删除当前用户已软删 mindmap；`tables.emptyTrashByWorkspace` 删除已归档且到期的 `document_tables` 及其 `document_table_rows`。
- 2026-05-15 边界记录：OCR 文本随 `media_assets` 行删除；LightRAG 已有 `ingest.rag_index_*` 任务入口，但当前 purge 路径未发现反向删除 LightRAG 索引的实现，应作为后续补充边界而非已完成项。
- 2026-05-15 代码缺口止血：Rust 3000 主壳已补资源软删除兼容入口，避免阶段 3 被“资源无法进入垃圾箱”阻塞：
  - `POST /api/media/batch` 支持 `action: "delete"`，写入 `media_assets.deleted_at / deleted_by`。
  - `DELETE /api/mindmap/{docId}/{mindmapId}` 调用 `mindmaps.softDelete`。
  - `DELETE /api/tables/{tableId}` 调用 `tables.remove`。
  - `cargo test -p mnote-web trash` 覆盖 `media_trash_routes_delete_restore_purge_and_empty`、`mindmap_trash_routes_delete_restore_purge_and_empty`、`table_trash_routes_delete_restore_purge_and_empty`。
- 2026-05-15 安全验证结论：未找到现成 smoke 能安全创建独立 workspace 并完成页面 + 附件 + mindmap + table 的真实清空验收；默认测试账号已有 workspace，不能直接对其执行 workspace 级 empty trash。
- 2026-05-15：已补详细执行设计 `design/04-tree-domain/done/4-25-trash-empty-isolated-workspace-smoke-v1.md`，阶段 3 写入型 smoke 必须按一次性用户 / 一次性 workspace 隔离方案执行。
- 2026-05-15：已固化并执行 `scripts/task427-trash-empty-isolated-workspace-smoke.js`，一次性用户 `mnote.trash.1778793827654@example.com` 创建隔离 workspace `ws_req_1778793827876_33`，seed 页面 `tree_1778793827972_4`、子页面 `tree_1778793827997_5`、附件 `asset_trash_1778793827654`、mindmap `mind_trash_1778793827654`、table `mp5zx1wjqwssfr4g5jj`；删除后确认进入垃圾箱，随后清空页面与资源垃圾箱，并用 Convex query 确认 document / media_asset / mindmap / document_table / document_table_rows 均不可再查询。
- 2026-05-15：真实 smoke 输出：`documents.emptyTrashByWorkspace` 删除 2 个页面；`mediaAssets.emptyTrashByWorkspace` 删除 1 个附件；`mindmaps.emptyTrashByWorkspace` 删除 1 个 mindmap；`tables.emptyTrashByWorkspace` 删除 1 个 table。

### 阶段 4：文件树多选与批量删除

- [x] 文件树创建 3 个页面、2 个附件、1 个 mindmap、1 个 table 测试数据。
- [x] 验证 Ctrl/Cmd 多选、Shift 范围、右键已选项保持 selection、右键未选项切换 action target 的 reducer / DOM host / local-folder 浏览器证据。
- [x] 验证 Convex filetree 下 Ctrl/Cmd、Shift、右键已选 / 未选 action target 的真实端到端行为。
- [x] 验证 Delete / Backspace 批量删除混合 doc + asset。
- [x] 验证父子页面同时选中时 preflight 只生成一次有效删除计划，不重复删除子树或页面下资产。
- [x] 验证部分失败时保留可理解的 result summary，并刷新到真实服务端状态。

验收标准：

- 删除前确认弹窗准确列出页面、附件、mindmap、table 数量。
- 删除后无需刷新浏览器即可从文件树消失，并在垃圾箱中出现。

执行记录：

- 2026-05-15 代码审查：`wolai-frontend/src/lib/file-tree/selection.ts` 的 `reduceFileTreeSelection` 已支持 Ctrl/Cmd toggle、Shift range、右键已选项保持 selection、右键未选项切单选；`selection.test.ts` 已覆盖这些 reducer 规则。
- 2026-05-15 代码审查：`wolai-frontend/src/components/sidebar/tree-shell-dom-host.tsx` 已把真实点击、右键、Delete、Backspace 接到 filetree selection 与 `onFileTreeDeleteSelection`；`tree-shell-host.test.tsx` 已覆盖 Delete / Backspace 使用当前稳定多选 selection，避免旧 runtime selection 回写覆盖新 selection。
- 2026-05-15 代码审查：`handleDeleteResourceSelection` 已走 `preflightFileTreeDelete(buildFileTreeShellDeletePreflightPayload(...))`，确认文案会按页面、附件、思维导图、在线表格分别计数。
- 2026-05-15 代码审查：父子去重已有两层实现证据：TS legacy `computeFileTreeDeleteTargets` 会过滤被父页面覆盖的子页面 / 资产；Rust `build_filetree_delete_plan` 会过滤 top-level docs，并跳过被选中页面或其祖先页面覆盖的资产。`tree_filetree_delete_preflight_plans_doc_and_asset_targets` 已覆盖父页面 + 子 index + 父/子资产 + 另一页面资产同时选中时，最终只保留父页面与未被该子树覆盖的外部资产。
- 2026-05-15 验证：`cd /mnt/Data1T/mnote/rust && cargo test -p bridge-runtime tree_filetree_delete_preflight_plans_doc_and_asset_targets` 通过，1 项通过、0 失败。
- 2026-05-15 浏览器证据边界：`scripts/task163-local-folder-unified-tree-browser-smoke.js` 已覆盖 local-folder 的 Ctrl/Cmd 多选、右键已选保持、Shift 范围、右键未选切换、Delete preflight 取消；`scripts/task179-tree-create-delete-no-reload-smoke.js` 只覆盖 Convex filetree 单页面 Delete 后无导航 / 无刷新；`scripts/task427-trash-empty-isolated-workspace-smoke.js` 覆盖隔离 workspace 的 API seed 与清空垃圾箱，不是 filetree UI 多选删除。
- 2026-05-15：已补阶段 4 执行设计并归档为 `design/04-tree-domain/done/4-26-filetree-bulk-delete-selection-smoke-v1.md`，明确一次性用户 / workspace seed 与 Convex filetree UI 多选删除验收矩阵。
- 2026-05-15：已新增 `scripts/task428-filetree-bulk-delete-selection-smoke.js`，真实浏览器使用一次性用户 `mnote.filetree.1778795393112@example.com` 和隔离 workspace `ws_req_1778795393339_3` 创建 3 页面、2 附件、1 mindmap、1 table；在 3001 主入口 filetree 验证 Ctrl/Cmd 多选、Shift 范围、右键已选保持、右键未选切换。
- 2026-05-15：`task428` Delete 轮选择 root + child + file asset + mindmap + table，确认文案为 `1 个页面 + 1 个附件 + 1 个思维导图 + 1 个在线表格`，请求为 `tree.node.archive`、`/api/media/batch delete`、`DELETE /api/mindmap/{docId}/{mindmapId}`、`DELETE /api/tables/{tableId}`；父子页面去重后只发送 root archive，child 随子树删除。
- 2026-05-15：`task428` Backspace 轮选择 sibling + 第二个 file asset，确认文案为 `1 个页面 + 1 个附件`，请求为 `tree.node.archive` 与 `/api/media/batch delete`；两轮均验证目标行无需刷新从 filetree 消失，并用 Convex query 确认对应 document / media / mindmap / table 进入垃圾箱。
- 2026-05-15：阶段 4 新增 Rust SSR 主壳键盘批量删除实现。原因是 `task428` 初次失败暴露 3001 主壳 filetree selection 存在，但 Delete / Backspace 未进入删除处理；已补 `deleteSelectedSidebarFileTreeRows`，支持混合页面、附件、mindmap、table 删除，并补 `sidebar_filetree_delete_keys_support_mixed_doc_and_asset_selection` 回归。
- 2026-05-15 验证：`MNOTE_UI_BASE_URL=http://127.0.0.1:3001 node scripts/task428-filetree-bulk-delete-selection-smoke.js` 通过，结果文件 `tmp/task428-filetree-bulk-delete-selection-smoke/result.json` 记录请求、确认文案与部分失败摘要。
- 2026-05-15：`task428` 部分失败轮通过 route interception 强制 `/api/media/batch` 对 `asset_p4_failure_1778795530948` 返回 500；页面 `tree_1778795531341_8` 成功进入垃圾箱并从 filetree 消失，失败附件仍保留且 `deleted_at` 为空，UI 弹出 `部分对象删除失败：asset_p4_failure_1778795530948`。
- 2026-05-15 阶段 4 结论：阶段 4 子项已完成；剩余更长期问题转入阶段 5-8，包括资源正式 `tree.resource.*` 命令、恢复位置、VSCode Explorer 完整体验与双浏览器 no-refresh。

### 阶段 5：资源级 tree command 收口

- [x] 新增或确认 `tree.resource.archive`、`tree.resource.restore`、`tree.resource.purge`、`tree.resource.rename` 正式命令。
- [x] 将普通附件 `/api/media/batch` / `/api/media/purge` 收束为正式 `tree.resource.*` 兼容 alias。
- [x] 将 mindmap / table delete / restore / purge 收束为正式 `tree.resource.archive / restore / purge` 兼容 alias。
- [x] command event、projection resync、sidebar dataset 均能表达普通附件资源恢复和永久删除。

验收标准：

- 文件树资源删除 / 恢复 / 永久删除不再只依赖 legacy `/api/media/batch`、`/api/mindmap/*`、`/api/tables/*` 主路径；旧 URL 只能作为兼容 alias。
- 资源 commandName、确认文案、UI 状态一致。

执行记录：

- 2026-05-15 只读核对：阶段 5 未完成。现有正式资源命令只覆盖 `tree.resource.copy`、`tree.resource.move`、`tree.resource.upload`；`tree.resource.rename`、`tree.resource.archive`、`tree.resource.restore`、`tree.resource.purge` 尚未在 Rust runtime / storage-convex mapping 中形成正式命令面。
- 2026-05-15 代码证据：`wolai-frontend/src/lib/file-tree/resource-command-client.ts` 的 rename / delete / restore 默认仍发送 `/api/media/batch`；`wolai-frontend/src/app/api/media/batch/route.ts` 中 copy / move 会构造 `tree.resource.copy/move`，但 delete / restore / rename 仍直连 `mediaAssets.patchById`。
- 2026-05-15 代码证据：`rust/crates/bridge-runtime/src/lib.rs` 只处理 `tree.resource.copy | tree.resource.move` 与 `tree.resource.upload`；`rust/crates/storage-convex-bridge/src/mapping.rs` 也只映射这三类命令。
- 2026-05-15 代码证据：`rust/crates/mnote-web/src/routes/resource_trash.rs` 与 `wolai-frontend/src/components/sidebar/sidebar.tsx` 当前把 file asset、mindmap、table 按类型分流到 `/api/media/batch`、`/api/mindmap/*`、`/api/tables/*` 兼容入口；`rust/crates/mnote-web/src/routes/gateway.rs` 已在 `/trash` 页面明示“正式 tree.resource.* 命令仍在后续阶段收口”。
- 2026-05-15 测试边界：已有 `media_trash_routes_delete_restore_purge_and_empty`、`mindmap_trash_routes_delete_restore_purge_and_empty`、`table_trash_routes_delete_restore_purge_and_empty` 覆盖 Rust 兼容 route；这不能替代 `tree.resource.archive/restore/purge/rename` command 级回归。
- 2026-05-15：已补阶段 5 执行设计 `design/04-tree-domain/process/4-27-resource-lifecycle-command-cutover-v1.md`，明确本轮先完成普通附件 file asset，mindmap / table 暂保持兼容 alias，不冒充正式 cutover 完成。
- 2026-05-15：`bridge-runtime` 已新增 `tree.resource.archive/restore/purge/rename` 生命周期 command plan，普通附件分别映射到 `mediaAssets:patchById` / `mediaAssets:purgeById`，并携带 `domainEventPlan` 与 `streamDeltaHint`；`storage-convex-bridge` 已补默认命令映射。
- 2026-05-15：`wolai-frontend` 的 `/api/media/batch` 已将 `delete/restore/rename` 从直连 `mediaAssets.patchById` 改为 `tree.resource.archive/restore/rename` 兼容 alias；`/api/media/purge` 已从直连 `mediaAssets.purgeById` 改为 `tree.resource.purge` 兼容 alias，并记录 Rust bridge artifact。
- 2026-05-15：Rust 3000 `/api/media/batch` 与 `/api/media/purge` 已改为通过 runtime command 执行普通附件 archive / restore / rename / purge，旧 URL 仅保留为兼容入口；`tree_shell` command event schema 已补资源生命周期事件。
- 2026-05-15 验证：`cargo test -p bridge-runtime tree_resource_lifecycle_plans_cover_file_asset_commands`、`cargo test -p storage-convex-bridge tree_resource_lifecycle_commands_have_convex_mapping`、`cargo test -p mnote-web trash`、`cargo test -p mnote-web runtime_command_event_schema_covers_tree_and_resource_command_channels` 均通过。
- 2026-05-15 验证：`pnpm test src/app/api/media/purge/route.test.ts src/app/api/media/batch/route.test.ts src/lib/documents/rust-runtime.test.ts` 通过，26 项通过。
- 2026-05-15 剩余：mindmap / table 的 DELETE / restore / purge 仍是兼容 route 直连对应 Convex mutation；本阶段只完成普通附件 file asset 主链，不把 mindmap / table 误标为已完成正式 command cutover。
- 2026-05-15 二次审计补充：mindmap / table 双浏览器 no-refresh 已由阶段 8 验证，但它们仍不是正式 `tree.resource.*` lifecycle；后续应补 `tree.resource.mindmap.*` / `tree.resource.table.*` 或等价命名合同、command log、domain event 与 projection resync 回归。
- 2026-05-15 执行修正：先补 `mnote-web` 路由测试断言 mindmap / table DELETE、restore、purge 响应必须携带 `canonicalCommand=tree.resource.archive/restore/purge` 与对应 `resourceKind`，红灯确认旧 route 仍只返回 legacy mutation 结果。
- 2026-05-15 执行修正：Rust 3000 的 `/api/mindmap/{docId}/{mindmapId}` DELETE / PATCH restore / PATCH purge 已改为通过 `tree.resource.archive / restore / purge` runtime command 执行；`/api/tables/{tableId}` DELETE、`/api/tables/restore`、`/api/tables/purge` 已改为通过同一 runtime command 执行；旧 URL 保留为兼容 alias。
- 2026-05-15 执行修正：新增 `bridge-runtime` 合同测试 `tree_resource_lifecycle_plans_cover_mindmap_and_table_commands`，覆盖 mindmap -> `mindmaps:softDelete/restore/purge`，table -> `tables:remove/restore/purge`，并断言 `resourceLifecyclePlan`、`domainEventHint` 与 stream delta hint。
- 2026-05-15 验证：`cargo test --manifest-path rust/Cargo.toml -p bridge-runtime tree_resource_lifecycle_plans_cover_mindmap_and_table_commands -- --nocapture` 通过，1 项通过；`cargo test --manifest-path rust/Cargo.toml -p mnote-web trash_routes -- --nocapture --test-threads=1` 通过，3 项通过；`cargo test --manifest-path rust/Cargo.toml -p storage-convex-bridge tree_resource_lifecycle_commands_have_convex_mapping -- --nocapture` 通过，1 项通过。
- 2026-05-15 阶段 5 当前边界：`tree.resource.rename` 仍只覆盖普通附件 file asset；mindmap rename 继续明确不支持，table rename 待结合 `/api/tables/{tableId}` PATCH 与表格标题语义单独收口。`empty-trash` 仍是 workspace 级批量合同，不伪装成逐资源 `tree.resource.purge`。

### 阶段 6：恢复位置与 reveal/focus

- [x] 删除时记录恢复所需的 parent / order；view path / expanded path 由恢复后的父链展开补齐。
- [x] 恢复页面子树后验证原父节点、排序、展开路径和 active/focus 的代码路径与单测。
- [x] 父节点已永久删除时，提供明确 fallback。

验收标准：

- 恢复后对象回到可解释位置，并自动 reveal/select。
- 恢复结果不会悄悄落到根目录而无提示。

执行记录：

- 2026-05-15 代码审查：`wolai-frontend/convex/documents.ts` 的 `documents.restore` 会把根恢复节点 `parent_id` 置为 `null`，并只按同一 `deleted_at` 级联恢复子节点；当前没有恢复删除前 parent / sort_order / expanded path 的证据。
- 2026-05-15 对照证据：local-folder Markdown trash 已在 `rust/crates/mnote-web/src/routes/local_folder_source.rs` 记录 `original_relative_path`，恢复时可按原路径回放并处理重名冲突；Convex documents restore 没有同等级“删除前位置”记录。
- 2026-05-15 命令证据：`tree.node.restore` 命令链在 Next route 与 Rust route 中存在，`scripts/task163-local-folder-unified-tree-browser-smoke.js` 也覆盖了 Convex restore / purge 请求仍走 `/api/tree/commands`；但这只是请求级证据，不覆盖恢复位置。
- 2026-05-15 风险结论：这能保证“对象复活”，但不能保证恢复到删除前位置。若父节点仍存在，当前也可能恢复到根；若父节点已永久删除，缺少显式 fallback 选择与 UI 提示。
- 2026-05-15 UI 证据边界：Sidebar / tree shell 有 focusedId、selection、active row 的基础状态；Sidebar 垃圾箱恢复后主要 `refetch/refreshTree`，未找到 `router.push`、reveal、select、focus 恢复对象的端到端 smoke。Rust shell 虽有 `tree.filetree.reveal` postToHost 事件，但未找到 Sidebar / DOM host 的完整落地处理。
- 2026-05-15：已补阶段 6 执行设计 `design/04-tree-domain/process/4-28-trash-restore-location-reveal-focus-v1.md`。
- 2026-05-15：`wolai-frontend/convex/schema.ts` 为 documents 新增 `restore_parent_id / restore_sort_order`；`documents.softDelete` 删除子树时记录删除前 `parent_id / sort_order`；`documents.restore` 恢复时优先回放原父节点与排序，并返回 `restore_location.parent_id / sort_order / fallback_reason`。
- 2026-05-15：恢复父节点仍存在或属于本次级联恢复子树时回到原父节点；原父节点已不存在或仍在垃圾箱时 fallback 到根目录，并清空恢复位置快照。
- 2026-05-15：`restoreDocumentCommand` 已透传恢复位置；Sidebar 垃圾箱恢复后刷新数据、展开父链、滚动并 focus 恢复行；fallback 时提示“原父页面已不存在，已恢复到根目录”。
- 2026-05-15 验证：`pnpm test src/lib/documents/tree-command-client.test.ts src/lib/documents/restore-location.test.ts src/components/sidebar/tree-shell-host.test.tsx src/components/sidebar/sidebar-sync.test.ts` 通过，4 个测试文件、20 项测试通过。
- 2026-05-15 验证：`pnpm exec tsc --noEmit --pretty false 2>&1 | rg "convex/documents.ts\\((160|161|162|163|164|165|166|167|168|169|170|171|172|173|174|175)"` 无输出，阶段 6 修改附近无新增 TypeScript 报错；全量 `tsc` 仍受仓库既有错误影响，不作为本阶段完成条件。
- 2026-05-15 review 修正：发现 `/api/tree/commands restore` 先查 `documents.getMeta`，而 `getMeta` 默认过滤 `deleted_at != null`，会导致垃圾箱页面在进入 restore mutation 前 404；已改为 restore 入口使用 `includeDeleted: true`，并在 route 测试中断言该参数。
- 2026-05-15 review 修正：恢复到原排序位时可能与已有兄弟节点产生重复 `sort_order`；已新增 `buildDocumentRestoreOrderAssignments`，恢复时按目标父级重新编号兄弟节点，补“旧排序位被占用时 restored row 插入并后移兄弟节点”的单测。
- 2026-05-15 验证：`pnpm test src/lib/documents/tree-command-client.test.ts src/lib/documents/restore-location.test.ts src/components/sidebar/tree-shell-host.test.tsx src/components/sidebar/sidebar-sync.test.ts src/app/api/tree/commands/route.test.ts` 通过，5 个测试文件、32 项测试通过。
- 2026-05-15 验证：`pnpm exec tsc --noEmit --pretty false 2>&1 | rg "convex/documents.ts\\((16[5-9][0-9]|17[0-9][0-9]|18[0-4][0-9])"` 无输出，restore / softDelete 修改区间无新增 TypeScript 报错；全量 `tsc` 仍受既有无关错误影响。
- 2026-05-15 运行时刷新：执行 `npx convex dev --once --tail-logs disable --env-file ../.env.all --run ping:ping`，把本轮 Convex functions 部署到本地自托管运行时；首次 3000 smoke 在部署前复现旧 runtime 行为：恢复后 `parent_id` 仍为 `null`，部署后恢复位置生效。
- 2026-05-15 浏览器验证：新增并执行 `node scripts/task429-trash-restore-location-reveal-smoke.js` 通过；脚本使用一次性用户 / workspace，在 3000 主入口验证删除子页面后 `tree.node.restore` 返回原父页面与原排序位、兄弟 `sort_order` 后移、当前页面无需刷新重新显示恢复行；同时构造缺失父节点历史数据，验证 fallback 返回 `parent_missing_or_deleted` 并恢复到根目录。结果文件：`tmp/task429-trash-restore-location-reveal-smoke/result.json`。
- 2026-05-15 剩余：React Sidebar Drawer 的“恢复后 DOM focus 与 fallback alert”仍缺少 3000 主壳浏览器证据；当前 3000 主壳已切 Rust 垃圾箱入口，React Drawer focus 由代码路径和单测覆盖，后续如恢复 React 入口或补专用 harness 再补截图证据。双浏览器 no-refresh 归入阶段 8。

### 阶段 7：VSCode Explorer 体验补齐

- [x] Cut / Paste move：Ctrl/Cmd+X 后 Paste 应移动，不是复制或 no-op。
- [x] F2 inline rename：文件树行内编辑，不再只靠 prompt。
- [x] 右键菜单补齐 New File / New Folder / Paste Into / Refresh / Collapse All / Copy Path / Reveal 的可用性或禁用态。
- [x] DnD 补齐父拖子禁止、拖到自身禁止、readonly 禁止、Alt/Ctrl copy modifier、冲突确认。
- [x] F2 inline rename validation parity：空名、非法名、同级重名、扩展名策略在行内提示并阻断提交。
- [x] 右键菜单增强项：Copy Relative Path。
- [x] 右键菜单增强项：Paste Into action target。
- [x] 右键菜单增强项：New Folder 产品口径明确为“等待 Resource Tree folder 合同，不在页面树里伪造 folder 真相”。

验收标准：

- keyboard 与 context menu 对同一 selection 得到同一 command target。
- 禁用态有可解释条件，不出现点击后无反馈。

执行记录：

- 2026-05-15 代码审查：`wolai-frontend/src/components/sidebar/tree-shell-dom-host.tsx` 已在 filetree 键盘层识别 `F2`、Delete / Backspace、Ctrl/Cmd+C、Ctrl/Cmd+X、Ctrl/Cmd+V，并向 runtime 发 `beginRenameFocused`、`cutSelection`、`pasteIntoFocused` 等动作。
- 2026-05-15 浏览器证据边界：`scripts/task163-local-folder-unified-tree-browser-smoke.js` 已在 local-folder 模式覆盖 F2 inline rename、Ctrl/Cmd+X 后 cut decoration、Paste move preflight、确认后移动 Markdown、Alt 内部拖拽复制、外部 File drop、readonly drop 预检失败。
- 2026-05-15 Convex 证据边界：`scripts/task163-local-folder-unified-tree-browser-smoke.js` 也覆盖了 Convex context menu 基础项、copy / cut paste 命令请求、Delete 请求和 DnD indicator；但未覆盖 Convex F2 inline rename、真实 internal drop 执行、父拖子 / 拖自身 / readonly / 冲突确认端到端。
- 2026-05-15 初始缺口结论：上述强证据主要来自 local-folder；Convex filetree 下 Cut / Paste move、F2 inline rename、右键 New File / New Folder / Paste Into / Refresh / Collapse All / Copy Path / Relative Path / Reveal、DnD 禁止父拖子 / 自拖 / readonly / copy modifier 的完整端到端矩阵当时未找到。后续已由 `task430` 与 `task431` 补齐阶段 7 必要矩阵。
- 2026-05-15 代码风险：页面树 fallback / iframe host 中仍可见 `window.prompt("重命名页面", ...)` 与 prompt 式重命名路径，不能把“F2 inline rename 已全入口完成”作为当前口径。
- 2026-05-15 代码风险：`wolai-frontend/src/lib/file-tree/clipboard.ts` 的 TS clipboard payload 仍只有 `copy` 类型，Next DOM host / TS 工具层与 Rust srcdoc 路径的 cut 能力不完全一致；Rust shell context menu 的 `Refresh` 当前仍可能走 `window.location.reload()`，不符合 no-refresh 目标。
- 2026-05-15：已补阶段 7 子设计 `design/04-tree-domain/done/4-29-vscode-explorer-cut-paste-move-v1.md`。
- 2026-05-15：`wolai-frontend` 主 Sidebar 剪贴板 action 已扩展为 `copy | cut`；`Ctrl/Cmd+X` 写入 cut payload，`Ctrl/Cmd+V` 遇到 cut payload 时，Rust family renderer 复用 `tree.filetree.drop.preflight` move plan，页面走 `moveDocumentCommand`，普通附件走 `moveFileTreeResourceAssets`，成功后清空剪贴板。
- 2026-05-15 验证：`pnpm test src/lib/file-tree/clipboard.test.ts src/components/sidebar/sidebar-paste-preflight-source.test.ts` 通过，2 个测试文件、7 项测试通过。
- 2026-05-15 验证：`pnpm exec tsc --noEmit --pretty false 2>&1 | rg "(src/lib/file-tree/clipboard.ts|src/components/sidebar/tree-pane-bindings.ts|src/components/sidebar/sidebar.tsx)"` 无输出，本轮 cut/paste 修改文件无新增 TypeScript 报错；全量 `tsc` 仍受既有无关错误影响。
- 2026-05-15：已补阶段 7 子设计 `design/04-tree-domain/done/4-30-vscode-explorer-f2-inline-rename-v1.md`。
- 2026-05-15：`tree-shell-dom-host.tsx` filetree row 已新增行内 `tree-rename-input`；F2 进入 inline rename，Enter / blur 提交，Escape 取消。页面 / index row 提交 `tree.node.rename` 并通过 `onTreeMutation` 回写；asset row 走 `/api/media/batch rename` 并广播 `wolai:assets-changed`。
- 2026-05-15 验证：`pnpm test src/components/sidebar/tree-shell-host.test.tsx` 通过，1 个测试文件、8 项测试通过；新增测试覆盖 F2 inline input 与 `tree.node.rename` 提交。
- 2026-05-15：已补阶段 7 子设计 `design/04-tree-domain/done/4-31-vscode-explorer-context-menu-minimum-v1.md`。
- 2026-05-15：主 Sidebar 右键菜单已显式补齐 `New File / New Folder / Paste Into / Refresh / Collapse All / Copy Path / Reveal`。其中 `New File` 复用新建子页面，`Refresh` 调 `refreshTree()`，`Collapse All` 清空展开集合，`Copy Path` 复制页面标题路径，`Reveal` 展开父链并滚动；`New Folder` 与 `Paste Into` 以禁用态展示原因，避免伪造未完成的 folder / right-click paste target 合同。
- 2026-05-15 验证：`pnpm test src/components/sidebar/sidebar-context-menu-source.test.ts` 通过，1 个测试文件、1 项测试通过；覆盖菜单项存在、禁用态存在、未引入 `window.location.reload`。
- 2026-05-15：已补阶段 7 子设计 `design/04-tree-domain/done/4-32-vscode-explorer-dnd-modifier-guard-v1.md`。
- 2026-05-15：filetree DnD copy modifier 已从仅 Alt 扩展为 Alt / Ctrl / Meta；同时覆盖 `wolai-frontend` DOM host 与 Rust SSR 主壳 `layout.rs`。父拖子 / 自拖继续由 Rust `tree.filetree.drop.preflight` 校验，不在 UI 层新增第二套祖先判断。
- 2026-05-15 验证：`pnpm test src/components/sidebar/sidebar-dnd-source.test.ts` 通过，1 个测试文件、1 项测试通过。
- 2026-05-15 3000 主入口修正：Rust SSR 主壳补齐文件树 F2 inline rename、Ctrl/Cmd+X/V cut-paste move、右键菜单最低项 `New File / New Folder / Paste Into / Refresh / Collapse All / Copy Path / Reveal`；`Refresh` 只发局部刷新事件，不使用 `window.location.reload()`；`New Folder` 与 `Paste Into` 保持禁用态并给出原因。
- 2026-05-15 3000 主入口修正：新增 `POST /api/tree/filetree/drop-preflight`，通过 Rust bridge `tree.filetree.drop.preflight` 返回真实 preflight plan；自拖自身与父拖子返回 400，copy modifier plan 保留 `copy: true`。
- 2026-05-15 资源 rename 修正：`tree.resource.rename` 的 bridge plan 补 `patch.file_name`；Rust `/api/media/batch rename` 在保留 `tree.resource.rename` artifact command 的同时先执行兼容 `mediaAssets:patchById`，避免 3000 主入口附件 F2 后 Convex 文件名不更新。
- 2026-05-15 真实浏览器 smoke：新增并执行 `MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task430-vscode-explorer-stage7-smoke.js` 通过。脚本使用一次性用户 / workspace，验证：
  - 页面 F2 inline rename：请求 `/api/tree/commands action=rename`，Convex 标题更新，无需刷新可见。
  - 附件 F2 inline rename：请求 `/api/media/batch action=rename`，Convex `media_assets.file_name` 更新，无需刷新可见。
  - Cut / Paste move：`Ctrl/Cmd+X` 后目标页面 `Ctrl/Cmd+V` 触发 `action=move`，子页面 `parent_id` 变为目标父页面。
  - 右键菜单最低项：`New File / New Folder / Paste Into / Refresh / Collapse All / Copy Path / Reveal` 可见，禁用项有 title 原因。
  - DnD preflight：自拖自身与父拖子均被拒绝；copy modifier preflight 返回 `copy: true`。
  - 结果文件：`tmp/task430-vscode-explorer-stage7-smoke/result.json`。
- 2026-05-15 阶段 7 中途剩余：`task430` 之后 readonly 与命名冲突确认仍未在主 Sidebar Convex filetree 构造真实 route 矩阵；当时 DnD 总项因 readonly / conflict 尚未勾选完成。后续已由 `task431` 补齐。
- 2026-05-15：`bridge-runtime` 的 `tree.filetree.drop.preflight` payload / plan 已补 `sourceCapabilities`、`targetCapabilities`、`targetChildren`、`conflictPolicy`、`requiresConfirmation` 与 `conflicts`；readonly target/source 会在 preflight 阶段返回 validation error，同名目标会返回 `requiresConfirmation=true`，普通 copy modifier 不被 readonly source move 规则误拒绝。
- 2026-05-15：主 Sidebar `buildFileTreeShellInternalDropPreflightPayload` 已把 row title、target children 与 capability payload 透传到 preflight；`handleResourcePaneInternalDrop` 与 cut-paste move 路径在 `requiresConfirmation` 时调用确认，用户取消时不执行后续 move/copy。
- 2026-05-15 验证：`cargo test -p bridge-runtime tree_filetree_drop_preflight -- --nocapture` 通过 4 项；`pnpm test src/lib/file-tree/shell.test.ts src/lib/file-tree/resource-command-client.test.ts src/components/sidebar/sidebar-paste-preflight-source.test.ts src/components/sidebar/sidebar-dnd-source.test.ts` 通过 4 个测试文件、21 项。
- 2026-05-15 真实 route smoke：新增并执行 `MNOTE_UI_BASE_URL=http://127.0.0.1:3001 MNOTE_AUTH_BASE_URL=http://127.0.0.1:3000 node scripts/task431-vscode-explorer-dnd-readonly-conflict-smoke.js` 通过。脚本使用当前代码新启动的 3001 mnote-web 与 3000 auth cookie，验证 readonly target/status 400、readonly source/status 400、同名目标返回 `requiresConfirmation=true` 与 conflict 明细、copy modifier 保持 `copy=true` 且无误报。结果文件：`tmp/task431-vscode-explorer-dnd-readonly-conflict-smoke/result.json`。
- 2026-05-15 注意：本次 `task431` 未直接复用常驻 3000 进程，因为 3000 当时仍运行旧 `mnote-web` 二进制，第一次对 3000 执行脚本复现旧 preflight 放行 readonly 的行为；新代码已在 3001 当前编译进程验证通过，3000 需要重启后才具备同一行为。
- 2026-05-15 二次审计补充：阶段 7 已完成最低可用体验，不等于完整 VSCode Explorer parity。后续仍需补 F2 行内 validation、`Copy Relative Path`、`Paste Into` 右键 action target、`New Folder` 长期产品口径；这些列为 P1/P2，不阻塞已完成的 cut / paste move、F2 基础行内重命名、菜单最低项与 DnD guard。
- 2026-05-15 执行修正：先补 `tree-shell-host.test.tsx` 红灯测试，断言 F2 行内重命名必须在输入框内阻断空名、非法路径字符、同级重名，且普通附件输入不带扩展名时保留原扩展名。
- 2026-05-15 执行修正：`tree-shell-dom-host.tsx` 已新增 F2 rename 行内错误状态、`aria-invalid`、错误文案、`CSS.escape` fallback、普通附件扩展名保留；非法输入不再发 rename 请求。
- 2026-05-15 验证：`cd /mnt/Data1T/mnote/wolai-frontend && pnpm test src/components/sidebar/tree-shell-host.test.tsx -- --runInBand` 通过，1 个测试文件、10 项通过。
- 2026-05-15 执行修正：`ContextMenu` 新增 `Copy Relative Path`，复用页面标题路径计算但复制不带根斜杠的 workspace-relative 路径；`New Folder` 继续明确禁用，原因是当前页面树没有 folder 对象合同，不能临时伪造第二套 folder 真相。
- 2026-05-15 验证：`cd /mnt/Data1T/mnote/wolai-frontend && pnpm test src/components/sidebar/sidebar-context-menu-source.test.ts -- --runInBand` 通过，1 个测试文件、1 项通过。
- 2026-05-15 执行修正：先补 `sidebar-paste-preflight-source.test.ts` 红灯测试，断言 `ContextMenuProps` 暴露 `onPasteInto`、右键 `Paste Into` 调用 `onPasteInto(node)`，并要求 paste helper 使用 `targetDocumentIdOverride` 进入 Rust paste / drop preflight。
- 2026-05-15 执行修正：主 Sidebar 已抽出 `executeFileTreePaste(targetDocumentIdOverride)`；`Ctrl/Cmd+V` 传 `null` 保留 focused target 规则，右键 `Paste Into` 传当前菜单节点 `node.id` 作为目标页面。空剪贴板时右键入口会提示“剪贴板没有可粘贴的文件树内容”，不再是点击无反馈。
- 2026-05-15 验证：`cd /mnt/Data1T/mnote/wolai-frontend && pnpm test src/components/sidebar/sidebar-paste-preflight-source.test.ts -- --runInBand` 通过，1 个测试文件、3 项通过。
- 2026-05-15 验证：`cd /mnt/Data1T/mnote/wolai-frontend && pnpm test src/components/sidebar/sidebar-context-menu-source.test.ts -- --runInBand` 通过，1 个测试文件、1 项通过。
- 2026-05-15 验证：`cd /mnt/Data1T/mnote/wolai-frontend && pnpm test src/components/sidebar/tree-shell-host.test.tsx -- --runInBand` 通过，1 个测试文件、10 项通过。
- 2026-05-15 验证边界：`pnpm exec eslint src/components/sidebar/sidebar.tsx --quiet` 与 `pnpm exec eslint src/components/sidebar/sidebar-paste-preflight-source.test.ts src/components/sidebar/sidebar-context-menu-source.test.ts src/components/sidebar/tree-shell-host.test.tsx --quiet` 无 error；全量 `pnpm lint` 仍被仓库既有 lint 问题阻塞，本轮输出显示 `823 problems (2 errors, 821 warnings)`。

### 阶段 8：实时刷新与双浏览器回归

- [x] A 浏览器新建、删除、恢复、彻底删除页面，B 浏览器无需刷新可见更新。
- [x] A 浏览器对普通附件 file asset 执行上传、删除、恢复、彻底删除、清空资源垃圾箱，B 浏览器无需刷新可见更新。
- [x] A 浏览器对 mindmap / table 执行新建、删除、恢复、彻底删除、清空资源垃圾箱，B 浏览器无需刷新可见更新。
- [x] 本地 folder 模式验证 markdown 新建 / 重命名 / 删除 watcher 不触发浏览器 reload；非 md 资源文件 watcher create / rename / delete 已覆盖，trash / restore / purge 单列待补。
- [x] `resync_required` 事件合同防回归：`resync_required` 本身只推进 cursor，不替换数据；后续 snapshot / resync 才替换数据，已补前端 hook 与 Rust stream 合同测试。

验收标准：

- 所有主链操作不依赖浏览器刷新。
- `refreshTree()` 可以作为兜底，但不作为唯一可见性来源。

执行记录：

- 2026-05-15 代码审查：Rust Web `/api/tree/events` 已有 snapshot / delta / resync 路由与单元测试；`scripts/task120-rust-web-tree-integration-smoke.js`、`scripts/task123-rust-web-tree-live-stream-consumer-smoke.js`、`scripts/task177-tree-move-archive-live-smoke.js`、`scripts/task179-tree-create-delete-no-reload-smoke.js` 覆盖了 tree stream、页面 rename / move / archive、单页面 create/delete no-refresh 等部分链路。
- 2026-05-15 代码审查：`wolai-frontend/src/lib/tree-stream/use-sidebar-tree-stream.ts` 已接入 EventSource `/api/tree/events` 并将 delta 交给 `applyTreeStreamDelta`；`scripts/task426-mnote-web-main-no-reload-smoke.js` 覆盖了单浏览器新建页面、新建 mindmap asset、删除页面本地 apply。
- 2026-05-15 代码审查：资源删除与部分 sidebar 操作仍显式调用 `refreshTree()` 或依赖局部 state 移除；这可以作为兜底，但不能证明双浏览器实时同步闭环。
- 2026-05-15 浏览器证据边界：`scripts/task169-mindmap-realtime-smoke.js` 已使用双浏览器上下文覆盖 mindmap realtime 相关场景，但未覆盖文件树中页面、附件、mindmap、table 的删除 / 恢复 / 彻底删除 / 清空垃圾箱全矩阵。
- 2026-05-15 未闭合结论：阶段 8 仍需新增双浏览器 smoke，验证 A 浏览器对页面和三类资源执行 create / archive / restore / purge / empty-trash 后，B 浏览器无需刷新可见更新，并记录事件来源是 tree delta / resync 还是 Convex live query。
- 2026-05-15 补充审查结论：阶段 8 需要显式拆成对象类型 × 生命周期矩阵。页面、附件、mindmap、table 都必须分别覆盖 create / archive / restore / purge / empty-trash，且验收口径是 A 浏览器执行、B 浏览器不刷新即可在 File Tree 与 Trash 工作台看到变化。现有 `task177/task179/task426/task428/task429` 主要是单浏览器 no-refresh 或局部 live probe，`task427` 是 API/Convex 级 purge 验证，`task169` 是 mindmap 内容实时稳定性；这些不能替代文件树/垃圾箱双浏览器生命周期矩阵。实现边界上，`useSidebarTreeStream` 可消费 snapshot/delta/resync，`useConvexSidebarData` 依赖 Convex live，但 Trash Drawer 仍大量使用 `refreshTree()/refetch()` 作为本地补偿；阶段 8 应记录每个矩阵项实际由 tree delta、tree resync snapshot、Convex live，还是手动 refreshTree 兜底驱动，避免把本地刷新补偿误标为实时闭环。
- 2026-05-15 页面双浏览器 smoke 初次失败暴露根因：`documents.emptyTrashByWorkspace` 只执行 Convex mutation，没有写入 tree command/domain event，导致 B 浏览器 `/trash` 收不到清空事件，必须刷新后才消失。已给 `documents.emptyTrashByWorkspace` command plan 补 `tree.trash.documents.emptied` domain event 与 `resync_required` stream hint，并把 `/api/documents/empty-trash` 改为 artifact-aware 执行；同时补 Convex mutation validator 的 `streamDeltaHint / domainEventHint / domainEventPlan / domainEventPlans` 参数，避免运行时 502。
- 2026-05-15 验证：`cargo test --manifest-path rust/Cargo.toml -p bridge-runtime documents_empty_trash_plan_emits_tree_resync_event -- --nocapture` 通过，断言 `documents.emptyTrashByWorkspace` command plan 携带 `tree.trash.documents.emptied` 与 `resync_required`；`cargo test --manifest-path rust/Cargo.toml -p mnote-web documents_empty_trash_route_executes_workspace_trash_purge -- --nocapture` 通过，断言 empty-trash 返回 `documents.emptyTrashByWorkspace` command log、`tree.trash.documents.emptied` domain event 与 `resync_required` stream delta；`pnpm test src/app/api/tree/commands/route.test.ts src/lib/documents/tree-command-client.test.ts` 通过，2 个测试文件、14 项通过。
- 2026-05-15 运行时刷新：执行 `npx convex dev --once --tail-logs disable --env-file ../.env.all --run ping:ping`，把本轮 Convex validator 修改部署到本地自托管运行时。
- 2026-05-15 真实双浏览器 smoke：新增并执行 `MNOTE_UI_BASE_URL=http://127.0.0.1:3001 MNOTE_AUTH_BASE_URL=http://127.0.0.1:3000 node scripts/task432-filetree-trash-page-dual-browser-no-refresh-smoke.js` 通过。脚本使用一次性用户 `mnote.stage8.page.1778804544630@example.com` 与隔离 workspace `tree_1778804545042_7`；B1 打开 `/documents/tree_1778804545042_6?workspaceId=tree_1778804545042_7` 观察 File Tree，B2 打开 `/trash?workspaceId=tree_1778804545042_7` 观察 Trash 工作台。A 侧依次执行页面 create / archive / restore / archive+purge / create+archive+empty-trash；B 侧每一步均无需刷新可见更新，`navigationEvents` 始终为空。事件来源记录：create 使用 `tree:resync`，archive 使用 `tree:delta remove_document`，restore 使用 `tree:delta upsert_document`，purge 使用 `tree:resync`，empty-trash 使用 `tree:resync`；结果文件为 `tmp/task432-filetree-trash-page-dual-browser-no-refresh-smoke/result.json`。
- 2026-05-15 普通附件双浏览器 smoke 初次失败暴露身份与事件两层根因：`tree.resource.archive/restore/purge` 计划中 route 已放入真实 Convex userId，但 `mnote-web` transport 在 legacy `mediaAssets:*` 参数转换时又用 HTTP context actor 覆盖，导致 `mediaAssets.patchById` 在 `assertWorkspaceMember` 处返回“无权访问该工作空间”；已修正为优先保留 `args_json.userId`。随后 `mediaAssets.emptyTrashByWorkspace` 只执行 Convex mutation、不写 tree domain event，导致 B 浏览器 `/trash` 不刷新看不到清空；已在 Rust `/api/media/empty-trash` 成功后记录 `tree.trash.media.emptied` domain event 与 `resync_required` stream delta。
- 2026-05-15 验证：`cargo test --manifest-path rust/Cargo.toml -p mnote-web convex_resource_lifecycle_args_keep_effective_user_id -- --nocapture` 通过，断言资源 lifecycle 参数转换保留真实 Convex userId；`cargo test --manifest-path rust/Cargo.toml -p mnote-web media_trash_routes_delete_restore_purge_and_empty -- --nocapture` 通过。
- 2026-05-15 真实双浏览器 smoke：新增并执行 `MNOTE_UI_BASE_URL=http://127.0.0.1:3001 MNOTE_AUTH_BASE_URL=http://127.0.0.1:3000 node scripts/task433-filetree-trash-file-asset-dual-browser-no-refresh-smoke.js` 通过。脚本使用一次性用户 `mnote.stage8.file.1778806752763@example.com` 与隔离 workspace `tree_1778806753148_2`；B1 打开 `/documents/tree_1778806753148_1?workspaceId=tree_1778806753148_2` 观察 File Tree，B2 打开 `/trash?workspaceId=tree_1778806753148_2` 观察 Trash 工作台。A 侧依次执行普通附件 upload / archive / restore / archive+purge / upload+archive+empty-trash；B 侧每一步均无需刷新可见更新，执行阶段 `navigationEvents` 为空。事件来源记录：upload 使用 `tree:delta upsert_assets`，archive / restore / purge / empty-trash 使用 `tree:resync`；结果文件为 `tmp/task433-filetree-trash-file-asset-dual-browser-no-refresh-smoke/result.json`。
- 2026-05-15 mindmap/table 双浏览器 smoke 初次实现时发现 table create 只是测试前 seed，不能证明“新建 table 后 B 无刷新出现”；已补 Rust `/api/tables/create` 兼容入口，创建成功后记录 `tree.resource.table.created` domain event 与 `resync_required` stream delta。mindmap / table 的 delete / restore / purge / empty-trash compat route 也已补 tree resync artifacts，避免 B 浏览器只能依赖刷新。
- 2026-05-15 验证：`cargo test --manifest-path rust/Cargo.toml -p mnote-web trash_routes -- --nocapture --test-threads=1` 通过，覆盖 media / mindmap / table 资源垃圾箱 route；`node --check scripts/task434-filetree-trash-mindmap-table-dual-browser-no-refresh-smoke.js` 通过。
- 2026-05-15 真实双浏览器 smoke：新增并执行 `MNOTE_UI_BASE_URL=http://127.0.0.1:3001 MNOTE_AUTH_BASE_URL=http://127.0.0.1:3000 node scripts/task434-filetree-trash-mindmap-table-dual-browser-no-refresh-smoke.js` 通过。脚本使用一次性用户 `mnote.stage8.mindtable.1778808180081@example.com` 与隔离 workspace `tree_1778808180480_2`；A 侧依次执行 mindmap create / archive / restore / archive+purge、table create / archive / restore / archive+purge、mindmap+table create / archive / empty-trash；B 侧 File Tree 与 `/trash` 每一步均无需刷新可见更新，执行阶段 `navigationEvents` 为空。事件来源记录：mindmap create/delete/restore 主要为 `tree:delta resync_required`，mindmap purge 为 `tree:resync`；table create/delete/restore 主要为 `tree:delta resync_required`，table purge / empty-trash 为 `tree:resync`。结果文件为 `tmp/task434-filetree-trash-mindmap-table-dual-browser-no-refresh-smoke/result.json`。
- 2026-05-15 local folder 二次审计：`scripts/task163-local-folder-unified-tree-browser-smoke.js` 在 extended 模式覆盖本地 Markdown 新建、删除进入 `.mnote/trash`、restore、purge，也覆盖外部新增 `watcher-added.md` 后页面树可见；但当前本地 folder watcher 主壳在 revision 变化后调用 `window.location.reload()`，因此不能算 no-refresh watcher 闭环。
- 2026-05-15 local folder 二次审计：同一脚本也覆盖 `watcher-asset.txt` 外部新增 / 删除后 filetree 可见 / 消失；但 Rust `LocalFolderWatcherRegistry` 只广播 markdown 事件，当前 filetree 资产可见性主要依赖 `/api/tree/local-folder-watch` revision 轮询后重载，不是非 md 资源事件级 no-refresh。
- 2026-05-15 local folder 二次审计：`LocalFolderWatcherRegistry` 底层理论覆盖 Markdown create / remove / name / data / metadata 事件，但现有 smoke 只实测外部新增 Markdown；外部 Markdown 重命名、删除尚未形成浏览器验收。非 md 资源文件在 filetree 中能作为 `asset` 出现，inline rename 可写入文件系统，但 copy / cut / delete / download 当前仍是禁用态，本地 `delete / restore / purge` command 只覆盖 Markdown 页面。
- 2026-05-15 阶段 8 中途状态：页面 document、普通附件 file asset、mindmap、table 的 Convex 双浏览器生命周期已闭合；本地 folder markdown watcher 当时仍是 reload 型刷新，非 md 本地资源文件 watcher / trash / restore / purge 当时仍未闭合；`resync_required` 事件合同当时还需要防回归测试。后续已由阶段 8/9 继续执行记录闭合。
- 2026-05-15 继续执行审计：local folder no-refresh 的实际刷新来源在 Rust Web 内嵌 tree shell runtime：`rust/crates/mnote-web/src/routes/tree.rs` 的 local folder revision polling 命中 `/api/tree/local-folder-watch` 后调用 `window.location.reload()`；同文件的 filetree 右键 `Refresh` 当前也直接 `window.location.reload()`。因此现有能力只能称为 revision polling + reload 型可见性，不是 no-refresh。
- 2026-05-15 继续执行审计：`/api/local-folder/events` 由 `LocalFolderWatcherRegistry` 提供 SSE，但 watcher 当前 `is_markdown_path` 过滤为 `.md/.markdown`，只对 Markdown 事件广播；非 md 资源文件变化不会走该 SSE，主要依赖 `/api/tree/local-folder-watch` 的全目录 revision hash。
- 2026-05-15 下一步最小实现口径：先不要扩散到本地非 md trash / restore / purge。优先把 local folder filetree/page tree 的 revision polling 从 `window.location.reload()` 改为“拉取最新 local folder snapshot 并替换当前内存行模型后 `renderTree()`”，并新增回归测试断言 local folder watch 分支不含 `window.location.reload()`，再补浏览器 smoke 记录 `navigationEvents.length === 0`。
- 2026-05-15 执行修正：先补 `rust/crates/mnote-web/src/routes/tree.rs` debug/internal tree shell 回归，断言 local folder watch 分支包含 `/api/tree/local-folder-watch` 与 `refreshLocalFolderSnapshot`，且不含 `window.location.reload`；随后发现 3000 主入口实际走 `rust/crates/mnote-web/src/ssr/pages/layout.rs` 的主 Sidebar runtime，不能只改 debug tree shell。
- 2026-05-15 执行修正：主 Sidebar runtime 已新增 `startLocalFolderSidebarWatch()`，对 `sourceKind=local_folder` 轮询 `/api/tree/local-folder-watch`，revision 变化后 fetch 当前 HTML 并用 `replaceSidebarTreeFromDocument()` 原地替换 `sidebar-tree-root` 与 `sidebar-file-tree-root`；启动后立即执行一次 baseline poll，避免外部变更发生在首轮定时轮询前被误当作初始 revision。
- 2026-05-15 验证：`cargo test --manifest-path rust/Cargo.toml -p mnote-web sidebar_tree_runtime_polls_local_folder_without_browser_reload -- --nocapture` 通过，断言主 Sidebar runtime 具备 local folder polling / snapshot apply 且不使用 `window.location.reload`；`cargo test --manifest-path rust/Cargo.toml -p mnote-web local_folder_tree_shell_does_not_reload_page_for_refresh -- --nocapture` 通过，保留 debug/internal tree shell no-reload 防回归。
- 2026-05-15 真实浏览器 smoke：新增并执行 `MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3427 node scripts/task435-local-folder-watch-no-reload-smoke.js` 通过。脚本使用临时本地目录，验证 Markdown 外部 create / rename / delete 在 page tree 与 filetree 原地更新；`.txt` 与 `.png` 两类非 md 资源外部 create / rename / delete 在 filetree 原地更新；所有步骤 `navigationEventsBefore=0`、`navigationEventsAfter=0`，结果文件 `tmp/task435-local-folder-watch-no-reload-smoke/result.json`。
- 2026-05-15 `resync_required` 合同修正：只读审计确认前端 `applyTreeStreamDelta` 对 `op=resync_required` 不替换数据，只推进 cursor；已补 `wolai-frontend/src/lib/tree-stream/use-sidebar-tree-stream.test.tsx`，断言 `resync_required` delta 保持旧数据、后续 `resync` 才替换数据。已补 `rust/crates/mnote-web/src/routes/stream_support.rs`，断言 matching command/domain event 中的 `streamDelta.op=resync_required` 会作为 delta 透传并推进 cursor。
- 2026-05-15 验证：`cd wolai-frontend && pnpm test src/lib/tree-stream/use-sidebar-tree-stream.test.tsx -- --runInBand` 通过，1 个测试文件、4 项通过；`cargo test --manifest-path rust/Cargo.toml -p mnote-web stream_change_preserves_resync_required_delta_contract -- --nocapture` 通过，1 项通过。

### 阶段 9：Local Folder watcher 与本地资源生命周期

- [x] Markdown 外部 watcher 三件套：外部创建 `.md`、外部重命名 `.md`、外部删除 `.md`，page tree 与 filetree 均无需手动刷新可见。
- [x] 打开的本地 Markdown 文档在外部修改 / 重命名 / 删除时，能收到明确刷新或冲突提示，不静默覆盖用户编辑。
- [x] 非 md 本地资源文件外部 create / rename / delete 在 filetree 可见，并记录实际机制是 `/api/tree/local-folder-watch` revision polling + HTML snapshot apply，不是 `LocalFolderWatcherRegistry` SSE delta，也不是 browser reload。
- [x] 非 md 本地资源文件 delete / restore / purge 支持本地垃圾箱或明确不支持；若支持，必须记录原路径、冲突策略与 purge 后索引清理。
- [x] 本地 folder watcher 验收脚本必须记录 `navigationEvents` / reload 次数；若发生 `window.location.reload()`，不得标为 no-refresh 完成。

验收标准：

- Markdown watcher 不依赖手动刷新；若内部实现仍需 reload，文档必须标为“reload 型刷新”，不能写成 no-refresh。
- 非 md 资源文件至少覆盖 `.txt/.png/.pdf/.xlsx` 中两类，create / rename / delete 后 filetree 状态与本地文件系统一致。
- 本地资源进入垃圾箱时必须写入 `.mnote/trash` 或等价索引；restore 回原路径，冲突时有可解释 fallback；purge 后原文件、trash 文件、索引均不可再查询。

执行记录：

- 2026-05-15：只读审计确认 `LocalFolderWatcherRegistry` 递归监听 root，但 `is_markdown_path` 只允许 `.md/.markdown`，非 md 直接过滤；`/api/local-folder/events` 主要服务打开的 Markdown session。
- 2026-05-15：只读审计确认主 tree 对 local folder 不接 `/api/tree/events`，而是用 `/api/tree/local-folder-watch` 轮询 revision；revision 扫描可见条目并 hash path / name / dir / symlink / readonly / size / mtime，非 md 文件也会改变 revision，但当前变化后会触发 `window.location.reload()`。
- 2026-05-15：只读审计确认 `task163` 覆盖外部新增 `watcher-added.md` 后 page tree 出现，以及外部新增 / 删除 `watcher-asset.txt` 后 filetree 出现 / 消失；缺少外部 Markdown rename / delete、非 md rename、非 md trash / restore / purge 的写入型浏览器验收。
- 2026-05-15 继续执行审计：`rust/crates/mnote-web/src/local_folder_watcher_registry.rs` 的 `should_emit_event_kind` 接受 create/remove/name/data/metadata，但实际发送前仍按 `is_markdown_path` 过滤；`rust/crates/mnote-web/src/routes/local_folder_events.rs` 也会按 `documentId` 限定单个 Markdown 相对路径，因此它更适合打开文档内容刷新，不足以承担完整 filetree 资源 watcher。
- 2026-05-15 继续执行审计：`rust/crates/mnote-web/src/routes/local_folder_source.rs` 的 `local_folder_watch_revision_for_root` 会遍历 visible files 并把 path/name/dir/symlink/readonly/size/mtime 纳入 hash，因此它可以发现 Markdown rename/delete 与非 md create/rename/delete；缺口在客户端收到 revision 变化后执行整页 reload，而不是应用 snapshot delta。
- 2026-05-15 执行记录：本轮已把 3000 主 Sidebar local folder watch 从“static / reload 型”改为 revision polling + HTML snapshot apply。`task435` 记录 `navigationEvents=[]`，覆盖 Markdown page tree 三件套、Markdown filetree 三件套、`.txt` 与 `.png` 非 md filetree 三件套。
- 2026-05-15 打开态本地 Markdown 修正：新增 `scripts/task436-local-markdown-open-document-external-change-smoke.js`，先红灯确认外部重命名后的打开 session 未进入 `external-change-conflict`，根因是 `/api/page-aggregate/<旧 documentId>` 404 被静默忽略，且 local folder SSE 对 rename/remove 事件按新 `documentId` 过滤后不能可靠触达旧 session。
- 2026-05-15 打开态本地 Markdown 修正：`rust/crates/mnote-web/src/routes/web_shell.rs` 已改为本地 Markdown 外部刷新遇到非 OK aggregate 响应时标记 `external-change-conflict`；对 `eventKind` 包含 `Remove` / `Name` 的 local folder 事件，不再仅按新 `documentId` 过滤，而是让同 root 打开的 session 重查当前 aggregate，由 conflict key 或 404 决定自动同步或冲突提示。
- 2026-05-15 验证：`MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3427 node scripts/task436-local-markdown-open-document-external-change-smoke.js` 通过，覆盖打开文档外部修改自动同步、dirty 文档外部修改冲突提示、打开文档外部重命名冲突提示、打开文档外部删除冲突提示；四步 `navigationEventsBefore=0`、`navigationEventsAfter=0`，结果文件 `tmp/task436-local-markdown-open-document-external-change-smoke/result.json`。
- 2026-05-15 验证：`cargo test --manifest-path rust/Cargo.toml -p mnote-web document_shell_renders_local_markdown_with_same_sidebar_surfaces -- --nocapture`、`cargo test --manifest-path rust/Cargo.toml -p mnote-web sidebar_tree_runtime_polls_local_folder_without_browser_reload -- --nocapture`、`cargo test --manifest-path rust/Cargo.toml -p mnote-web local_folder_tree_shell_does_not_reload_page_for_refresh -- --nocapture` 均通过。
- 2026-05-15 非 md 本地资源生命周期设计：已在 `design/04-tree-domain/process/4-27-resource-lifecycle-command-cutover-v1.md` 追加 `local_folder file_path resource lifecycle` 子阶段，明确 `tree.resource.archive/restore/purge`、`resourceKind=local_file`、`trashEntryId`、trash index、restore 冲突与 purge 幂等策略。
- 2026-05-15 非 md 本地资源生命周期实现：先补 `tree_command_local_folder_asset_trash_restore_and_purge_use_trash_index` 红灯，确认 `local:asset:docs/photo.png` delete 原本掉到 Markdown 删除路径；随后扩展 local folder executor，使 `local:asset:*` / `local:node:*` raw file 通过 `tree.resource.archive/restore/purge` 进入 `.mnote/trash` 与 `trash-index.json`，restore 回原路径，purge 删除 trash 文件并清理索引。Markdown 页面原有 trash / restore / purge 回归保持不变。
- 2026-05-15 UI 接入：Rust 3000 主壳 filetree delete plan 对 `sourceKind=local_folder && rowKind=asset` 使用 rowId 作为本地资源 identity，Delete / Backspace 走 `/api/tree/commands`，不再落到 `/api/media/batch`；补 `sidebar_filetree_delete_keys_support_mixed_doc_and_asset_selection` 字符串级回归。
- 2026-05-15 真实浏览器 smoke：新增并执行 `MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3427 node scripts/task437-local-folder-asset-trash-lifecycle-smoke.js` 通过。脚本在本地临时 root 下创建 `docs/asset.txt`，主壳 filetree 选中后按 Delete，确认源文件消失、`.mnote/trash/asset.txt` 出现、trash index 包含 `local-file:docs/asset.txt`；随后通过 tree command restore 回原路径，再次 delete 后 purge，确认源文件、trash 文件和 trash index entry 均不存在；全程 `navigationEvents=[]`。
- 2026-05-15 阶段 9 结论：Markdown 外部 watcher、打开态 Markdown 外部变更提示、非 md 外部可见性、本地非 md 资源 delete / restore / purge 与 no-reload 记录均已闭合；后续更完整的 `.pdf/.xlsx` 类型矩阵、restore 冲突 UI、Trash 工作台展示 local_file entry 可作为下一轮增强，不阻塞本阶段 checklist。

### 阶段 10：Explorer parity 收口项

- [x] F2 inline rename validation：空名、非法名、同级重名、扩展名策略行内提示，并阻断提交。
- [x] Copy Relative Path：Convex workspace-relative 路径已接入页面右键菜单；local root-relative 仍归入 local folder 阶段 9 验收。
- [x] Paste Into action target：主 Sidebar 节点右键使用当前菜单节点作为目标；Ctrl/Cmd+V 继续使用 focused target；空白区当前没有独立右键菜单入口，后续若新增必须复用同一 helper。
- [x] New Folder：当前 Convex workspace 页面树不支持 folder 概念，禁用态保留解释；后续若支持，必须等待 Resource Tree folder 合同并走 tree command。
- [x] `resync_required` 合同：前端明确 `resync_required` 只推进 cursor、不替换数据；后续 snapshot / resync 才替换数据。Rust stream 合同测试覆盖 matching command/domain event 的 `resync_required` delta 透传。

验收标准：

- VSCode Explorer 对标只能按“最低可用完成 / parity backlog”分层汇报，不能把禁用态或待补项描述成完整完成。
- 所有右键命令与键盘命令必须共享同一 action target 规则。
- 事件合同测试能复现“只发 resync_required 不发 resync 时 B 端不会更新”的失败模式，并用实现修复后变绿。

执行记录：

- 2026-05-15：`use-sidebar-tree-stream.test.tsx` 已补 `resync_required delta 只推进 cursor，后续 resync 才替换数据`；`stream_support.rs` 已补 `stream_change_preserves_resync_required_delta_contract`。这固定了当前合同口径：`resync_required` 是“需要 resync 的信号”，不是可直接应用的数据 delta。

## 4. 最小浏览器验收脚本建议

测试数据前缀：`TEST-10REVIEW-07-<timestamp>`。

必须覆盖：

- 登录 `http://localhost:3000/auth`，使用测试账号快速登录。
- 在 3000 主入口创建父页面、子页面、附件、mindmap、table。
- 文件树多选父页面 + 子页面 + 附件，按 Delete。
- 断言：文件树立即消失，垃圾箱 Drawer 或 `/trash` 立即出现相应条目。
- 恢复父页面，断言子页面恢复规则、位置、focus/reveal。
- 对同一批对象执行彻底删除 / 清空垃圾箱。
- 查询 Convex 或通过 API 断言 purge 后不存在。
- 双浏览器验证不刷新同步。

## 5. 不应误读的口径

- 不应说“04-tree 文件树已完全对标 VSCode Explorer”。更准确的说法是：文件树 renderer、selection、preflight、部分批量删除和垃圾桶基础已经具备；VSCode Explorer 产品闭环仍在收口。
- 不应说“Trash 已完整完成”。更准确的说法是：页面软删 / 恢复 / purge 与 Sidebar Drawer 基础已存在；Rust 主壳、`/trash` 入口、资源级命令、清空后 Convex 同步删除还需要端到端验收。
- 不应说“Resource Tree runtime 已完成”。更准确的说法是：object identity、block-asset relation、file_tree projection 合同已落地；资源树产品面与资源命令闭环待核验。
- 不应说“local folder 已满足 no-refresh”。更准确的说法是：local folder Markdown lifecycle 与非 md 文件基础可见性已有 smoke 覆盖，但当前本地 watch 仍有 `window.location.reload()`，非 md 资源 trash / restore / purge 未收口。
- 不应说“mindmap / table 已正式切到 tree.resource.*”。更准确的说法是：mindmap / table 生命周期已有 compat route 与 no-refresh 证据，正式资源命令合同仍待补。
- 不应把 07-ai 开发建立在“04-tree 删除 / 垃圾箱 / Explorer 能力已经全闭环”的假设上。后续 07-ai 若需要操作页面、附件、mindmap、table，必须优先走统一 tree command 和 projection，不要继续扩散 legacy route。
