Files
mnote/design/10-review/done/07-vscode-explorer-filetree-trash-gap-review.md
T
lix-2026 384da4e44c feat(tree): checkpoint resource lifecycle work
提交当前顶层 mnote Git 工作区,范围集中在 04-tree-domain 的 resource/trash/filetree 生命周期、mnote-web resource_trash 路由、Convex/Next 兼容接口、sidebar/file-tree 客户端适配、smoke 脚本与对应设计/bug 记录。

不包含被 ignore 的 design/05-editor-mainline/reference-code/leptos-tiptap 嵌套仓库改动。新增 smoke 的测试密码改为运行时读取 MNOTE_E2E_PASSWORD,避免提交明文 credential assignment。
2026-05-16 07:38:45 +08:00

77 KiB
Raw Blame History

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 P0Rust Web 主壳“删除到垃圾桶”实际触发永久删除

初始证据:

  • rust/crates/mnote-web/src/ssr/pages/layout.rsdelete-trash 分支显示确认文案“删除到垃圾桶”,但 dispatch action: 'purge'
  • wolai-frontend/src/app/api/tree/commands/route.tspurge 对应 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 entryhref: "/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 主链中已经走 archiveConvex 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.tstree_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。

应执行:

  • 文件树先补真实浏览器 smokeCtrl/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.archivetree.resource.restoretree.resource.purgetree.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 documentsmedia_assetsmindmapsdocument_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 P1VSCode 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 不是同一 executorwatch 范围也不一致

证据:

  • 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 P1F2 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 P2resync_required 事件合同需要防回归

证据:

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

影响:

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

应执行:

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

3. 按顺序执行的 checklist

阶段 1P0 删除语义止血

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

验收标准:

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

执行记录:

  • 2026-05-15delete-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:统一垃圾箱入口与产品口径

  • 决定 /trash 是独立页面还是移除入口;如果保留,补真实 Trash 工作台。
  • Sidebar Drawer 与 /trash 使用同一 datasettrashed_documentstrashed_media_assetstrashed_mindmap_assetstrashed_table_assets
  • 页面、附件、mindmap、table 均支持恢复、彻底删除、清空。

验收标准:

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

执行记录:

  • 2026-05-15Rust 3000 主壳已挂载 GET /trash,复用 sidebar.datasetListtrashed_* 数据渲染垃圾箱工作台;页面条目提供恢复与彻底删除按钮。
  • 2026-05-15:补 trash_entry_renders_real_workspace_trash_workbench 路由级回归;浏览器 smoke 访问 /trash?workspaceId=<active> 返回 200,出现 mnote-trash-workbench、页面区、资源区、恢复按钮和彻底删除按钮。
  • 2026-05-15Rust 3000 主壳已挂载 POST /api/documents/empty-trash,补 documents_empty_trash_route_executes_workspace_trash_purge 回归;/trash 页面已出现“清空页面垃圾箱”按钮,并只读浏览器 smoke 验证按钮存在,未点击清空以避免删除既有垃圾箱数据。
  • 2026-05-15Rust 3000 主壳已新增资源垃圾箱兼容入口:POST /api/media/batch restore、POST /api/media/purgePOST /api/media/empty-trashPATCH /api/mindmap/{docId}/{mindmapId} restore / purge、POST /api/mindmap-trash/emptyPOST /api/tables/restorePOST /api/tables/purgePOST /api/tables/empty-trash;补 media_trash_routes_restore_purge_and_emptymindmap_trash_routes_restore_purge_and_emptytable_trash_routes_restore_purge_and_empty 回归。
  • 2026-05-15/trash 资源区已从“待迁移”改为真实恢复 / 彻底删除按钮,并新增“清空资源垃圾箱”;路由级 fixture 覆盖附件、mindmap、table 三类资源按钮。
  • 2026-05-15cargo 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 同步永久删除

  • 创建测试页面、子页面、附件、mindmap、table,删除后均进入对应垃圾箱。
  • 清空页面垃圾箱后,确认 documents.emptyTrashByWorkspace 或等价 command 删除文档子树及关联数据。
  • 清空附件垃圾箱后,确认 mediaAssets.emptyTrashByWorkspacemindmaps.emptyTrashByWorkspacetables.emptyTrashByWorkspace 或统一 route 均执行成功。
  • 记录 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 清理关联 mindmapsmedia_assetsdocument_tables / document_table_rowscomment_messages / comment_threadspage_referencesdocument_starsuser_recent_pages;附件底层 Convex Files 只在同一 storage_id 没有其它未清理引用时删除。
  • 2026-05-15 代码审查:mediaAssets.emptyTrashByWorkspace 会清理已过期 deleted_at 或历史 purged_atmedia_assets,并按 storage_id 引用计数决定是否删除 Convex Filesmindmaps.emptyTrashByWorkspace 删除当前用户已软删 mindmaptables.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_emptymindmap_trash_routes_delete_restore_purge_and_emptytable_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_33seed 页面 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 个 mindmaptables.emptyTrashByWorkspace 删除 1 个 table。

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

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

