# 4-28 Trash Restore Location Reveal Focus v1

> 状态：done
> 创建时间：2026-05-15
> 所属主线：04-tree-domain / VSCode Explorer 文件树与垃圾箱闭环
> 来源：`design/10-review/07-vscode-explorer-filetree-trash-gap-review.md` 阶段 6

## 1. 目标

页面进入垃圾箱前必须记录可恢复的位置快照；恢复时优先回到删除前父节点与排序位置，并在 UI 中 reveal / focus 恢复对象。若原父节点已不存在或仍在垃圾箱，应显式 fallback 到根目录并提示用户。

## 2. 已落地范围

- `documents` schema 新增 `restore_parent_id` 与 `restore_sort_order`。
- `documents.softDelete` 对页面子树写入删除前 `parent_id / sort_order` 快照。
- `documents.restore`：
  - 读取同 workspace 当前文档集合。
  - 父节点仍存在或属于本次级联恢复子树时，恢复到原父节点。
  - 父节点已不存在或仍在垃圾箱时，恢复到根目录。
  - 恢复时回写 `parent_id / sort_order`，并对目标父级兄弟节点重新编号，避免恢复到原排序位时产生重复 `sort_order`。
  - 清空恢复位置快照。
  - 返回 `restore_location.parent_id / sort_order / fallback_reason`。
- `/api/tree/commands restore` 读取页面 meta 时允许包含已删除页面，避免垃圾箱页面在进入 restore mutation 前被 `getMeta` 过滤成 404。
- `restoreDocumentCommand` 透传恢复位置结果。
- Sidebar 垃圾箱恢复后刷新数据、展开恢复父链、滚动并 focus 恢复行；发生 fallback 时提示“原父页面已不存在，已恢复到根目录”。
- local folder 非 md 资源 restore 通过 `mnote.pendingLocalFolderRestoreFiletreeRowId` 记录恢复目标，filetree 在加载与轮询刷新时消费该 pending row-id 并聚焦恢复行。

## 3. 设计口径

- 当前记录的是稳定数据位置：`parent_id / sort_order`。
  `view path / expanded path` 不持久化到 Convex，恢复后由 Sidebar 根据当前父链展开得到。
- 子树恢复只恢复与父节点同一 `deleted_at` 批次的子节点，避免把更早独立删除的子页面误恢复。
- fallback 策略先固定为“恢复到根目录 + 明示提示”，不在本阶段引入位置选择弹窗。
- `sort_order` 会按当前兄弟数量 clamp；如果原位置越界，则放到可解释的最近位置。

## 4. 已验证

```bash
cd /mnt/Data1T/mnote/wolai-frontend
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 项测试通过。

补充 route 与排序回归：

```bash
cd /mnt/Data1T/mnote/wolai-frontend
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 项测试通过。

真实浏览器 smoke：

```bash
cd /mnt/Data1T/mnote
node scripts/task429-trash-restore-location-reveal-smoke.js
```

结果：通过。脚本使用一次性用户 / workspace，在 3000 主入口验证；历史记录中曾需手工注册 Convex functions，当前默认 auth/session/control-plane 已切到 SQLite，不再要求先启动 Convex：

- 删除子页面后通过 `tree.node.restore` 恢复，返回 `restore_location.parent_id` 为原父页面、`sort_order` 为原排序位。
- 原排序位已被兄弟页面占用时，兄弟页面被后移，Convex 中不产生重复 `sort_order`。
- 当前浏览器页面不刷新，恢复后的页面行重新出现在侧边栏树中。
- 构造 `restore_parent_id` 指向缺失父节点的历史数据，恢复后返回 `fallback_reason: parent_missing_or_deleted`，并落到根目录。

局部 TypeScript 筛查：

```bash
cd /mnt/Data1T/mnote/wolai-frontend
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` 仍受仓库既有错误影响，未作为本阶段完成条件。

## 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 路径仍未直接调用，但 local folder 已通过 pending row-id 消费实现恢复行 focus。

### 5.2 Local folder 非 md 资源 restore focus（已完成，Worker C 2026-05-21/22 复核）

当前状态：**已完成**。实现通过 pending row-id 传递与 filetree 轮询消费完成，审计证据及源码对照如下：

| 检查项 | 状态 | 证据来源 |
|--------|------|----------|
| `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`，但 filetree 轮询可消费 pending focus |
| `selectSidebarFileTreeDocument` 是否支持 `local-file:` row-id | ❌ 否 | 目前 focus 由 `selectSidebarFileTreeRowById` 走 `local:asset:` row-id 完成 |

### 5.3 已采用方案（供后续复核）

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/22 浏览器复核

- 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/`。
