提交当前顶层 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。
77 KiB
77 KiB
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中“删除到垃圾桶”上下文菜单实际 dispatchaction: 'purge',这是已在阶段 1 止血的最高优先级语义风险;后续仍需防回归。
2. 主要缺口
F-01 P0:Rust Web 主壳“删除到垃圾桶”实际触发永久删除
初始证据:
rust/crates/mnote-web/src/ssr/pages/layout.rs的delete-trash分支显示确认文案“删除到垃圾桶”,但 dispatchaction: '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,Convexdocuments.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 命令面未完全对齐
证据:
- 文件树键盘层会产生
cutSelectionreducer 事件,但全局粘贴链主要读取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-23import/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-frontendDOM 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_requireddelta、不写 snapshot / resync,B 浏览器会停留在旧数据。 - 这类问题表面上会再次表现为“必须刷新浏览器才能看到变化”。
应执行:
- 给 tree event stream 增加合同级回归:每个
resync_required必须伴随后续 snapshot / resync,或客户端必须主动拉取新 snapshot。 - 验收断言包括 cursor 顺序、断线重连、重复 resync 去重。
- smoke 结果中继续记录实际事件来源,不把本地
refreshTree()误标为实时同步。
3. 按顺序执行的 checklist
阶段 1:P0 删除语义止血
- 修正
rust/crates/mnote-web/src/ssr/pages/layout.rs:delete-trash必须 dispatcharchive,不能 dispatchpurge。 - 补单元测试或字符串级回归:菜单文案“删除到垃圾桶”不得与
tree.node.purge绑定。 - 真实浏览器验证: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 commandtree.node.archive、响应deleted_at非空、同页面purge请求数为 0。
阶段 2:统一垃圾箱入口与产品口径
- 决定
/trash是独立页面还是移除入口;如果保留,补真实 Trash 工作台。 - Sidebar Drawer 与
/trash使用同一 dataset:trashed_documents、trashed_media_assets、trashed_mindmap_assets、trashed_table_assets。 - 页面、附件、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/batchrestore、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 同步永久删除
- 创建测试页面、子页面、附件、mindmap、table,删除后均进入对应垃圾箱。
- 清空页面垃圾箱后,确认
documents.emptyTrashByWorkspace或等价 command 删除文档子树及关联数据。 - 清空附件垃圾箱后,确认
mediaAssets.emptyTrashByWorkspace、mindmaps.emptyTrashByWorkspace、tables.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清理关联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创建隔离 workspacews_req_1778793827876_33,seed 页面tree_1778793827972_4、子页面tree_1778793827997_5、附件asset_trash_1778793827654、mindmapmind_trash_1778793827654、tablemp5zx1wjqwssfr4g5jj;删除后确认进入垃圾箱,随后清空页面与资源垃圾箱,并用 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:文件树多选与批量删除
- 文件树创建 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.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会过滤被父页面覆盖的子页面 / 资产;Rustbuild_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和隔离 workspacews_req_1778795393339_3创建 3 页面、2 附件、1 mindmap、1 table;在 3001 主入口 filetree 验证 Ctrl/Cmd 多选、Shift 范围、右键已选保持、右键未选切换。 - 2026-05-15:
task428Delete 轮选择 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:
task428Backspace 轮选择 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 收口
- 新增或确认
tree.resource.archive、tree.resource.restore、tree.resource.purge、tree.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.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/renamecommand 级回归。 - 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_shellcommand 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 / purgeruntime 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
- 删除时记录恢复所需的 parent / order;view path / expanded path 由恢复后的父链展开补齐。
- 恢复页面子树后验证原父节点、排序、展开路径和 active/focus 的代码路径与单测。
- 父节点已永久删除时,提供明确 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.revealpostToHost 事件,但未找到 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 体验补齐
- Cut / Paste move:Ctrl/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 发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.preflightmove 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.tsxfiletree 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-frontendDOM host 与 Rust SSR 主壳layout.rs。父拖子 / 自拖继续由 Rusttree.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 bridgetree.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.renameartifact 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,Convexmedia_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。
- 页面 F2 inline rename:请求
- 2026-05-15 阶段 7 中途剩余:
task430之后 readonly 与命名冲突确认仍未在主 Sidebar Convex filetree 构造真实 route 矩阵;当时 DnD 总项因 readonly / conflict 尚未勾选完成。后续已由task431补齐。 - 2026-05-15:
bridge-runtime的tree.filetree.drop.preflightpayload / 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.escapefallback、普通附件扩展名保留;非法输入不再发 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:实时刷新与双浏览器回归
- 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.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.emptyTrashByWorkspacecommand plan 补tree.trash.documents.emptieddomain event 与resync_requiredstream 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.emptyTrashByWorkspacecommand 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.emptyTrashByWorkspacecommand log、tree.trash.documents.emptieddomain event 与resync_requiredstream 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与隔离 workspacetree_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-webtransport 在 legacymediaAssets:*参数转换时又用 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.emptieddomain event 与resync_requiredstream 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与隔离 workspacetree_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.createddomain event 与resync_requiredstream 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与隔离 workspacetree_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 可见 / 消失;但 RustLocalFolderWatcherRegistry只广播 markdown 事件,当前 filetree 资产可见性主要依赖/api/tree/local-folder-watchrevision 轮询后重载,不是非 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 / purgecommand 只覆盖 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.rsdebug/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_requireddelta 保持旧数据、后续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 与本地资源生命周期
- Markdown 外部 watcher 三件套:外部创建
.md、外部重命名.md、外部删除.md,page tree 与 filetree 均无需手动刷新可见。 - 打开的本地 Markdown 文档在外部修改 / 重命名 / 删除时,能收到明确刷新或冲突提示,不静默覆盖用户编辑。
- 非 md 本地资源文件外部 create / rename / delete 在 filetree 可见,并记录实际机制是
/api/tree/local-folder-watchrevision polling + HTML snapshot apply,不是LocalFolderWatcherRegistrySSE 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轮询 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.pngdelete 原本掉到 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 收口项
- F2 inline rename validation:空名、非法名、同级重名、扩展名策略行内提示,并阻断提交。
- Copy Relative Path:Convex 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_requireddelta 透传。
验收标准:
- 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。