# 4-26 [done][bug] Sidebar File Tree projection 在树/资产变更后不刷新 v1

> 更新时间：2026-05-14
>
> 分类归属：
> - `04-tree-domain/done`
> - 关联缺陷：`bugs/04-tree-domain/done/4-25-tree-command-create-delete-reload-latency-v1.md`
> - 涉及边界：`05-editor-mainline/sidebar shell`、`06-mindmap/mindmap asset`
>
> 用户反馈：
> - “新建页面/删除页面必须得刷新浏览器才能出现新页面或看到页面删除。”
> - “包括新建思维导图，也是得刷新才能看到。”
> - “有点像是全局事件被禁用了或类似的。”

## 1. 问题定义

当前主页面 Sidebar / File Tree 在页面新建、页面删除、mindmap 新建后没有即时反映最新 projection。浏览器刷新后能看到正确结果，说明后端数据已经写入，问题集中在 `tree command / asset mutation -> sidebar projection -> File Tree UI` 的前端刷新和局部 apply 链路。

这不是单纯的“整页 reload 太慢”问题，而是 4-25 去掉强刷新后暴露出的新回归：普通命令不再强制刷新浏览器，但 File Tree projection 也没有被同步更新。

## 2. 真实现象

已确认现象：

1. 新建页面后，当前页面壳里的 File Tree / Sidebar 不能立即看到新页面。
2. 删除页面后，当前页面壳里的 File Tree / Sidebar 仍可能保留旧页面行。
3. 新建 mindmap 后，当前页面壳里的 File Tree 不能立即看到 `mindmap-<id>.json` 资源行。
4. 手动刷新浏览器后，页面或 mindmap 资源会按后端最新数据出现/消失。

期望结果：

1. `tree.node.create` 成功后，Page Tree 与 File Tree 都能在当前页面壳中即时出现新页面。
2. `tree.node.archive` / 删除成功后，Page Tree 与 File Tree 都能在当前页面壳中即时移除目标页面及子树。
3. `mindmaps.put(createOnly=true)` 或新 mindmap mount 成功后，File Tree 能即时出现对应 mindmap asset row。
4. 不依赖浏览器刷新作为普通成功路径；刷新只能作为 projection 无法局部 apply 时的显式 fallback。

## 3. 确认证据

### 3.1 主 Sidebar 的数据源仍来自 preferred snapshot

`wolai-frontend/src/components/sidebar/sidebar.tsx:201` 到 `:209`：

- Sidebar 通过 `usePreferredSidebarSnapshot()` 在 `initialData`、`sidebarQuery.data`、`treeStream.data` 之间选择数据源。
- 后续 File Tree projection 仍读取这个 `sidebarData`。

### 3.2 Page Tree 有本地 setTree，但 File Tree projection 没有同步局部更新

`wolai-frontend/src/components/sidebar/sidebar.tsx:305` 到 `:316`：

- `sidebarData.kernelSidebarTree` 变化时会同步到本地 `tree` state。

`wolai-frontend/src/components/sidebar/sidebar.tsx:1900` 到 `:1957`：

- `handleCreate()` 在 `createDocumentCommand()` 成功后只对页面树做本地 `setTree()`，然后 `router.push()`。
- 该路径没有同步更新 `sidebarData.kernelFileTreeProjection.items`，也没有广播 `emitDocumentsChanged()`。

`wolai-frontend/src/components/sidebar/sidebar.tsx:2239` 到 `:2249`：

- `handleDelete()` 成功后调用 `removeDocumentsFromTree()` 和 `emitDocumentsChanged()`。
- 该本地删除只影响 Page Tree 的 `tree` state，不会同步移除 File Tree projection items。

### 3.3 File Tree shell 直接依赖 stale projection items

`wolai-frontend/src/components/sidebar/sidebar.tsx:678` 到 `:687`：

- 非搜索状态下 `effectiveResourceTreeShellItems` 直接等于 `sidebarData.kernelFileTreeProjection.items`。
- 如果 preferred snapshot 没有变化，File Tree 行模型就不会包含新页面、删除结果或新 mindmap asset。

`wolai-frontend/src/components/sidebar/sidebar.tsx:695` 到 `:702`：

- `buildFileTreeShellRowById()` 基于 `effectiveResourceTreeShellItems` 构建 row map。
- 即使 `assetById` 里有新 asset，只要 projection items 没有对应 row，File Tree 也无法显示这条资源。

### 3.4 全局事件监听存在，但 refetch 在 Convex live 模式下是空操作

`wolai-frontend/src/components/sidebar/sidebar-events.ts:17` 到 `:28`：

- `wolai:documents-changed` / `wolai:assets-changed` 会触发 `sidebarRefetch()`。
- 这说明全局事件监听并非完全禁用。