验收标准:

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

执行记录:

  • 2026-05-15 代码审查:wolai-frontend/src/lib/file-tree/selection.tsreduceFileTreeSelection 已支持 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 与 onFileTreeDeleteSelectiontree-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-15task428 Delete 轮选择 root + child + file asset + mindmap + table,确认文案为 1 个页面 + 1 个附件 + 1 个思维导图 + 1 个在线表格,请求为 tree.node.archive/api/media/batch deleteDELETE /api/mindmap/{docId}/{mindmapId}DELETE /api/tables/{tableId};父子页面去重后只发送 root archivechild 随子树删除。
  • 2026-05-15task428 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-15task428 部分失败轮通过 route interception 强制 /api/media/batchasset_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 收口

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

验收标准:

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

执行记录:

  • 2026-05-15 只读核对:阶段 5 未完成。现有正式资源命令只覆盖 tree.resource.copytree.resource.movetree.resource.uploadtree.resource.renametree.resource.archivetree.resource.restoretree.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/batchwolai-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.movetree.resource.uploadrust/crates/storage-convex-bridge/src/mapping.rs 也只映射这三类命令。
  • 2026-05-15 代码证据:rust/crates/mnote-web/src/routes/resource_trash.rswolai-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_emptymindmap_trash_routes_delete_restore_purge_and_emptytable_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 assetmindmap / table 暂保持兼容 alias,不冒充正式 cutover 完成。
  • 2026-05-15bridge-runtime 已新增 tree.resource.archive/restore/purge/rename 生命周期 command plan,普通附件分别映射到 mediaAssets:patchById / mediaAssets:purgeById,并携带 domainEventPlanstreamDeltaHintstorage-convex-bridge 已补默认命令映射。
  • 2026-05-15wolai-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-15Rust 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_commandscargo test -p storage-convex-bridge tree_resource_lifecycle_commands_have_convex_mappingcargo test -p mnote-web trashcargo 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/purgetable -> tables:remove/restore/purge,并断言 resourceLifecyclePlandomainEventHint 与 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 assetmindmap rename 继续明确不支持,table rename 待结合 /api/tables/{tableId} PATCH 与表格标题语义单独收口。empty-trash 仍是 workspace 级批量合同,不伪装成逐资源 tree.resource.purge

阶段 6:恢复位置与 reveal/focus

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

验收标准:

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

执行记录:

  • 2026-05-15 代码审查:wolai-frontend/convex/documents.tsdocuments.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-15wolai-frontend/convex/schema.ts 为 documents 新增 restore_parent_id / restore_sort_orderdocuments.softDelete 删除子树时记录删除前 parent_id / sort_orderdocuments.restore 恢复时优先回放原父节点与排序,并返回 restore_location.parent_id / sort_order / fallback_reason
  • 2026-05-15:恢复父节点仍存在或属于本次级联恢复子树时回到原父节点;原父节点已不存在或仍在垃圾箱时 fallback 到根目录,并清空恢复位置快照。
  • 2026-05-15restoreDocumentCommand 已透传恢复位置;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。

