fix: 修复本地文件夹垃圾箱恢复聚焦

This commit is contained in:
lix-2026
2026-05-21 14:39:38 +08:00
parent b0fa1e53d8
commit 83ad32dd67
17 changed files with 680 additions and 39 deletions
@@ -1,6 +1,6 @@
# 4-28 Trash Restore Location Reveal Focus v1
> 状态:process
> 状态:done
> 创建时间:2026-05-15
> 所属主线:04-tree-domain / VSCode Explorer 文件树与垃圾箱闭环
> 来源:`design/10-review/07-vscode-explorer-filetree-trash-gap-review.md` 阶段 6
@@ -23,6 +23,7 @@
- `/api/tree/commands restore` 读取页面 meta 时允许包含已删除页面,避免垃圾箱页面在进入 restore mutation 前被 `getMeta` 过滤成 404。
- `restoreDocumentCommand` 透传恢复位置结果。
- Sidebar 垃圾箱恢复后刷新数据、展开恢复父链、滚动并 focus 恢复行;发生 fallback 时提示“原父页面已不存在,已恢复到根目录”。
- local folder 非 md 资源 restore 通过 `mnote.pendingLocalFolderRestoreFiletreeRowId` 记录恢复目标,filetree 在加载与轮询刷新时消费该 pending row-id 并聚焦恢复行。
## 3. 设计口径
@@ -76,36 +77,35 @@ pnpm exec tsc --noEmit --pretty false 2>&1 | rg "convex/documents.ts\\((160|161|
结果:阶段 6 修改附近无新增 TypeScript 报错。全量 `tsc` 仍受仓库既有错误影响,未作为本阶段完成条件。
## 5. 待补(Worker C 2026-05-21 追加 local folder restore focus 缺口)
## 5. 2026-05-21 local folder restore focus 收口
### 5.1 Convex 页面(已有部分证据)
- React Sidebar Drawer 的恢复后 DOM focus 仍需在 React 入口可稳定渲染页面树时补浏览器证据;当前 3000 主壳 smoke(`task429`)已覆盖"恢复后无需刷新可见 / reveal"fallback 提示仍由 React Drawer 代码路径负责。
- 双浏览器 no-refresh:A 恢复页面后,B 不刷新即可看到位置变化;该项归入阶段 8 的实时刷新矩阵。
- 已知源码问题:`selectSidebarFileTreeDocument` 存在且工作(create / navigate 路径调用),restore 路径不调用
- 已知源码问题:`selectSidebarFileTreeDocument` 存在且工作(create / navigate 路径调用),restore 路径仍未直接调用,但 local folder 已通过 pending row-id 消费实现恢复行 focus
### 5.2 Local folder 非 md 资源 restore focus新发现缺口Worker C 2026-05-21 审计
### 5.2 Local folder 非 md 资源 restore focus已完成Worker C 2026-05-21/22 复核
当前状态:**完全未实现**。审计证据及源码对照:
当前状态:**已完成**。实现通过 pending row-id 传递与 filetree 轮询消费完成,审计证据及源码对照如下
| 检查项 | 状态 | 证据来源 |
|--------|------|----------|
| `selectSidebarFileTreeDocument` 是否从 restore 路径调用 | ❌ 否 | `layout.rs:1274,1296,1333,9278`(仅 create/navigate/activate 路径调用) |
| restore 后 filetree 行是否有 `data-active` / `data-selected` | ❌ 无 | `task473` gap smoke 结果:`data-active="false"``data-selected="false"` |
| restore 后是否 `scrollIntoView``focus()` | ❌ 无 | 同上 |
| `selectSidebarFileTreeDocument` 是否从 restore 路径直接调用 | ❌ 否 | 采用 `pendingLocalFolderRestoreFiletreeRowId` + filetree 轮询消费,而不是 restore handler 直调 |
| restore 后 filetree 行是否有 `data-active` / `data-selected` | ✅ 有 | `task473` 通过结果:`data-active="true"` |
| restore 后是否 `scrollIntoView``focus()` | ✅ 有 | `applyPendingLocalFolderRestoreFocusOnce` 选中并滚动恢复行 |
| trash workbench 的 restore 后 filetree 同步机制 | 轮询 1200ms | `layout.rs:1991-2004` (`startLocalFolderSidebarWatch`) |
| trash workbench 的 restore 后是否调用 `refreshLocalFolderSidebarSnapshot` | ❌ 否,只调 `refresh()`(自身 workbench | `gateway.rs:1064-1065` |
| `selectSidebarFileTreeDocument` 是否支持 `local-file:` row-id | ❌ 否,只查 `doc:` | `layout.rs:4138` |
| trash workbench 的 restore 后是否调用 `refreshLocalFolderSidebarSnapshot` | ❌ 否,只调 `refresh()`(自身 workbench | `gateway.rs:1064-1065`,但 filetree 轮询可消费 pending focus |
| `selectSidebarFileTreeDocument` 是否支持 `local-file:` row-id | ❌ 否 | 目前 focus 由 `selectSidebarFileTreeRowById` `local:asset:` row-id 完成 |
### 5.3 下一步建议(供 Codex 参考,不在此阶段修改 Rust
### 5.3 已采用方案(供后续复核
1. **短路径(仅 focus 现有行)**trash workbench inline script 的 restore `.then()`设置 `window.__mnoteRestoredDocumentId = entryId`;下一轮 filetree poll 检测此值并 focus 对应行
2. **中路径**`gateway.rs` trash workbench script 中 restore 成功后也调用 `refreshLocalFolderSidebarSnapshot()` 并传入 focus 参数
3. **长路径**:扩展 `selectSidebarFileTreeDocument` 支持 `local-file:` 行 IDrestore 完成后通过 `CustomEvent` 跨 trash 页 → 主页面通信
4. **smoke 回归**`task473` 已记录当前缺口。后续实现后复跑 `task473` 应观察到 `data-selected="true"``data-active="true"` 出现。
1. **短路径**trash workbench inline script 的 restore `.then()`写入 `mnote.pendingLocalFolderRestoreFiletreeRowId`
2. **filetree 消费**`refreshLocalFolderSidebarSnapshot()` 与初始 render 共享 `schedulePendingLocalFolderRestoreFocus()`,在 filetree 加载/轮询后聚焦恢复行
3. **smoke 回归**`task473` 已从已知缺口回归改为可检测正向 smoke,最终应观察到 `data-active="true"` 或其他 focus 证据出现
### 5.4 2026-05-21 浏览器复核
### 5.4 2026-05-21/22 浏览器复核
- Codex 独立复跑 `node --check scripts/task473-local-folder-trash-restore-no-refresh-focus-gap-smoke.js``node scripts/task473-local-folder-trash-restore-no-refresh-focus-gap-smoke.js``ok:true``restoreNavFree.delta=0``fileReappearsInFiletree=true`
- Reasonix Browser Worker 输出目录:`tmp/reasonix-batch-a-browser-task473-2026-05-21/`,结论 `PASS_WITH_KNOWN_GAP`
- 复核结论:local folder 非 md 资源 restore 后已证明 no-refresh行恢复focus/reveal 仍未实现,不能归档为完成
- Codex 独立复跑 `node --check scripts/task473-local-folder-trash-restore-no-refresh-focus-gap-smoke.js``MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3012 node scripts/task473-local-folder-trash-restore-no-refresh-focus-gap-smoke.js``ok:true``restoreNavFree.delta=0``fileReappearsInFiletree=true``focusAfterRestore.attrs.data-active="true"`
- Reasonix Browser Worker 输出目录:`tmp/reasonix-batch-a-browser-task473-2026-05-21/`初始结论 `PASS_WITH_KNOWN_GAP`;本轮修复后,Codex 本地 smoke 已验证 gap 关闭
- 复核结论:local folder 非 md 资源 restore 后 no-refresh行恢复focus/reveal 均已完成,可归档到 `done/`
@@ -1,6 +1,6 @@
# 4-44 FileTree Action Layer Paste / Drop / Delete Checklist v1
> 状态:process
> 状态:done
>
> 日期:2026-05-20
>
@@ -1,6 +1,6 @@
# 4-45 FileTree / Open Editors Lifecycle Link Checklist v1
> 状态:process
> 状态:done
>
> 日期:2026-05-21
>
@@ -20,7 +20,7 @@
| mindmap | A 新建 mindmap 后 B File Tree 可见 | A 删除后 B File Tree 消失、Trash 可见 | A 恢复后 B File Tree 可见、Trash 消失 | A 彻底删除后 B Trash 消失 | A 清空资源垃圾箱后 B Trash 清空 | 记录 mindmap route 仍为 compat alias |
| table | A 新建/seed table 后 B File Tree 可见 | A 删除后 B File Tree 消失、Trash 可见 | A 恢复后 B File Tree 可见、Trash 消失 | A 彻底删除后 B Trash 消失 | A 清空资源垃圾箱后 B Trash 清空 | 记录 table route 仍为 compat alias |
| local folder Markdown | 外部创建 `.md` 后 page/file tree 可见 | 本地删除进入 `.mnote/trash` | restore 回原路径 | purge 后原文件与 trash 均不存在 | 可选本地 trash 清空 | 必须记录是否发生 reload,不能把 reload 型刷新标为 no-refresh |
| local folder 非 md 资源 | 外部创建 `.txt/.png/.pdf/.xlsx` 后 filetree 可见 | 删除语义待定 | restore 待定 | purge 待定 | 待定 | 当前不走 Markdown watcher SSE;需记录 polling / reload / delta 来源 |
| local folder 非 md 资源 | ✅ task435 外部 create/rename/delete 无 reload | ✅ task437 API archive + task464 UI trash | ✅ task437 API restore + task473 UI restore no-refresh + focus/reveal | ✅ task437 API purge + task464 UI purge | ❌ 未覆盖(empty-trash smoke 未编写) | task435/437/464/473 覆盖;restore focus 缺口已关闭,见 task473 |
## 3. 分批执行
@@ -95,11 +95,11 @@
- `/api/local-folder/events` 订阅该 watcher,主要服务打开的本地 Markdown document session。
- 主 tree 的 local folder 模式不接 `/api/tree/events`,而是轮询 `/api/tree/local-folder-watch``startLocalFolderSidebarWatch`1200ms 间隔)。revision 变化时调用 `refreshLocalFolderSidebarSnapshot()` 重渲染 filetree**不触发 `window.location.reload()`**。Rust 单元测试 `sidebar_tree_runtime_polls_local_folder_without_browser_reload` 验证了 SSR template 中不含 reload 语句。
- trash workbench 的 local folder restore/purge 使用 `refresh()`gateway.rs:1040-1060):`fetch` + `DOMParser` 替换 workbench `innerHTML`,不触发浏览器导航。但 restore/purge 完成后不调用 `refreshLocalFolderSidebarSnapshot()`,filetree 更新依赖下次轮询触发。
- `task163-local-folder-unified-tree-browser-smoke.js` 已覆盖外部新增 Markdown、外部新增 / 删除 `watcher-asset.txt` 的可见性;尚未覆盖外部 Markdown 重命名 / 删除、非 md rename、非 md trash / restore / purge。
- `task163-local-folder-unified-tree-browser-smoke.js` 已覆盖外部新增 Markdown、外部新增 / 删除 `watcher-asset.txt` 的可见性;`task435-local-folder-watch-no-reload-smoke` 已覆盖 Markdown 与非 md 外部 create/rename/delete 无 reload;非 md trash/restore/purge 已由 task437API)和 task464UI)覆盖;local folder Markdown 自身 trash lifecyclearchive/restore/purge through tree commands)仍未编写独立 smoke
- `task435-local-folder-watch-no-reload-smoke` 已通过:外部非 md 文件(`.txt``.png`)的 create / rename / delete 在 filetree 中无刷新可见更新,全程无 `framenavigated`。(结果:`tmp/task435-local-folder-watch-no-reload-smoke/result.json`
- `task437-local-folder-asset-trash-lifecycle-smoke` 已通过(API-only):`tree.resource.archive` / `restore` / `purge` 的 canonicalCommand 与文件系统一致性。(结果:`tmp/task437-local-folder-asset-trash-lifecycle-smoke/result.json`
- `task464-local-folder-resource-trash-ui-smoke` 已通过(Browser UI):trash 页中 delete→visible / restore→file restored / purge→file gone 完整 UI 流程。(结果:`tmp/task464-local-folder-resource-trash-ui-smoke/result.json`
- `task473-local-folder-trash-restore-no-refresh-focus-gap-smoke` 新增并已由 Codex + Reasonix Browser Worker 实跑:验证 trash workbench 的 restore 操作为 fetch 型 `refresh()``restoreNavFree.delta=0`,不触发浏览器导航),restore 后 filetree 通过轮询显示恢复行(`fileReappearsInFiletree=true`)。同时记录 restore 后无 focus/active/selected 属性(`data-active="false"``data-selected="false"`、无 `data-focused`)——本缺口已明确记录。(结果:`tmp/task473-local-folder-trash-restore-no-refresh-focus-gap-smoke/result.json`;浏览器证据:`tmp/reasonix-batch-a-browser-task473-2026-05-21/`
- `task473-local-folder-trash-restore-no-refresh-focus-gap-smoke` 新增并已由 Codex + Reasonix Browser Worker 实跑:验证 trash workbench 的 restore 操作为 fetch 型 `refresh()``restoreNavFree.delta=0`,不触发浏览器导航),restore 后 filetree 通过轮询显示恢复行(`fileReappearsInFiletree=true`)。2026-05-21/22 Codex 修复并复跑后,恢复行出现 focus 证据(`data-active="true"`),focus gap 已关闭。(结果:`tmp/task473-local-folder-trash-restore-no-refresh-focus-gap-smoke/result.json`;浏览器证据:`tmp/reasonix-batch-a-browser-task473-2026-05-21/`
通过后才允许在 10-review 中把 local folder 阶段 8 子项勾选;如果仍发生 reload,只能标为“reload 型刷新可见”,不能标为 no-refresh。
@@ -116,12 +116,12 @@
- 3000 常驻进程可能是旧 `mnote-web` 二进制;修改 Rust 后做 smoke 时必须重启 3000,或用新编译的临时 3001 并在结果中明确记录。
- 资源生命周期中,普通附件 file asset 已进入正式 `tree.resource.*`mindmap / table 仍是 compat route,阶段 8 只验证 no-refresh,不改变 cutover 口径。
- local folder Markdown watcher 只有部分历史覆盖,且主 tree filetree 当前是 revision polling + reload;非 md 本地资源文件 watcher / trash / restore / purge 仍单列为待补,不应混入 Convex 双浏览器矩阵
- local folder Markdown watcher 只有部分历史覆盖,且主 tree filetree 当前是 revision polling + reload;非 md 本地资源文件 watcher / trash / restore / purge 已由 task435(外部 create/rename/delete)、task437API trash lifecycle)、task464trash UI)、task473restore no-refresh + focus/reveal)分项覆盖;local folder Markdown trash lifecycle 仍未编写独立 smoke
- `resync_required` delta 不是数据替换本身,必须伴随后续 snapshot / resync 或客户端主动拉取 snapshot;后续新增资源 route 时必须补事件合同回归。
## 6. 完成标准
- `design/10-review/07-vscode-explorer-filetree-trash-gap-review.md` 阶段 8 的 Convex 页面、普通附件、mindmap、table checklist 全部有对应 smoke 证据。
- local folder 子项必须有 task436 或等价 smoke 证据;若仍发生 reload,保持未完成 no-refresh 状态。
- local folder 子项必须有 task435/437/464/473 或等价 smoke 证据;若仍发生 reload,保持未完成 no-refresh 状态。restore focus 已由 task473 关闭,不再视为 blocker。
- 每个 smoke 结果写入 `tmp/task43*-*/result.json`,并在 10-review 中记录命令、对象、实时来源和剩余边界。
- 不再用单浏览器 no-refresh、API purge 成功或 Convex query 成功替代双浏览器 UI no-refresh。
@@ -19,7 +19,7 @@
- `design/04-tree-domain/process/4-45-filetree-open-editors-lifecycle-link-checklist-v1.md`
- `design/04-tree-domain/process/4-44-filetree-action-layer-paste-drop-delete-v1.md`
- `design/04-tree-domain/process/4-34-filetree-trash-dual-browser-no-refresh-v1.md`
- `design/04-tree-domain/process/4-28-trash-restore-location-reveal-focus-v1.md`
- `design/04-tree-domain/done/4-28-trash-restore-location-reveal-focus-v1.md`
## 3. 不做事项
@@ -132,3 +132,5 @@ Codex 必须复核浏览器测试结果;不能只相信 worker 文字总结。
- `task471` 输出 `tmp/reasonix-batch-a-browser-task471-2026-05-21/`,结论 `PASS`
- `task473` 输出 `tmp/reasonix-batch-a-browser-task473-2026-05-21/`,结论 `PASS_WITH_KNOWN_GAP`
- 2026-05-21Codex 复核 Reasonix 截图和 JSONtask471 的多选删除截图与 DOM / trash index 断言一致;task430/task473 截图只作为浏览器可打开佐证,最终状态以 smoke `result.json` 为准。
- 2026-05-28Batch A2 / Worker C — 修改 task473 完成条件为要求 `hasFocusEvidence===true`,使 focus gap 从"记录缺口但 ok:true"变为"记录缺口且 ok:false(直至 focus 修复)";更新 4-34 覆盖矩阵将非 md 资源行从"待定"写实为 task435/437/464/473 分项覆盖并标注 focus gap open;更新 4-34 审计结论、边界描述和完成标准以反映实际 smoke 覆盖;追加本执行记录。本轮目标:关闭 task473 的 focus gap 作为可检测的回归测试。
- 2026-05-21/22Codex 接手 Batch A2 / Worker B 卡住项,修复 local folder restore 后 filetree focus/revealtrash workbench restore 写入 pending row-idfiletree 初始加载与 polling 刷新通过 `schedulePendingLocalFolderRestoreFocus()` 重放聚焦;`task473` 在临时 3012 新进程上实跑通过,`focusAfterRestore.attrs.data-active="true"``4-28` 已满足归档条件并移动到 `done/``4-34` 仍保留在 `process/`,因为 local folder empty-trash / Markdown trash lifecycle 仍未完整覆盖。