`wolai-frontend/src/hooks/use-sidebar-data.ts:93` 到 `:101`：

- `sidebarQuery.refetch()` 在 Convex live subscription 模式下转调 `convexSidebar.refetch()`。

`wolai-frontend/src/hooks/use-convex-sidebar-data.ts:90` 到 `:96`：

- `hasLiveSubscription = shouldFetch`。
- `refetch` 明确是空操作，注释为“Convex 会自动同步数据”。

因此，只要 Convex live query 没有及时推送或 preferred snapshot 仍选择旧 tree stream/query 数据，`emitDocumentsChanged()` / `emitAssetsChanged()` 对 File Tree projection 就不会强制拉取新 snapshot。

### 3.5 mindmap 新建只乐观补 asset 列表，不补 File Tree projection row

`wolai-frontend/src/components/editor/blocks/MindmapBlock.tsx:1527` 到 `:1534`、`:1591` 到 `:1621`、`:1636` 到 `:1647`：

- mindmap 保存、初始同步、实例就绪都会调用 `emitAssetsChanged(docId, asset)`。

`wolai-frontend/src/components/sidebar/sidebar.tsx:506` 到 `:528`：

- Sidebar 收到带 `asset` 的事件后会把 mindmap 加入 `mindmapAssets` 本地 state。
- 但 File Tree 的 row 输入仍是 `sidebarData.kernelFileTreeProjection.items`，没有把新 mindmap asset materialize 成 projection item。

这解释了“新建思维导图也必须刷新浏览器才能看到”：asset 本地列表可能更新了，但 Rust-family File Tree shell 的可见行不来自这个列表本身，而来自 stale file projection。

## 4. 当前判断

该问题已通过代码路径确认，属于事件/刷新链路和 projection 局部 apply 缺口：

1. 事件监听存在，不是全局事件完全被禁用。
2. 文档变更事件在 create 路径缺失，在 delete 路径存在但只触发空 refetch。
3. asset 变更事件能把 mindmap 加进本地 asset state，但不能生成 File Tree projection row。
4. File Tree shell 以 `kernelFileTreeProjection.items` 为事实源，当前缺少对 create / archive / mindmap asset create 的局部 reducer 或强制 projection reload。
5. 浏览器刷新后能看到正确结果，进一步说明后端写入成功，前端 projection 没有及时刷新。

## 5. 建议修复方向

1. 将 `tree.node.create` / `tree.node.archive` 的 command result 或 stream delta 同步应用到 `kernelFileTreeProjection.items`，不要只更新 Page Tree state。
2. `emitDocumentsChanged()` 应能触发一个真实的 projection refresh：在 Convex live 模式下不能继续是空操作，至少需要 invalidate/fetch `/api/sidebar` 或 `/api/tree/projections/file`。
3. `emitAssetsChanged(asset)` 对 mindmap create 应能 materialize 对应 File Tree asset row，或触发 File Tree projection 重新拉取。
4. `usePreferredSidebarSnapshot()` 需要避免旧 tree stream snapshot 长时间压住更新后的 query/http projection。
5. 增加主页面 Sidebar smoke，覆盖真实 `/documents/<id>` 页面内的新建页面、删除页面、新建 mindmap，断言不刷新浏览器即可看到 UI 变化。

## 6. 修复记录

2026-05-14 已完成修复：

1. `wolai-frontend/src/components/sidebar/sidebar-local-projection.ts`
   - 新增 Sidebar 本地 documents/file-tree projection helper。
   - `tree.node.create` 成功后可将新页面 upsert 到本地 documents，并重建 File Tree projection。
   - `tree.node.archive` / 删除成功后可从本地 documents 中移除目标页面及子树，并重建 File Tree projection。
   - mindmap asset 进入本地 `mindmapAssets` 后，可 materialize 成 File Tree asset row。
2. `wolai-frontend/src/components/sidebar/sidebar.tsx`
   - 新增本地 `documents` state，并随后端 sidebar snapshot 校准。
   - File Tree 非搜索状态改为消费 `localFileTreeProjection.items`，不再只依赖 stale `sidebarData.kernelFileTreeProjection.items`。
   - `handleCreate()` 成功后同步 upsert documents、广播 `emitDocumentsChanged(nextNode.id)`，Page Tree 与 File Tree 都可即时反映。
   - `handleDelete()` / 文件树批量删除成功后同步移除 documents 子树，File Tree 不再保留旧 projection row。
   - rename / move / Rust-family tree shell mutation 也同步更新本地 documents，避免 File Tree row 标题或父级滞后。
3. `wolai-frontend/src/hooks/use-sidebar-data.ts`
   - Convex live 模式下的显式 `refetch()` 不再只是空操作；现在会拉取 `/api/sidebar?workspaceId=<id>` 作为手动 snapshot 补偿。
   - 手动 snapshot 绑定 workspaceId，避免跨工作区污染。