阶段 7VSCode Explorer 体验补齐

  • Cut / Paste moveCtrl/Cmd+X 后 Paste 应移动,不是复制或 no-op。
  • F2 inline rename:文件树行内编辑,不再只靠 prompt。
  • 右键菜单补齐 New File / New Folder / Paste Into / Refresh / Collapse All / Copy Path / Reveal 的可用性或禁用态。
  • DnD 补齐父拖子禁止、拖到自身禁止、readonly 禁止、Alt/Ctrl copy modifier、冲突确认。
  • F2 inline rename validation parity:空名、非法名、同级重名、扩展名策略在行内提示并阻断提交。
  • 右键菜单增强项:Copy Relative Path。
  • 右键菜单增强项:Paste Into action target。
  • 右键菜单增强项: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 发 beginRenameFocusedcutSelectionpasteIntoFocused 等动作。
  • 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-folderConvex 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 的完整端到端矩阵当时未找到。后续已由 task430task431 补齐阶段 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-15wolai-frontend 主 Sidebar 剪贴板 action 已扩展为 copy | cutCtrl/Cmd+X 写入 cut payloadCtrl/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-15tree-shell-dom-host.tsx filetree row 已新增行内 tree-rename-inputF2 进入 inline renameEnter / 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 复用新建子页面,RefreshrefreshTree()Collapse All 清空展开集合,Copy Path 复制页面标题路径,Reveal 展开父链并滚动;New FolderPaste 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-15filetree 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 / RevealRefresh 只发局部刷新事件,不使用 window.location.reload()New FolderPaste Into 保持禁用态并给出原因。
  • 2026-05-15 3000 主入口修正:新增 POST /api/tree/filetree/drop-preflight,通过 Rust bridge tree.filetree.drop.preflight 返回真实 preflight plan;自拖自身与父拖子返回 400copy modifier plan 保留 copy: true
  • 2026-05-15 资源 rename 修正:tree.resource.rename 的 bridge plan 补 patch.file_nameRust /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=renameConvex media_assets.file_name 更新,无需刷新可见。
    • Cut / Paste moveCtrl/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-15bridge-runtimetree.filetree.drop.preflight payload / plan 已补 sourceCapabilitiestargetCapabilitiestargetChildrenconflictPolicyrequiresConfirmationconflictsreadonly target/source 会在 preflight 阶段返回 validation error,同名目标会返回 requiresConfirmation=true,普通 copy modifier 不被 readonly source move 规则误拒绝。
  • 2026-05-15:主 Sidebar buildFileTreeShellInternalDropPreflightPayload 已把 row title、target children 与 capability payload 透传到 preflighthandleResourcePaneInternalDrop 与 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 PathPaste 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+Vnull 保留 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 --quietpnpm 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:实时刷新与双浏览器回归

  • A 浏览器新建、删除、恢复、彻底删除页面,B 浏览器无需刷新可见更新。
  • A 浏览器对普通附件 file asset 执行上传、删除、恢复、彻底删除、清空资源垃圾箱,B 浏览器无需刷新可见更新。
  • A 浏览器对 mindmap / table 执行新建、删除、恢复、彻底删除、清空资源垃圾箱,B 浏览器无需刷新可见更新。
  • 本地 folder 模式验证 markdown 新建 / 重命名 / 删除 watcher 不触发浏览器 reload;非 md 资源文件 watcher create / rename / delete 已覆盖,trash / restore / purge 单列待补。
  • 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.jsscripts/task123-rust-web-tree-live-stream-consumer-smoke.jsscripts/task177-tree-move-archive-live-smoke.jsscripts/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 交给 applyTreeStreamDeltascripts/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 probetask427 是 API/Convex 级 purge 验证,task169 是 mindmap 内容实时稳定性;这些不能替代文件树/垃圾箱双浏览器生命周期矩阵。实现边界上,useSidebarTreeStream 可消费 snapshot/delta/resyncuseConvexSidebarData 依赖 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.emptiedresync_requiredcargo 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 deltapnpm 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_7B1 打开 /documents/tree_1778804545042_6?workspaceId=tree_1778804545042_7 观察 File TreeB2 打开 /trash?workspaceId=tree_1778804545042_7 观察 Trash 工作台。A 侧依次执行页面 create / archive / restore / archive+purge / create+archive+empty-trashB 侧每一步均无需刷新可见更新,navigationEvents 始终为空。事件来源记录:create 使用 tree:resyncarchive 使用 tree:delta remove_documentrestore 使用 tree:delta upsert_documentpurge 使用 tree:resyncempty-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.patchByIdassertWorkspaceMember 处返回“无权访问该工作空间”;已修正为优先保留 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 userIdcargo 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_2B1 打开 /documents/tree_1778806753148_1?workspaceId=tree_1778806753148_2 观察 File TreeB2 打开 /trash?workspaceId=tree_1778806753148_2 观察 Trash 工作台。A 侧依次执行普通附件 upload / archive / restore / archive+purge / upload+archive+empty-trashB 侧每一步均无需刷新可见更新,执行阶段 navigationEvents 为空。事件来源记录:upload 使用 tree:delta upsert_assetsarchive / 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 资源垃圾箱 routenode --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_2A 侧依次执行 mindmap create / archive / restore / archive+purge、table create / archive / restore / archive+purge、mindmap+table create / archive / empty-trashB 侧 File Tree 与 /trash 每一步均无需刷新可见更新,执行阶段 navigationEvents 为空。事件来源记录:mindmap create/delete/restore 主要为 tree:delta resync_requiredmindmap purge 为 tree:resynctable create/delete/restore 主要为 tree:delta resync_requiredtable 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 runtimerust/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/eventsLocalFolderWatcherRegistry 提供 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-watchrefreshLocalFolderSnapshot,且不含 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-watchrevision 变化后 fetch 当前 HTML 并用 replaceSidebarTreeFromDocument() 原地替换 sidebar-tree-rootsidebar-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.reloadcargo 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=0navigationEventsAfter=0,结果文件 tmp/task435-local-folder-watch-no-reload-smoke/result.json
  • 2026-05-15 resync_required 合同修正:只读审计确认前端 applyTreeStreamDeltaop=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 项通过。

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

  • Markdown 外部 watcher 三件套:外部创建 .md、外部重命名 .md、外部删除 .mdpage tree 与 filetree 均无需手动刷新可见。
  • 打开的本地 Markdown 文档在外部修改 / 重命名 / 删除时,能收到明确刷新或冲突提示,不静默覆盖用户编辑。
  • 非 md 本地资源文件外部 create / rename / delete 在 filetree 可见,并记录实际机制是 /api/tree/local-folder-watch revision polling + HTML snapshot apply,不是 LocalFolderWatcherRegistry SSE delta,也不是 browser reload。
  • 非 md 本地资源文件 delete / restore / purge 支持本地垃圾箱或明确不支持;若支持,必须记录原路径、冲突策略与 purge 后索引清理。
  • 本地 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 轮询 revisionrevision 扫描可见条目并 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.rsshould_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.rslocal_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=0navigationEventsAfter=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 -- --nocapturecargo test --manifest-path rust/Cargo.toml -p mnote-web sidebar_tree_runtime_polls_local_folder_without_browser_reload -- --nocapturecargo 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/purgeresourceKind=local_filetrashEntryId、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/trashtrash-index.jsonrestore 回原路径,purge 删除 trash 文件并清理索引。Markdown 页面原有 trash / restore / purge 回归保持不变。
  • 2026-05-15 UI 接入:Rust 3000 主壳 filetree delete plan 对 sourceKind=local_folder && rowKind=asset 使用 rowId 作为本地资源 identityDelete / 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。

阶段 10Explorer parity 收口项

  • F2 inline rename validation:空名、非法名、同级重名、扩展名策略行内提示,并阻断提交。
  • Copy Relative PathConvex workspace-relative 路径已接入页面右键菜单;local root-relative 仍归入 local folder 阶段 9 验收。
  • Paste Into action target:主 Sidebar 节点右键使用当前菜单节点作为目标;Ctrl/Cmd+V 继续使用 focused target;空白区当前没有独立右键菜单入口,后续若新增必须复用同一 helper。
  • New Folder:当前 Convex workspace 页面树不支持 folder 概念,禁用态保留解释;后续若支持,必须等待 Resource Tree folder 合同并走 tree command。
  • 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-15use-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。