Files
mnote/bugs/04-tree-domain/done/4-26-sidebar-filetree-projection-refresh-stalled-after-tree-asset-mutations-v1.md
T

16 KiB
Raw Blame History

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 shell06-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()initialDatasidebarQuery.datatreeStream.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 没有对应 rowFile 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. http://127.0.0.1:3000/documents/<id> 主页面壳中新建页面后,不刷新浏览器即可在 Page Tree 与 File Tree 中看到新页面。代码证据:React Sidebar 路径由 handleCreate() 本地 upsert documentsRust 3000 主文档壳由 tree:local-command 本地插入 Page Tree 行与 File Tree doc:<id> / index:<id> 行;单测覆盖:sidebar-local-projection.test.tsRust runtime 覆盖:cargo test -p mnote-web sidebar_tree_runtime -- --nocapture
  2. 在主页面壳中删除页面后,不刷新浏览器即可在 Page Tree 与 File Tree 中看到目标页面消失。代码证据:React Sidebar 路径由 handleDelete() / handleDeleteResourceSelection() 同步 removeSidebarDocumentRecords()Rust 3000 主文档壳由 tree:local-commandpurge/delete/archive 成功后调用 remove delta 本地移除;单测覆盖:sidebar-local-projection.test.tssidebar-delete-preflight-source.test.tsRust runtime 覆盖:cargo test -p mnote-web sidebar_tree_runtime -- --nocapture
  3. 在主页面壳中新建 mindmap 后,不刷新浏览器即可在 File Tree 中看到对应 mindmap-<id>.json 行。代码证据:React Sidebar 路径由 emitAssetsChanged(asset) 进入本地 mindmapAssets 并重建 asset rowRust 3000 主文档壳由 wolai:assets-changed listener 和 /api/mindmap/<doc>/<mindmap> POST observer 本地插入 File Tree asset row;单测覆盖:sidebar-local-projection.test.tsRust runtime 覆盖:cargo test -p mnote-web sidebar_tree_runtime -- --nocapture
  4. 上述三条路径均不得依赖 window.location.reload() 或顶层 navigation。代码证据:本轮没有新增 reloadcreate/delete 以本地 state/projection apply 为主,显式 refetch 仅作 snapshot 校准。
  5. 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 applyuse-sidebar-data.test.tsx 锁定 Convex live 显式 refetch 会真实拉 /api/sidebarcargo test -p mnote-web sidebar_tree_runtime -- --nocapture 锁定 Rust 主文档壳 runtime 基础行为。
  6. 修复后补测试,防止 sidebarQuery.refetch() 在 Convex live 模式下继续作为空操作吞掉显式刷新请求。测试覆盖:use-sidebar-data.test.tsx

8. 验证记录

已通过:

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 passed465 passed

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

结果:无 errorsidebar.tsx 保留既有 warning。

真实浏览器 smoke 已通过:

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=createpageRows=1fileRows=["doc:tree_1778720530823_2","index:tree_1778720530823_2"]
  • 新建思维导图后:localApplied=truerows=[{"rowId":"asset:mindmap_1778720531308","docId":"tree_1778720530823_2","title":"mindmap-mindmap_1778720531308.json…","kind":"mindmap"}]
  • 删除后:localApplied=removepageRows=0fileRows=[]
  • requests 包含两次 POST /api/tree/commands、一次 mindmap projection GET、两次 /api/mindmap/<doc>/<mindmap> POST;无浏览器刷新依赖
  • 结果文件:tmp/task426-mnote-web-main-no-reload-smoke/result.json

补充对比:

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_2Page 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 可能未启用。

已尝试:

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

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

9. 流转条件

当前状态:done

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