4. `wolai-frontend/src/components/sidebar/sidebar-sync.ts`
   - `buildSidebarDataSyncKey()` 纳入顶层 `documents`，避免只有 documents 变化时稳定缓存仍命中旧 sidebar data。
5. `rust/crates/mnote-web/src/ssr/pages/layout.rs`
   - 追加修复 Rust 3000 主文档壳路径：`tree:local-command` 现在会在 `create / purge / delete / archive` 命令成功后本地 apply Sidebar DOM。
   - `create` 成功后立即插入 Page Tree 行，以及 File Tree 的 `doc:<id>` / `index:<id>` 行。
   - `purge/delete/archive` 成功后立即从 Page Tree 与 File Tree 移除目标页面行。
   - 命令成功后清理按钮 pending 状态，避免无整页刷新时按钮残留禁用。
6. `rust/crates/mnote-web/src/ssr/pages/layout.rs`
   - 追加修复 Rust 3000 页面内 mindmap 创建路径：主文档壳现在同时监听 `wolai:assets-changed`，并观察同页 `/api/mindmap/<doc>/<mindmap>` POST 成功请求。
   - mindmap 创建成功后立即把 `mindmap-<id>.json` 作为 File Tree asset row 插入当前页面下，避免等刷新后 projection 才出现。
   - 这覆盖 Leptos/Rust island 未触发 React `emitAssetsChanged()` 的路径。

2026-05-14 回归复现与补修：

- 用户复测发现 3000 主入口仍需刷新。重新用真实浏览器验证后确认：之前 `3001` Next 路径和 `/tree` debug 路由的验证不能代表当前 `mnote-web` 3000 主文档壳。
- 复现证据：旧 3000 进程为 `/mnt/Data1T/mnote/rust/target/debug/mnote-web (deleted)`；点击“新建页面”后接口返回成功并跳转到 `/documents/tree_1778714360642_10?...`，但 5 秒后 `#sidebar-tree-root` 仍不包含该 document id。
- 当前源码 3002 在补修前也复现同类问题：新建后跳转到 `/documents/tree_1778714400752_12?...`，但 Page Tree DOM 不包含当前 document id。
- 补修后已重启 3000，并在 3000 主入口完成新建/删除无刷新 smoke。
- 用户继续复测确认页面新建/删除已恢复，但页面内新建思维导图仍需刷新。真实 smoke 复核后确认：3000 页面内导图创建会走 `/api/mindmap/<doc>/<mindmap>` 写入链路，主文档壳原先没有把该成功写入同步 materialize 到 File Tree asset row。
- 补修后已在 3000 主入口通过 slash 菜单真实新建思维导图，并确认 File Tree 无刷新出现 `asset:<mindmapId>` 行。

## 7. 验收标准

只有满足以下条件后才能移动到 `bugs/04-tree-domain/done/`：

1. [x] 在 `http://127.0.0.1:3000/documents/<id>` 主页面壳中新建页面后，不刷新浏览器即可在 Page Tree 与 File Tree 中看到新页面。代码证据：React Sidebar 路径由 `handleCreate()` 本地 upsert documents；Rust 3000 主文档壳由 `tree:local-command` 本地插入 Page Tree 行与 File Tree `doc:<id>` / `index:<id>` 行；单测覆盖：`sidebar-local-projection.test.ts`，Rust runtime 覆盖：`cargo test -p mnote-web sidebar_tree_runtime -- --nocapture`。
2. [x] 在主页面壳中删除页面后，不刷新浏览器即可在 Page Tree 与 File Tree 中看到目标页面消失。代码证据：React Sidebar 路径由 `handleDelete()` / `handleDeleteResourceSelection()` 同步 `removeSidebarDocumentRecords()`；Rust 3000 主文档壳由 `tree:local-command` 在 `purge/delete/archive` 成功后调用 remove delta 本地移除；单测覆盖：`sidebar-local-projection.test.ts` 与 `sidebar-delete-preflight-source.test.ts`，Rust runtime 覆盖：`cargo test -p mnote-web sidebar_tree_runtime -- --nocapture`。
3. [x] 在主页面壳中新建 mindmap 后，不刷新浏览器即可在 File Tree 中看到对应 `mindmap-<id>.json` 行。代码证据：React Sidebar 路径由 `emitAssetsChanged(asset)` 进入本地 `mindmapAssets` 并重建 asset row；Rust 3000 主文档壳由 `wolai:assets-changed` listener 和 `/api/mindmap/<doc>/<mindmap>` POST observer 本地插入 File Tree asset row；单测覆盖：`sidebar-local-projection.test.ts`，Rust runtime 覆盖：`cargo test -p mnote-web sidebar_tree_runtime -- --nocapture`。
4. [x] 上述三条路径均不得依赖 `window.location.reload()` 或顶层 navigation。代码证据：本轮没有新增 reload；create/delete 以本地 state/projection apply 为主，显式 refetch 仅作 snapshot 校准。
5. [x] smoke 需要记录事件或本地 apply 标记、最终 DOM 状态与相关网络请求。React Sidebar 路径保留 `tmp/task426-sidebar-main-no-reload-smoke/result.json` 作为 Next/3001 与 mindmap asset 证据；Rust 3000 主文档壳新增 `tmp/task426-mnote-web-main-no-reload-smoke/result.json`，覆盖真实 3000 页面新建/删除无刷新 DOM 结果。代码级测试同时覆盖：`sidebar-delete-preflight-source.test.ts` 锁定 create/delete 事件与本地 documents apply；`use-sidebar-data.test.tsx` 锁定 Convex live 显式 refetch 会真实拉 `/api/sidebar`；`cargo test -p mnote-web sidebar_tree_runtime -- --nocapture` 锁定 Rust 主文档壳 runtime 基础行为。
6. [x] 修复后补测试，防止 `sidebarQuery.refetch()` 在 Convex live 模式下继续作为空操作吞掉显式刷新请求。测试覆盖：`use-sidebar-data.test.tsx`。

## 8. 验证记录

已通过：

```bash
cd /mnt/Data1T/mnote/wolai-frontend
pnpm test -- src/components/sidebar/sidebar-delete-preflight-source.test.ts src/components/sidebar/sidebar-local-projection.test.ts src/hooks/use-sidebar-data.test.tsx src/components/sidebar/sidebar-sync.test.ts
```

结果：`113 passed`，`465 passed`。

```bash
cd /mnt/Data1T/mnote/wolai-frontend
pnpm exec eslint src/components/sidebar/sidebar-sync.ts src/components/sidebar/sidebar-sync.test.ts src/components/sidebar/sidebar-local-projection.ts src/components/sidebar/sidebar-local-projection.test.ts src/hooks/use-sidebar-data.ts src/hooks/use-sidebar-data.test.tsx src/components/sidebar/sidebar.tsx
```

结果：无 error；`sidebar.tsx` 保留既有 warning。

真实浏览器 smoke 已通过：

```bash
cd /mnt/Data1T/mnote
MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task426-mnote-web-main-no-reload-smoke.js
```

结果文件：`tmp/task426-mnote-web-main-no-reload-smoke/result.json`。

关键结果：

- `ok=true`
- `baseUrl=http://127.0.0.1:3000`
- `createdDocumentId=tree_1778720530823_2`
- `mindmapId=mindmap_1778720531308`
- 新建页面后：`localApplied=create`、`pageRows=1`、`fileRows=["doc:tree_1778720530823_2","index:tree_1778720530823_2"]`
- 新建思维导图后：`localApplied=true`、`rows=[{"rowId":"asset:mindmap_1778720531308","docId":"tree_1778720530823_2","title":"mindmap-mindmap_1778720531308.json…","kind":"mindmap"}]`
- 删除后：`localApplied=remove`、`pageRows=0`、`fileRows=[]`
- `requests` 包含两次 `POST /api/tree/commands`、一次 mindmap projection GET、两次 `/api/mindmap/<doc>/<mindmap>` POST；无浏览器刷新依赖
- 结果文件：`tmp/task426-mnote-web-main-no-reload-smoke/result.json`

补充对比：

```bash
cd /mnt/Data1T/mnote
MNOTE_UI_BASE_URL=http://127.0.0.1:3002 node scripts/task426-mnote-web-main-no-reload-smoke.js
```

结果：`ok=true`；新建后 `createdDocumentId=tree_1778714773920_2`，Page Tree 与 File Tree 立即出现；删除后两处立即消失。

历史说明：

- 旧记录中的 `MNOTE_UI_BASE_URL=http://127.0.0.1:3001 node <task426-sidebar-main-no-reload-smoke>` 只验证了 Next/React Sidebar 路径，不能作为 Rust 3000 主入口验收依据。
- `/tree` debug route smoke 也不能替代 `/documents/<id>` 主文档壳验收，因为当前默认配置下 debug shell route 可能未启用。

已尝试：

```bash
cd /mnt/Data1T/mnote/wolai-frontend
pnpm exec tsc --noEmit --pretty false
```

结果：失败，但失败集中在既有 Convex / AI / OnlyOffice / 测试类型问题，不是本轮新增文件或本轮修改路径的专属错误。

## 9. 流转条件

当前状态：`done`

本缺陷已完成代码修复与相关测试覆盖，按 bugs 目录规则迁移到 `bugs/04-tree-domain/done/`。
