@allbluecn/web-app 0.22.4 → 0.23.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (500) hide show
  1. package/package.json +4 -14
  2. package/src/__tests__/integration/login-email-acceptance.test.ts +16 -0
  3. package/src/__tests__/story-view.test.ts +33 -1
  4. package/src/components/agents/__tests__/agent-bindings-tab.test.tsx +221 -0
  5. package/src/components/agents/__tests__/agents-page.test.tsx +284 -0
  6. package/src/components/agents/agent-bindings-tab.tsx +59 -45
  7. package/src/components/agents/agent-form-dialog.tsx +3 -3
  8. package/src/components/agents/agent-selector.tsx +2 -3
  9. package/src/components/agents/agents-page.tsx +66 -31
  10. package/src/components/agents/slash-skill-menu.tsx +2 -3
  11. package/src/components/auth/__tests__/session-guard.test.tsx +74 -0
  12. package/src/components/auth/session-guard.tsx +8 -0
  13. package/src/components/billing/login-prompt-dialog.tsx +1 -1
  14. package/src/components/billing/subscription-provider.tsx +2 -16
  15. package/src/components/bug/__tests__/peek-modes.test.tsx +8 -0
  16. package/src/components/bug/__tests__/subgroup-header.test.tsx +202 -40
  17. package/src/components/bug/peek-panel.tsx +5 -3
  18. package/src/components/bug/views/bug-list/group.tsx +12 -2
  19. package/src/components/bug/views/bug-list/index.tsx +94 -67
  20. package/src/components/command/command-palette.tsx +10 -9
  21. package/src/components/design/canvas/document/frame-custom-presets.ts +9 -1
  22. package/src/components/design/canvas/panels/toolbar.tsx +21 -22
  23. package/src/components/design/canvas/react/__tests__/canvas-context-load-echo.test.tsx +199 -0
  24. package/src/components/design/canvas/react/__tests__/hooks.test.tsx +17 -3
  25. package/src/components/design/canvas/react/canvas-context.tsx +47 -26
  26. package/src/components/design/canvas/react/hooks.ts +39 -34
  27. package/src/components/design/editor/layout/__tests__/collab-param-wiring.test.tsx +17 -0
  28. package/src/components/design/editor/layout/__tests__/context-menu.test.tsx +17 -1
  29. package/src/components/design/editor/layout/__tests__/no-page-save-toast.test.tsx +17 -0
  30. package/src/components/design/editor/layout/__tests__/persistence-restore.test.tsx +17 -1
  31. package/src/components/design/editor/layout/__tests__/ssr-localstorage-smoke.test.tsx +17 -1
  32. package/src/components/design/editor/layout/design-editor-layout.tsx +13 -2
  33. package/src/components/design/editor/layout/hooks/use-design-auto-save.ts +3 -19
  34. package/src/components/devtools/devtools-panel.tsx +56 -0
  35. package/src/components/forms/__tests__/create-app-form.test.tsx +131 -0
  36. package/src/components/forms/app-field-error.tsx +21 -1
  37. package/src/components/forms/app-field.tsx +8 -8
  38. package/src/components/forms/app-submit.tsx +12 -9
  39. package/src/components/forms/create-app-form.tsx +124 -0
  40. package/src/components/forms/index.ts +2 -3
  41. package/src/components/harness/__tests__/gallery-behavior.test.tsx +37 -1
  42. package/src/components/harness/__tests__/harness-workbench-first-send.test.tsx +4 -1
  43. package/src/components/harness/__tests__/message-actions.test.tsx +7 -6
  44. package/src/components/harness/__tests__/use-video-generation.test.tsx +165 -0
  45. package/src/components/harness/ai-chat-history/index.tsx +5 -3
  46. package/src/components/harness/gallery/gallery-card.tsx +57 -35
  47. package/src/components/harness/gallery/gallery-grid.tsx +32 -15
  48. package/src/components/harness/gallery/gallery-types.ts +19 -0
  49. package/src/components/harness/gallery/gallery-video-card.tsx +54 -0
  50. package/src/components/harness/gallery/types.ts +2 -0
  51. package/src/components/harness/gallery/use-video-generation.ts +95 -0
  52. package/src/components/harness/harness-workbench-page.tsx +2 -3
  53. package/src/components/harness/message-actions.tsx +5 -3
  54. package/src/components/hotkeys/shortcuts-panel.tsx +28 -13
  55. package/src/components/iteration/__tests__/burndown-chart.test.tsx +118 -0
  56. package/src/components/iteration/burndown-chart.tsx +54 -23
  57. package/src/components/knowledge/editor/__tests__/knowledge-doc-editor-collab.test.tsx +96 -0
  58. package/src/components/knowledge/editor/knowledge-doc-editor.tsx +56 -1
  59. package/src/components/knowledge/editor/mindmap-doc-editor.tsx +12 -0
  60. package/src/components/knowledge/editor/use-auto-save-channels.ts +3 -1
  61. package/src/components/layout/sidebar-03/__tests__/nav-workspace.test.tsx +0 -4
  62. package/src/components/layout/sidebar-03/nav-knowledge.tsx +11 -11
  63. package/src/components/layout/sidebar-03/nav-main.tsx +12 -15
  64. package/src/components/layout/sidebar-03/nav-notifications.tsx +5 -8
  65. package/src/components/layout/sidebar-03/nav-workspace.tsx +11 -9
  66. package/src/components/notifications/__tests__/settings-notifications.test.tsx +158 -0
  67. package/src/components/notifications/settings-notifications.tsx +37 -13
  68. package/src/components/notifications/team-notifications.tsx +10 -14
  69. package/src/components/settings/personal-team/account/__tests__/account-client.test.tsx +170 -0
  70. package/src/components/settings/personal-team/account/account-client.tsx +44 -7
  71. package/src/components/settings/personal-team/team/team-client.tsx +17 -19
  72. package/src/components/shared-kit/list-kit/__tests__/frozen-header.test.ts +210 -41
  73. package/src/components/shared-kit/list-kit/frozen-header.ts +134 -21
  74. package/src/components/shared-kit/list-kit/kanban/frozen-header.ts +20 -27
  75. package/src/components/story/__tests__/peek-panel-patch-sync.test.tsx +8 -0
  76. package/src/components/story/__tests__/story-list-table.test.tsx +214 -0
  77. package/src/components/story/columns.tsx +11 -2
  78. package/src/components/story/peek-panel.tsx +4 -2
  79. package/src/components/story/story-list-table.tsx +16 -1
  80. package/src/components/story/views/story-list/__tests__/frozen-headers.test.tsx +186 -0
  81. package/src/components/story/views/story-list/group.tsx +12 -1
  82. package/src/components/story/views/story-list/index.tsx +101 -68
  83. package/src/components/testcase/__tests__/case-row.test.tsx +134 -2
  84. package/src/components/testcase/__tests__/peek-panel.test.tsx +8 -0
  85. package/src/components/testcase/__tests__/step-editor.test.tsx +106 -0
  86. package/src/components/testcase/list/grouped-list.tsx +50 -22
  87. package/src/components/testcase/peek-panel.tsx +7 -4
  88. package/src/components/testcase/step-editor.tsx +196 -150
  89. package/src/components/tiptap/notion-like/editor-extensions.ts +0 -3
  90. package/src/components/tiptap/notion-like/notion-like-editor-header.tsx +1 -31
  91. package/src/components/tiptap/notion-like/notion-like-editor.tsx +32 -1
  92. package/src/components/workspace/__tests__/dialogs/create-workspace-dialog.test.tsx +2 -28
  93. package/src/components/workspace/__tests__/dialogs/edit-workspace-dialog.test.tsx +2 -28
  94. package/src/components/workspace/dialogs/create-workspace-dialog.tsx +45 -43
  95. package/src/components/workspace/dialogs/edit-workspace-dialog.tsx +38 -36
  96. package/src/design-engine/engine/engine-decor.ts +2 -0
  97. package/src/design-engine/engine/engine-path.ts +2 -0
  98. package/src/hooks/__tests__/use-unsaved-guard.test.tsx +180 -0
  99. package/src/hooks/use-notifications.ts +9 -16
  100. package/src/hooks/use-unsaved-guard.tsx +87 -0
  101. package/src/lib/__tests__/pacer.test.ts +33 -96
  102. package/src/lib/__tests__/query-defaults.test.ts +169 -0
  103. package/src/lib/__tests__/query-global-error.test.ts +256 -0
  104. package/src/lib/anyhub/anyhub-store.ts +2 -2
  105. package/src/lib/changelog/sdk-versions.ts +2 -10
  106. package/src/lib/collab/presence-store.ts +5 -5
  107. package/src/lib/collab/use-presence-list-refresh.ts +2 -2
  108. package/src/lib/collab/use-share-presence.ts +2 -2
  109. package/src/lib/collections/query-keys.ts +7 -2
  110. package/src/lib/commands/__tests__/command-palette-state.test.ts +12 -14
  111. package/src/lib/commands/__tests__/registry.test.ts +6 -3
  112. package/src/lib/commands/command-palette-state.ts +8 -31
  113. package/src/lib/hotkeys/__tests__/registry.test.ts +71 -18
  114. package/src/lib/hotkeys/registry.ts +188 -85
  115. package/src/lib/hotkeys/types.ts +1 -13
  116. package/src/lib/pacer.ts +1 -79
  117. package/src/lib/queries/README.md +67 -0
  118. package/src/lib/queries/__tests__/agents-queries.test.ts +101 -0
  119. package/src/lib/queries/__tests__/compliance-queries.test.ts +136 -0
  120. package/src/lib/queries/__tests__/notifications-queries.test.ts +175 -0
  121. package/src/lib/queries/__tests__/team-queries.test.ts +87 -0
  122. package/src/lib/queries/agents.ts +40 -0
  123. package/src/lib/queries/compliance.ts +69 -0
  124. package/src/lib/queries/notifications.ts +63 -0
  125. package/src/lib/queries/team.ts +39 -0
  126. package/src/lib/query-defaults.ts +8 -0
  127. package/src/lib/query-global-error.ts +128 -0
  128. package/src/lib/remote-auth-client.ts +16 -0
  129. package/src/paraglide/README.md +1 -1
  130. package/src/paraglide/messages/_index.d.ts +11 -0
  131. package/src/paraglide/messages/_index.js +5659 -88285
  132. package/src/paraglide/messages/common_error_generic.d.ts +16 -0
  133. package/src/paraglide/messages/common_error_generic.js +29 -0
  134. package/src/paraglide/messages/common_unsaved_body.d.ts +16 -0
  135. package/src/paraglide/messages/common_unsaved_body.js +29 -0
  136. package/src/paraglide/messages/common_unsaved_leave.d.ts +16 -0
  137. package/src/paraglide/messages/common_unsaved_leave.js +29 -0
  138. package/src/paraglide/messages/common_unsaved_stay.d.ts +16 -0
  139. package/src/paraglide/messages/common_unsaved_stay.js +29 -0
  140. package/src/paraglide/messages/common_unsaved_title.d.ts +16 -0
  141. package/src/paraglide/messages/common_unsaved_title.js +29 -0
  142. package/src/paraglide/messages/gallery_videofailed1.d.ts +17 -0
  143. package/src/paraglide/messages/gallery_videofailed1.js +30 -0
  144. package/src/paraglide/messages/gallery_videogenerating1.d.ts +17 -0
  145. package/src/paraglide/messages/gallery_videogenerating1.js +30 -0
  146. package/src/paraglide/messages/gallery_videomissingprompt2.d.ts +17 -0
  147. package/src/paraglide/messages/gallery_videomissingprompt2.js +30 -0
  148. package/src/paraglide/messages/gallery_videoprogress1.d.ts +19 -0
  149. package/src/paraglide/messages/gallery_videoprogress1.js +30 -0
  150. package/src/paraglide/messages/gallery_videostop1.d.ts +17 -0
  151. package/src/paraglide/messages/gallery_videostop1.js +30 -0
  152. package/src/paraglide/messages/story_table_resizecolumn1.d.ts +17 -0
  153. package/src/paraglide/messages/story_table_resizecolumn1.js +30 -0
  154. package/src/providers/index.tsx +5 -50
  155. package/src/routeTree.gen.ts +21 -0
  156. package/src/router.tsx +21 -8
  157. package/src/routes/__root.tsx +2 -1
  158. package/src/routes/__tests__/-edition-routing.test.tsx +6 -1
  159. package/src/routes/_login/forgot-password/route.lazy.tsx +7 -6
  160. package/src/routes/_login/login/route.lazy.tsx +10 -9
  161. package/src/routes/_login/register/route.lazy.tsx +19 -18
  162. package/src/routes/_login/reset-password/route.lazy.tsx +10 -9
  163. package/src/routes/_nav/knowledge/compliance/$id.lazy.tsx +9 -12
  164. package/src/routes/_nav/knowledge/compliance/index.lazy.tsx +16 -13
  165. package/src/routes/_nav/knowledge/laws/$id.lazy.tsx +1 -1
  166. package/src/routes/_nav/notifications/route.tsx +6 -16
  167. package/src/routes/_nav/settings/-settings-tabs.tsx +9 -12
  168. package/src/routes/api/generate/__tests__/media.test.ts +378 -0
  169. package/src/routes/api/generate/media.ts +179 -0
  170. package/src/server/__tests__/agent-client-graph.test.ts +0 -1
  171. package/src/server/__tests__/agent-crud.test.ts +37 -4
  172. package/src/server/__tests__/bug-groups-two-level.test.ts +34 -1
  173. package/src/server/__tests__/chat-conversations.test.ts +37 -4
  174. package/src/server/__tests__/chat-list-messages.test.ts +40 -7
  175. package/src/server/__tests__/csv-export.test.ts +36 -3
  176. package/src/server/__tests__/desktop-bootstrap.test.ts +189 -0
  177. package/src/server/__tests__/remote-auth.test.ts +72 -0
  178. package/src/server/__tests__/skill-crud.test.ts +37 -4
  179. package/src/server/__tests__/sso-domain.test.ts +1 -1
  180. package/src/server/__tests__/sso-rate-limit-singleton.test.ts +123 -0
  181. package/src/server/__tests__/task-serverfn-crud.test.ts +35 -2
  182. package/src/server/__tests__/task-serverfn-queries.test.ts +35 -2
  183. package/src/server/__tests__/testcase-board.test.ts +38 -5
  184. package/src/server/__tests__/testcase-bulk.test.ts +38 -5
  185. package/src/server/__tests__/testcase-list-scope.test.ts +38 -5
  186. package/src/server/ai-gateway/__tests__/ai-output-guard.test.ts +126 -2
  187. package/src/server/ai-gateway/__tests__/chat-stream-builder-code-mode.test.ts +4 -0
  188. package/src/server/ai-gateway/__tests__/chat-stream-builder-memory.test.ts +4 -0
  189. package/src/server/ai-gateway/__tests__/chat-stream-builder-sandbox.test.ts +4 -0
  190. package/src/server/ai-gateway/__tests__/chat-stream-builder-skills.test.ts +4 -0
  191. package/src/server/ai-gateway/__tests__/pgvector-memory-adapter.test.ts +30 -0
  192. package/src/server/ai-gateway/__tests__/pgvector-memory-conformance.test.ts +92 -0
  193. package/src/server/ai-gateway/__tests__/source-mode.test.ts +49 -8
  194. package/src/server/ai-gateway/ai-output-guard.ts +50 -2
  195. package/src/server/ai-gateway/chat-stream-builder.ts +3 -0
  196. package/src/server/ai-gateway/media/index.ts +36 -29
  197. package/src/server/ai-gateway/media/trim-input.ts +52 -0
  198. package/src/server/ai-gateway/memory/pgvector-adapter.ts +27 -13
  199. package/src/server/ai-gateway/sandbox/__tests__/harness-sandbox.test.ts +252 -41
  200. package/src/server/ai-gateway/sandbox/harness-sandbox.ts +6 -67
  201. package/src/server/ai-gateway/source-mode.ts +24 -8
  202. package/src/server/desktop-bootstrap.ts +74 -0
  203. package/src/server/lib/__tests__/rate-limit.test.ts +0 -1
  204. package/src/server/lib/rate-limit.ts +0 -2
  205. package/src/server/middlewares/__tests__/auth-context.test.ts +156 -0
  206. package/src/server/middlewares/auth-context.ts +62 -0
  207. package/src/server/remote-auth.ts +58 -31
  208. package/src/server/serverFns/__tests__/ai-hosted-models.test.ts +47 -15
  209. package/src/server/serverFns/__tests__/ai-source-mode.test.ts +45 -15
  210. package/src/server/serverFns/__tests__/billing.test.ts +37 -2
  211. package/src/server/serverFns/__tests__/collab-snapshot.test.ts +41 -14
  212. package/src/server/serverFns/__tests__/collab.test.ts +41 -14
  213. package/src/server/serverFns/__tests__/compliance.test.ts +33 -1
  214. package/src/server/serverFns/__tests__/email-verification.test.ts +35 -1
  215. package/src/server/serverFns/__tests__/knowledge-breadcrumb.test.ts +46 -6
  216. package/src/server/serverFns/__tests__/knowledge-delete.test.ts +42 -14
  217. package/src/server/serverFns/__tests__/knowledge-kb-projects.test.ts +46 -6
  218. package/src/server/serverFns/__tests__/knowledge-public-compliance.test.ts +36 -2
  219. package/src/server/serverFns/__tests__/knowledge-readable-kb.test.ts +42 -10
  220. package/src/server/serverFns/__tests__/knowledge-search.test.ts +45 -9
  221. package/src/server/serverFns/__tests__/legal-code-canonical.test.ts +36 -2
  222. package/src/server/serverFns/__tests__/link-preview.test.ts +38 -4
  223. package/src/server/serverFns/__tests__/notifications-list.test.ts +36 -3
  224. package/src/server/serverFns/__tests__/ownership-merge.test.ts +38 -4
  225. package/src/server/serverFns/__tests__/project-delete-transaction.test.ts +39 -2
  226. package/src/server/serverFns/__tests__/project-kb.test.ts +34 -1
  227. package/src/server/serverFns/__tests__/project-member-idor.test.ts +34 -1
  228. package/src/server/serverFns/__tests__/project-team-assign.test.ts +34 -1
  229. package/src/server/serverFns/__tests__/read-write-merge.test.ts +38 -4
  230. package/src/server/serverFns/__tests__/story-comments.test.ts +38 -4
  231. package/src/server/serverFns/__tests__/story-crud-archive-detail.test.ts +38 -4
  232. package/src/server/serverFns/__tests__/story-crud-labels-users.test.ts +37 -3
  233. package/src/server/serverFns/__tests__/story-crud.test.ts +36 -3
  234. package/src/server/serverFns/__tests__/story-links.test.ts +38 -4
  235. package/src/server/serverFns/__tests__/story-reactions.test.ts +38 -4
  236. package/src/server/serverFns/__tests__/story-tree.test.ts +38 -4
  237. package/src/server/serverFns/__tests__/team-accept.test.ts +34 -1
  238. package/src/server/serverFns/__tests__/team-invitation.test.ts +34 -1
  239. package/src/server/serverFns/__tests__/team-join-request.test.ts +34 -1
  240. package/src/server/serverFns/__tests__/team-members.test.ts +34 -1
  241. package/src/server/serverFns/__tests__/team-ownership-merge.test.ts +34 -1
  242. package/src/server/serverFns/__tests__/update-bug-guard.test.ts +37 -3
  243. package/src/server/serverFns/__tests__/update-document-guard.test.ts +38 -4
  244. package/src/server/serverFns/__tests__/update-page-guard.test.ts +40 -4
  245. package/src/server/serverFns/__tests__/update-seats.test.ts +33 -1
  246. package/src/server/serverFns/__tests__/update-story-guard.test.ts +36 -3
  247. package/src/server/serverFns/__tests__/update-testcase-guard.test.ts +37 -3
  248. package/src/server/serverFns/__tests__/workspace-kb-idor.test.ts +36 -2
  249. package/src/server/serverFns/__tests__/workspace-kb.test.ts +36 -2
  250. package/src/server/serverFns/__tests__/workspace.test.ts +34 -1
  251. package/src/server/serverFns/activity.ts +10 -14
  252. package/src/server/serverFns/agents/agent-crud.ts +16 -15
  253. package/src/server/serverFns/agents/bindings.ts +12 -13
  254. package/src/server/serverFns/agents/skill-crud.ts +16 -15
  255. package/src/server/serverFns/ai/code-mode-readiness.ts +3 -4
  256. package/src/server/serverFns/ai/search-capability.ts +3 -4
  257. package/src/server/serverFns/anyhub/app.ts +10 -14
  258. package/src/server/serverFns/anyhub/index.ts +7 -10
  259. package/src/server/serverFns/anyhub/install.ts +4 -4
  260. package/src/server/serverFns/anyhub/permissions.ts +10 -14
  261. package/src/server/serverFns/anyhub/proxy.ts +4 -6
  262. package/src/server/serverFns/auth/email-verification.ts +4 -5
  263. package/src/server/serverFns/auth/session.ts +9 -0
  264. package/src/server/serverFns/billing.ts +18 -28
  265. package/src/server/serverFns/blob-upload.ts +6 -9
  266. package/src/server/serverFns/bug/__tests__/bug-access-tokens.test.ts +34 -1
  267. package/src/server/serverFns/bug/__tests__/bug-activities.test.ts +34 -1
  268. package/src/server/serverFns/bug/__tests__/bug-comments.test.ts +34 -1
  269. package/src/server/serverFns/bug/__tests__/bug-crud.test.ts +34 -1
  270. package/src/server/serverFns/bug/__tests__/bug-fix.test.ts +34 -1
  271. package/src/server/serverFns/bug/__tests__/bug-groups.test.ts +34 -1
  272. package/src/server/serverFns/bug/__tests__/bug-reactions.test.ts +34 -1
  273. package/src/server/serverFns/bug/__tests__/bug-reorder.test.ts +39 -5
  274. package/src/server/serverFns/bug/__tests__/bug-repro.test.ts +34 -1
  275. package/src/server/serverFns/bug/__tests__/bug-story.test.ts +34 -1
  276. package/src/server/serverFns/bug/__tests__/bug-webhooks.test.ts +34 -1
  277. package/src/server/serverFns/bug/bug-access-tokens.ts +16 -22
  278. package/src/server/serverFns/bug/bug-activities.ts +4 -6
  279. package/src/server/serverFns/bug/bug-collect-tokens.ts +13 -18
  280. package/src/server/serverFns/bug/bug-comments.ts +13 -18
  281. package/src/server/serverFns/bug/bug-crud/detail.ts +10 -14
  282. package/src/server/serverFns/bug/bug-crud.ts +28 -38
  283. package/src/server/serverFns/bug/bug-display-options.ts +8 -11
  284. package/src/server/serverFns/bug/bug-export.ts +5 -7
  285. package/src/server/serverFns/bug/bug-fix.ts +10 -14
  286. package/src/server/serverFns/bug/bug-groups.ts +10 -14
  287. package/src/server/serverFns/bug/bug-reactions.ts +10 -14
  288. package/src/server/serverFns/bug/bug-reorder.ts +4 -6
  289. package/src/server/serverFns/bug/bug-repro.ts +8 -11
  290. package/src/server/serverFns/bug/bug-story-bulk.ts +13 -18
  291. package/src/server/serverFns/bug/bug-story.ts +10 -14
  292. package/src/server/serverFns/bug/bug-views.ts +12 -18
  293. package/src/server/serverFns/bug/bug-webhooks.ts +13 -18
  294. package/src/server/serverFns/bug/test-bug-confirm.ts +7 -10
  295. package/src/server/serverFns/changelog/__tests__/fetch.test.ts +40 -1
  296. package/src/server/serverFns/changelog/fetch.ts +4 -6
  297. package/src/server/serverFns/chat/conversations.ts +17 -28
  298. package/src/server/serverFns/chat/list-messages.ts +4 -7
  299. package/src/server/serverFns/collab-presence.ts +11 -13
  300. package/src/server/serverFns/collab-snapshot.ts +6 -7
  301. package/src/server/serverFns/collab.ts +7 -7
  302. package/src/server/serverFns/compliance.ts +8 -8
  303. package/src/server/serverFns/design/__tests__/comments.test.ts +54 -10
  304. package/src/server/serverFns/design/__tests__/designs.test.ts +39 -16
  305. package/src/server/serverFns/design/__tests__/page-versions.test.ts +52 -14
  306. package/src/server/serverFns/design/__tests__/shares-password.test.ts +44 -14
  307. package/src/server/serverFns/design/__tests__/shares.test.ts +44 -14
  308. package/src/server/serverFns/design/__tests__/versions.test.ts +44 -14
  309. package/src/server/serverFns/design/ai/__tests__/agent-conversations.test.ts +50 -21
  310. package/src/server/serverFns/design/ai/__tests__/providers.test.ts +42 -8
  311. package/src/server/serverFns/design/ai/agent-conversations.ts +19 -26
  312. package/src/server/serverFns/design/ai/fill-annotation.ts +5 -7
  313. package/src/server/serverFns/design/ai/generate-prototype.ts +5 -7
  314. package/src/server/serverFns/design/ai/hosted-models.ts +4 -5
  315. package/src/server/serverFns/design/ai/providers.ts +30 -39
  316. package/src/server/serverFns/design/ai/source-mode.ts +10 -12
  317. package/src/server/serverFns/design/comments.ts +14 -13
  318. package/src/server/serverFns/design/designs.ts +13 -19
  319. package/src/server/serverFns/design/page-versions.ts +17 -18
  320. package/src/server/serverFns/design/pages.ts +17 -18
  321. package/src/server/serverFns/design/reviews.ts +8 -9
  322. package/src/server/serverFns/design/shares-password.ts +13 -19
  323. package/src/server/serverFns/design/shares.ts +22 -31
  324. package/src/server/serverFns/design/versions.ts +8 -9
  325. package/src/server/serverFns/harness/__tests__/attach-prototype.spec.ts +52 -2
  326. package/src/server/serverFns/harness/__tests__/attach-to-kb.spec.ts +52 -2
  327. package/src/server/serverFns/harness/__tests__/attach-to-story-ssrf.spec.ts +52 -2
  328. package/src/server/serverFns/harness/__tests__/attach-to-story.spec.ts +52 -2
  329. package/src/server/serverFns/harness/__tests__/cancel-run.test.ts +40 -15
  330. package/src/server/serverFns/harness/__tests__/extract-attachment.spec.ts +52 -2
  331. package/src/server/serverFns/harness/__tests__/gallery.spec.ts +76 -12
  332. package/src/server/serverFns/harness/__tests__/github-workspace.spec.ts +38 -6
  333. package/src/server/serverFns/harness/__tests__/media-prototype.spec.ts +50 -6
  334. package/src/server/serverFns/harness/__tests__/media.spec.ts +55 -11
  335. package/src/server/serverFns/harness/__tests__/sandbox-policy.test.ts +41 -16
  336. package/src/server/serverFns/harness/__tests__/workspace-files.test.ts +41 -16
  337. package/src/server/serverFns/harness/attach-prototype.ts +4 -4
  338. package/src/server/serverFns/harness/attach-to-kb.ts +4 -4
  339. package/src/server/serverFns/harness/attach-to-story.ts +4 -4
  340. package/src/server/serverFns/harness/cancel-run.ts +4 -6
  341. package/src/server/serverFns/harness/extract-attachment.ts +3 -4
  342. package/src/server/serverFns/harness/gallery.ts +19 -23
  343. package/src/server/serverFns/harness/github-workspace.ts +14 -21
  344. package/src/server/serverFns/harness/media.ts +18 -117
  345. package/src/server/serverFns/harness/sandbox-policy.ts +11 -10
  346. package/src/server/serverFns/harness/search-mention-candidates.ts +4 -6
  347. package/src/server/serverFns/harness/workspace-files.ts +19 -26
  348. package/src/server/serverFns/iteration/__tests__/iteration-auto-schedule.test.ts +35 -3
  349. package/src/server/serverFns/iteration/__tests__/iteration-crud.test.ts +35 -3
  350. package/src/server/serverFns/iteration/__tests__/iteration-stories.test.ts +35 -3
  351. package/src/server/serverFns/iteration/__tests__/iteration-transfer.test.ts +35 -3
  352. package/src/server/serverFns/iteration/iteration-auto-schedule.ts +7 -10
  353. package/src/server/serverFns/iteration/iteration-burndown.ts +4 -6
  354. package/src/server/serverFns/iteration/iteration-crud.ts +26 -35
  355. package/src/server/serverFns/iteration/iteration-stories.ts +10 -14
  356. package/src/server/serverFns/iteration/iteration-transfer.ts +4 -6
  357. package/src/server/serverFns/knowledge/public/bookmarks/categories.ts +11 -15
  358. package/src/server/serverFns/knowledge/public/bookmarks/create.ts +4 -6
  359. package/src/server/serverFns/knowledge/public/bookmarks/delete.ts +5 -7
  360. package/src/server/serverFns/knowledge/public/bookmarks/detail.ts +4 -6
  361. package/src/server/serverFns/knowledge/public/bookmarks/favorite.ts +4 -6
  362. package/src/server/serverFns/knowledge/public/bookmarks/list.ts +4 -6
  363. package/src/server/serverFns/knowledge/public/bookmarks/refresh-favicon.ts +4 -6
  364. package/src/server/serverFns/knowledge/public/bookmarks/share.ts +4 -6
  365. package/src/server/serverFns/knowledge/public/bookmarks/sync-github.ts +5 -7
  366. package/src/server/serverFns/knowledge/public/bookmarks/unshare.ts +4 -6
  367. package/src/server/serverFns/knowledge/public/bookmarks/update.ts +6 -8
  368. package/src/server/serverFns/knowledge/public/compliance/create.ts +4 -6
  369. package/src/server/serverFns/knowledge/public/compliance/delete.ts +5 -7
  370. package/src/server/serverFns/knowledge/public/compliance/detail.ts +4 -6
  371. package/src/server/serverFns/knowledge/public/compliance/list.ts +4 -6
  372. package/src/server/serverFns/knowledge/public/compliance/share.ts +4 -6
  373. package/src/server/serverFns/knowledge/public/compliance/unshare.ts +4 -6
  374. package/src/server/serverFns/knowledge/public/compliance/update.ts +5 -7
  375. package/src/server/serverFns/knowledge/public/dictionary/create.ts +4 -6
  376. package/src/server/serverFns/knowledge/public/dictionary/delete.ts +5 -7
  377. package/src/server/serverFns/knowledge/public/dictionary/detail.ts +4 -6
  378. package/src/server/serverFns/knowledge/public/dictionary/list.ts +4 -6
  379. package/src/server/serverFns/knowledge/public/dictionary/share.ts +4 -6
  380. package/src/server/serverFns/knowledge/public/dictionary/unshare.ts +4 -6
  381. package/src/server/serverFns/knowledge/public/dictionary/update.ts +5 -7
  382. package/src/server/serverFns/knowledge/public/entity-tags.ts +4 -6
  383. package/src/server/serverFns/knowledge/public/favorite.ts +7 -10
  384. package/src/server/serverFns/knowledge/public/legal/create.ts +4 -6
  385. package/src/server/serverFns/knowledge/public/legal/delete.ts +5 -7
  386. package/src/server/serverFns/knowledge/public/legal/detail.ts +4 -6
  387. package/src/server/serverFns/knowledge/public/legal/list.ts +4 -6
  388. package/src/server/serverFns/knowledge/public/legal/share.ts +4 -6
  389. package/src/server/serverFns/knowledge/public/legal/unshare.ts +4 -6
  390. package/src/server/serverFns/knowledge/public/legal/update.ts +5 -7
  391. package/src/server/serverFns/knowledge-index.ts +12 -16
  392. package/src/server/serverFns/knowledge.ts +50 -67
  393. package/src/server/serverFns/notifications.ts +25 -34
  394. package/src/server/serverFns/plugins/harness-plugins.ts +16 -23
  395. package/src/server/serverFns/project/module-overview.ts +7 -10
  396. package/src/server/serverFns/project.ts +48 -68
  397. package/src/server/serverFns/references/create.ts +4 -6
  398. package/src/server/serverFns/references/resolve.ts +4 -6
  399. package/src/server/serverFns/references/search.ts +4 -6
  400. package/src/server/serverFns/share/design/[designId]/events.ts +4 -6
  401. package/src/server/serverFns/sso.ts +1 -1
  402. package/src/server/serverFns/story/__tests__/bulk.test.ts +34 -1
  403. package/src/server/serverFns/story/__tests__/story-assoc.test.ts +34 -1
  404. package/src/server/serverFns/story/__tests__/story-attachments.test.ts +34 -1
  405. package/src/server/serverFns/story/__tests__/story-crud-date-key.test.ts +42 -17
  406. package/src/server/serverFns/story/__tests__/story-dependencies.test.ts +34 -1
  407. package/src/server/serverFns/story/__tests__/story-gantt-deps.test.ts +34 -1
  408. package/src/server/serverFns/story/__tests__/story-groups.test.ts +34 -1
  409. package/src/server/serverFns/story/__tests__/story-read-path-authz.test.ts +40 -4
  410. package/src/server/serverFns/story/__tests__/story-search.test.ts +34 -1
  411. package/src/server/serverFns/story/__tests__/story-transfer.test.ts +34 -1
  412. package/src/server/serverFns/story/link-preview.ts +5 -7
  413. package/src/server/serverFns/story/story-assoc.ts +28 -38
  414. package/src/server/serverFns/story/story-attachments.ts +7 -10
  415. package/src/server/serverFns/story/story-children.ts +4 -6
  416. package/src/server/serverFns/story/story-comments.ts +13 -18
  417. package/src/server/serverFns/story/story-crud/activities.ts +4 -6
  418. package/src/server/serverFns/story/story-crud/archive.ts +7 -10
  419. package/src/server/serverFns/story/story-crud/bulk.ts +10 -14
  420. package/src/server/serverFns/story/story-crud/detail.ts +7 -10
  421. package/src/server/serverFns/story/story-crud/label-suggest.ts +4 -6
  422. package/src/server/serverFns/story/story-crud/labels.ts +6 -9
  423. package/src/server/serverFns/story/story-crud/user-brief.ts +4 -6
  424. package/src/server/serverFns/story/story-crud.ts +13 -18
  425. package/src/server/serverFns/story/story-dependencies.ts +16 -22
  426. package/src/server/serverFns/story/story-groups.ts +10 -14
  427. package/src/server/serverFns/story/story-links.ts +13 -18
  428. package/src/server/serverFns/story/story-reactions.ts +13 -18
  429. package/src/server/serverFns/story/story-search.ts +7 -10
  430. package/src/server/serverFns/story/story-transfer.ts +10 -14
  431. package/src/server/serverFns/story/story-tree.ts +7 -10
  432. package/src/server/serverFns/story-display-options.ts +7 -8
  433. package/src/server/serverFns/story-view.ts +12 -18
  434. package/src/server/serverFns/tasks/task-crud.ts +13 -17
  435. package/src/server/serverFns/tasks/task-queries.ts +12 -18
  436. package/src/server/serverFns/team.ts +46 -66
  437. package/src/server/serverFns/testcase/__tests__/automation.test.ts +38 -5
  438. package/src/server/serverFns/testcase/__tests__/evidence-upload.test.ts +37 -3
  439. package/src/server/serverFns/testcase/__tests__/generation.test.ts +38 -5
  440. package/src/server/serverFns/testcase/__tests__/plans.test.ts +36 -3
  441. package/src/server/serverFns/testcase/__tests__/regression.test.ts +36 -3
  442. package/src/server/serverFns/testcase/__tests__/run-cases.test.ts +36 -3
  443. package/src/server/serverFns/testcase/__tests__/runs.test.ts +36 -3
  444. package/src/server/serverFns/testcase/__tests__/stats.test.ts +36 -3
  445. package/src/server/serverFns/testcase/__tests__/testcase-board.test.ts +39 -5
  446. package/src/server/serverFns/testcase/__tests__/testcase-crud.test.ts +36 -3
  447. package/src/server/serverFns/testcase/__tests__/testcase-folders.test.ts +36 -3
  448. package/src/server/serverFns/testcase/__tests__/testcase-groups.test.ts +38 -5
  449. package/src/server/serverFns/testcase/automation.ts +13 -17
  450. package/src/server/serverFns/testcase/bulk.ts +10 -14
  451. package/src/server/serverFns/testcase/evidence-upload.ts +5 -7
  452. package/src/server/serverFns/testcase/generation.ts +10 -13
  453. package/src/server/serverFns/testcase/plans.ts +23 -31
  454. package/src/server/serverFns/testcase/regression.ts +7 -10
  455. package/src/server/serverFns/testcase/run-cases.ts +13 -18
  456. package/src/server/serverFns/testcase/runs.ts +13 -18
  457. package/src/server/serverFns/testcase/stats.ts +13 -18
  458. package/src/server/serverFns/testcase/testcase-activities.ts +4 -6
  459. package/src/server/serverFns/testcase/testcase-board.ts +4 -6
  460. package/src/server/serverFns/testcase/testcase-crud.ts +20 -27
  461. package/src/server/serverFns/testcase/testcase-display-options.ts +8 -11
  462. package/src/server/serverFns/testcase/testcase-export.ts +5 -7
  463. package/src/server/serverFns/testcase/testcase-filter-views.ts +12 -18
  464. package/src/server/serverFns/testcase/testcase-folders.ts +16 -22
  465. package/src/server/serverFns/testcase/testcase-groups.ts +4 -6
  466. package/src/server/serverFns/testcase/testcase-reorder.ts +4 -6
  467. package/src/server/serverFns/translation.ts +4 -6
  468. package/src/server/serverFns/user/avatar.ts +4 -6
  469. package/src/server/serverFns/user/settings.ts +4 -6
  470. package/src/server/serverFns/user/update-locale.ts +4 -6
  471. package/src/server/serverFns/workspace-kb.ts +26 -35
  472. package/src/server/serverFns/workspace.ts +43 -59
  473. package/src/server/serverFns/ws.ts +4 -6
  474. package/src/server/serverfn-public-allowlist.ts +117 -0
  475. package/src/server/story/types.ts +0 -1
  476. package/src/server/sync/__tests__/scopes.test.ts +64 -0
  477. package/src/server/sync/scopes.ts +7 -0
  478. package/src/server/testcase/__tests__/auto-bug.test.ts +44 -12
  479. package/src/__tests__/bench/baselines/format-timestamp.json +0 -44
  480. package/src/__typeprobe__.d.ts +0 -4
  481. package/src/components/forms/app-form-dialog.tsx +0 -126
  482. package/src/components/forms/hooks/use-form-submit.ts +0 -57
  483. package/src/components/forms/types.ts +0 -34
  484. package/src/components/harness/code/workspace-picker.tsx +0 -239
  485. package/src/components/tiptap/extensions/local-file/insert-local-file.ts +0 -35
  486. package/src/components/tiptap/extensions/local-file/local-file-node.ts +0 -71
  487. package/src/devframe-docks/ai-chat-iframe.tsx +0 -29
  488. package/src/devframe-docks/session-iframe.tsx +0 -90
  489. package/src/lib/__tests__/desktop-detect.test.ts +0 -19
  490. package/src/lib/collections/index.ts +0 -22
  491. package/src/lib/collections/knowledge/index.ts +0 -18
  492. package/src/lib/collections/knowledge/knowledge-bases-collection.ts +0 -34
  493. package/src/lib/collections/reference/bookmark-categories-collection.ts +0 -31
  494. package/src/lib/collections/reference/labels-collection.ts +0 -31
  495. package/src/lib/collections/reference/projects-collection.ts +0 -31
  496. package/src/lib/collections/reference/users-brief-collection.ts +0 -35
  497. package/src/lib/collections/reference/workspaces-collection.ts +0 -34
  498. package/src/lib/desktop-detect.ts +0 -29
  499. package/src/paraglide/messages/en.js +0 -28260
  500. package/src/paraglide/messages/zh.js +0 -28258
@@ -36,7 +36,7 @@ import { Button } from '@/components/ui/button'
36
36
  import { ConfirmDialog } from '@/components/confirm-dialog';
37
37
  import { useSession } from '@/lib/auth/auth-client'
38
38
  import { listConversationsFn, createConversationFn, updateConversationFn, deleteConversationFn, forkConversationFn } from '@/server/serverFns/chat/conversations'
39
- import { listAgentsFn } from '@/server/serverFns/agents/agent-crud'
39
+ import { agentsQueryOptions } from '@/lib/queries/agents'
40
40
  import { useSelectedModel } from '@/lib/ai/use-selected-model'
41
41
  import { useSearchCapability } from '@/lib/ai/use-search-capability'
42
42
  import { useCodeModeReadiness } from '@/lib/ai/use-code-mode-readiness'
@@ -109,8 +109,7 @@ export default function HarnessWorkbenchPage({ initialThreadId, initialMessages
109
109
  const { workspace } = useWorkspace()
110
110
 
111
111
  const { data: agents } = useQuery({
112
- queryKey: ['agents'],
113
- queryFn: () => listAgentsFn(),
112
+ ...agentsQueryOptions(),
114
113
  staleTime: 30_000,
115
114
  enabled: initialThreadId == null,
116
115
  })
@@ -23,7 +23,7 @@ import {
23
23
  RotateCcw,
24
24
  } from "lucide-react";
25
25
  import { useCallback, useRef, useState } from "react";
26
- import { useHotkey } from "@tanstack/react-hotkeys";
26
+ import { useAppHotkeys, useHotkey } from "@tanstack/react-hotkeys";
27
27
  import { Button } from "@/components/ui/button";
28
28
  import {
29
29
  DropdownMenu,
@@ -52,10 +52,12 @@ export function MessageActions({
52
52
  }: MessageActionsProps) {
53
53
  const [isOpen, setIsOpen] = useState(false);
54
54
  const containerRef = useRef<HTMLDivElement>(null);
55
- useHotkey("Escape", () => setIsOpen(false), {
55
+ // 声明即绑定:close-panel / copy-message 条目单一来源(lib/hotkeys/registry.ts)
56
+ const hk = useAppHotkeys();
57
+ useHotkey(hk["close-panel"], () => setIsOpen(false), {
56
58
  enabled: isOpen,
57
59
  });
58
- useHotkey("Mod+C", () => {
60
+ useHotkey(hk["copy-message"], () => {
59
61
  onCopy();
60
62
  setIsOpen(false);
61
63
  }, {
@@ -16,7 +16,7 @@
16
16
  // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
17
 
18
18
  import { useCallback, useState } from "react"
19
- import { useHotkey } from "@tanstack/react-hotkeys"
19
+ import { useAppHotkeyList, useAppHotkeys, useHotkey } from "@tanstack/react-hotkeys"
20
20
  import {
21
21
  Dialog,
22
22
  DialogContent,
@@ -26,27 +26,38 @@ import {
26
26
  import { Button } from "@/components/ui/button"
27
27
  import { KeyboardMusic, X } from "lucide-react"
28
28
  import { ShortcutBadge } from "./shortcut-badge"
29
- import { HOTKEY_REGISTRY, CATEGORY_ORDER, HOTKEY_CATEGORY_LABEL_KEYS } from "@/lib/hotkeys/registry"
29
+ import {
30
+ formatAppHotkeyForBadge,
31
+ CATEGORY_ORDER,
32
+ HOTKEY_CATEGORY_LABEL_KEYS,
33
+ } from "@/lib/hotkeys/registry"
34
+ import type { HotkeyCategory } from "@/lib/hotkeys/types"
30
35
  import { t } from "@/lib/i18n/resolver"
31
36
  import { m } from "@/paraglide/messages.js";
32
37
 
33
38
  export function ShortcutsPanel() {
34
39
  const [open, setOpen] = useState(false)
35
40
 
36
- useHotkey("Mod+/", () => setOpen((prev) => !prev), {
41
+ const hk = useAppHotkeys()
42
+ useHotkey(hk["shortcuts-panel"], () => setOpen((prev) => !prev), {
37
43
  ignoreInputs: false,
38
44
  })
39
45
 
46
+ // 官方 useAppHotkeyList:声明顺序返回全部应用热键(含未挂载绑定的 inactive 条目)
47
+ const items = useAppHotkeyList()
48
+
40
49
  const grouped = useCallback(() => {
41
- const map = new Map<string, (typeof HOTKEY_REGISTRY)[0][]>()
50
+ const map = new Map<HotkeyCategory, typeof items>()
42
51
  for (const category of CATEGORY_ORDER) {
43
- const items = HOTKEY_REGISTRY.filter((h) => h.category === category)
44
- if (items.length > 0) {
45
- map.set(category, items)
52
+ const inCategory = items.filter(
53
+ ({ appHotkey }) => appHotkey.meta?.category === category,
54
+ )
55
+ if (inCategory.length > 0) {
56
+ map.set(category, inCategory)
46
57
  }
47
58
  }
48
59
  return map
49
- }, [])
60
+ }, [items])
50
61
 
51
62
  return (
52
63
  <Dialog open={open} onOpenChange={setOpen}>
@@ -69,16 +80,20 @@ export function ShortcutsPanel() {
69
80
  {Array.from(grouped()).map(([category, shortcuts]) => (
70
81
  <div key={category}>
71
82
  <h4 className="mb-2 text-xs font-medium text-muted-foreground">
72
- {t(HOTKEY_CATEGORY_LABEL_KEYS[category as keyof typeof HOTKEY_CATEGORY_LABEL_KEYS])}
83
+ {t(HOTKEY_CATEGORY_LABEL_KEYS[category])}
73
84
  </h4>
74
85
  <div className="space-y-1.5">
75
- {shortcuts.map((h) => (
86
+ {shortcuts.map(({ appHotkey }) => (
76
87
  <div
77
- key={h.id}
88
+ key={appHotkey.id}
78
89
  className="flex items-center justify-between rounded-md px-2 py-1.5 hover:bg-muted/50"
79
90
  >
80
- <span className="text-sm">{t(h.descriptionKey)}</span>
81
- <ShortcutBadge keys={h.keys} />
91
+ <span className="text-sm">
92
+ {appHotkey.meta?.descriptionKey
93
+ ? t(appHotkey.meta.descriptionKey)
94
+ : ""}
95
+ </span>
96
+ <ShortcutBadge keys={formatAppHotkeyForBadge(appHotkey.hotkey)} />
82
97
  </div>
83
98
  ))}
84
99
  </div>
@@ -0,0 +1,118 @@
1
+ // @vitest-environment jsdom
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+ // AllBlue - 理想之海
4
+ // Copyright (C) 2026 AllBlue Contributors
5
+ //
6
+ // This program is free software: you can redistribute it and/or modify
7
+ // it under the terms of the GNU Affero General Public License as published by
8
+ // the Free Software Foundation, either version 3 of the License, or
9
+ // (at your option) any later version.
10
+ //
11
+ // This program is distributed in the hope that it will be useful,
12
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ // GNU Affero General Public License for more details.
15
+ //
16
+ // You should have received a copy of the GNU Affero General Public License
17
+ // along with this program. If not, see <https://www.gnu.org/licenses/>.
18
+ import { render, screen } from "@testing-library/react";
19
+ import { describe, expect, it } from "vitest";
20
+ import { BurndownChart } from "../burndown-chart";
21
+ import type { BurndownResult } from "@/lib/iteration/burndown";
22
+
23
+ // leading:末点 pending < ideal(与 computeBurndown 的状态判定语义一致)
24
+ const leadingData: BurndownResult & { timezone: string } = {
25
+ timezone: "Asia/Shanghai",
26
+ status: "leading",
27
+ points: [
28
+ { date: "2026-01-01", scope: 10, pending: 10, ideal: 10 },
29
+ { date: "2026-01-02", scope: 10, pending: 6, ideal: 7 },
30
+ { date: "2026-01-03", scope: 11, pending: 3, ideal: 4 },
31
+ ],
32
+ };
33
+
34
+ const trailingData: BurndownResult & { timezone: string } = {
35
+ ...leadingData,
36
+ status: "trailing",
37
+ points: [
38
+ { date: "2026-02-01", scope: 10, pending: 10, ideal: 10 },
39
+ { date: "2026-02-02", scope: 10, pending: 9, ideal: 7 },
40
+ { date: "2026-02-03", scope: 11, pending: 8, ideal: 4 },
41
+ ],
42
+ };
43
+
44
+ function chartSvg(container: HTMLElement): SVGSVGElement {
45
+ const svg = container.querySelector("svg.ts-chart");
46
+ expect(svg).not.toBeNull();
47
+ return svg as SVGSVGElement;
48
+ }
49
+
50
+ describe("BurndownChart 数据→图形", () => {
51
+ it("无数据点时渲染空态且不出 SVG", () => {
52
+ const { container } = render(
53
+ <BurndownChart data={{ ...leadingData, points: [] }} />,
54
+ );
55
+ expect(screen.getByText("暂无数据,添加需求后生成")).toBeInTheDocument();
56
+ expect(container.querySelector("svg")).toBeNull();
57
+ });
58
+
59
+ it("按数据点绘制图形:5 条网格线 + 双折线 + 面积 + 每点一个圆点", () => {
60
+ const { container } = render(<BurndownChart data={leadingData} />);
61
+ const svg = chartSvg(container);
62
+ // 无障碍:图表 svg 以标题文案作为 aria-label
63
+ expect(svg.getAttribute("aria-label")).toBe("燃尽图");
64
+
65
+ // ruleY 网格线:0/25%/50%/75%/100% 五条水平线,全宽
66
+ const rules = svg.querySelectorAll("g.ts-chart__rule-y line");
67
+ expect(rules).toHaveLength(5);
68
+ const ruleYs = Array.from(rules).map((l) => Number(l.getAttribute("y1")));
69
+ expect([...ruleYs].sort((a, b) => a - b)).toEqual([16, 58, 100, 142, 184]);
70
+
71
+ // ideal 线:虚线、muted-foreground
72
+ const idealPath = svg.querySelector('path[stroke="var(--muted-foreground)"]');
73
+ expect(idealPath).not.toBeNull();
74
+ expect(idealPath!.getAttribute("stroke-dasharray")).toBe("4 3");
75
+ expect(idealPath!.getAttribute("stroke-width")).toBe("1.5");
76
+
77
+ // pending 线:实线、primary、宽 2
78
+ const pendingPath = svg.querySelector('path[stroke="var(--primary)"]:not([stroke-dasharray])');
79
+ expect(pendingPath).not.toBeNull();
80
+ expect(pendingPath!.getAttribute("stroke-width")).toBe("2");
81
+
82
+ // 面积:pending 与 ideal 之间的多边形,leading 为绿色 15% 透明度
83
+ const areaPath = svg.querySelector("g.ts-chart__area path");
84
+ expect(areaPath).not.toBeNull();
85
+ expect(areaPath!.getAttribute("fill")).toBe("var(--color-green-600)");
86
+ expect(areaPath!.getAttribute("fill-opacity")).toBe("0.15");
87
+ // 面积路径几何闭合(Z 收尾)且覆盖两个数据系列
88
+ expect(areaPath!.getAttribute("d")).toMatch(/Z$/);
89
+
90
+ // 圆点:每个 pending 数据点一个,r=2.5、primary 填充
91
+ const dots = svg.querySelectorAll("g.ts-chart__dot circle");
92
+ expect(dots).toHaveLength(leadingData.points.length);
93
+ for (const dot of dots) {
94
+ expect(dot.getAttribute("fill")).toBe("var(--primary)");
95
+ expect(Number(dot.getAttribute("r"))).toBe(2.5);
96
+ }
97
+ });
98
+
99
+ it("trailing 状态面积切换为红色", () => {
100
+ const { container } = render(<BurndownChart data={trailingData} />);
101
+ const areaPath = chartSvg(container).querySelector("g.ts-chart__area path");
102
+ expect(areaPath!.getAttribute("fill")).toBe("var(--color-red-600)");
103
+ });
104
+
105
+ it("首尾日期标注渲染为文本", () => {
106
+ render(<BurndownChart data={leadingData} />);
107
+ expect(screen.getByText("2026-01-01")).toBeInTheDocument();
108
+ expect(screen.getByText("2026-01-03")).toBeInTheDocument();
109
+ });
110
+
111
+ it("状态文案随 status 切换(领先/滞后)", () => {
112
+ const { unmount } = render(<BurndownChart data={leadingData} />);
113
+ expect(screen.getByText("领先")).toBeInTheDocument();
114
+ unmount();
115
+ render(<BurndownChart data={trailingData} />);
116
+ expect(screen.getByText("滞后")).toBeInTheDocument();
117
+ });
118
+ });
@@ -14,21 +14,62 @@
14
14
  //
15
15
  // You should have received a copy of the GNU Affero General Public License
16
16
  // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+ import { areaY, defineChart, dot, lineY, ruleY } from "@tanstack/charts";
18
+ import { scaleLinear } from "@tanstack/charts/scales/linear";
19
+ import { scalePoint } from "@tanstack/charts/scales/point";
20
+ import { tooltip } from "@tanstack/charts/tooltip";
21
+ import { Chart } from "@tanstack/charts/react";
22
+ import { useMemo } from "react";
17
23
  import type { BurndownResult } from "@/lib/iteration/burndown";
18
24
  import { m } from "@/paraglide/messages.js";
19
25
 
20
26
  export function BurndownChart({ data }: { data: BurndownResult & { timezone: string } }) {
27
+ const leading = data.status === "leading";
28
+ const definition = useMemo(() => {
29
+ const points = data.points;
30
+ const maxVal = Math.max(1, ...points.map((p) => Math.max(p.scope, p.pending, p.ideal)));
31
+ return defineChart({
32
+ // 原手写 SVG 无坐标轴刻度,仅 5 条横向网格线,故整体关闭 guides 由 ruleY 复刻
33
+ guides: false,
34
+ margin: 16,
35
+ marks: [
36
+ ruleY([0, maxVal / 4, maxVal / 2, (maxVal * 3) / 4, maxVal], {
37
+ stroke: "var(--border)",
38
+ strokeWidth: 1,
39
+ strokeOpacity: 1,
40
+ }),
41
+ areaY(points, {
42
+ x: "date",
43
+ y1: "ideal",
44
+ y2: "pending",
45
+ // --color-green-600/--color-red-600 由头部状态文案的 text-* 工具类保活
46
+ //(Tailwind v4 会 tree-shake 未被引用的调色板变量)
47
+ fill: leading ? "var(--color-green-600)" : "var(--color-red-600)",
48
+ fillOpacity: 0.15,
49
+ }),
50
+ lineY(points, {
51
+ x: "date",
52
+ y: "ideal",
53
+ stroke: "var(--muted-foreground)",
54
+ strokeWidth: 1.5,
55
+ strokeDasharray: "4 3",
56
+ }),
57
+ lineY(points, { x: "date", y: "pending", stroke: "var(--primary)", strokeWidth: 2 }),
58
+ dot(points, { x: "date", y: "pending", r: 2.5, fill: "var(--primary)" }),
59
+ ],
60
+ scales: {
61
+ x: { scale: () => scalePoint<string>().padding(0) },
62
+ y: { scale: scaleLinear().domain([0, maxVal]) },
63
+ },
64
+ svgAnimation: true,
65
+ tooltip,
66
+ });
67
+ }, [data.points, leading]);
21
68
  if (data.points.length === 0) {
22
69
  return <div className="flex h-48 items-center justify-center text-xs text-muted-foreground">{m.iterations_burndown_noData()}</div>;
23
70
  }
24
- const W = 640, H = 200, PAD = 28;
25
- const maxVal = Math.max(1, ...data.points.map((p) => Math.max(p.scope, p.pending, p.ideal)));
26
- const x = (i: number) => PAD + (i * (W - PAD * 2)) / Math.max(1, data.points.length - 1);
27
- const y = (v: number) => H - PAD - (v * (H - PAD * 2)) / maxVal;
28
- const line = (key: "pending" | "ideal") => data.points.map((p, i) => `${i === 0 ? "M" : "L"}${x(i)},${y(p[key])}`).join(" ");
29
- const fill = data.points.map((p, i) => `${i === 0 ? "M" : "L"}${x(i)},${y(p.pending)}`).join(" ")
30
- + " " + [...data.points].reverse().map((p, i) => `L${x(data.points.length - 1 - i)},${y(p.ideal)}`).join(" ");
31
- const leading = data.status === "leading";
71
+ const first = data.points[0];
72
+ const last = data.points.at(-1)!;
32
73
  return (
33
74
  <div className="space-y-2">
34
75
  <div className="flex items-center justify-between text-xs">
@@ -37,21 +78,11 @@ export function BurndownChart({ data }: { data: BurndownResult & { timezone: str
37
78
  {leading ? m.iterations_burndown_leading() : data.status === "trailing" ? m.iterations_burndown_trailing() : m.iterations_burndown_pending()}
38
79
  </span>
39
80
  </div>
40
- <svg viewBox={`0 0 ${W} ${H}`} className="w-full" role="img">
41
- {[0, 0.25, 0.5, 0.75, 1].map((r) => (
42
- <line key={r} x1={PAD} x2={W - PAD} y1={y(maxVal * r)} y2={y(maxVal * r)} className="stroke-border" strokeWidth={1} />
43
- ))}
44
- <path d={fill} className={leading ? "fill-green-500/15" : "fill-red-500/15"} />
45
- <path d={line("ideal")} className="stroke-muted-foreground" strokeWidth={1.5} strokeDasharray="4 3" fill="none" />
46
- <path d={line("pending")} className="stroke-primary" strokeWidth={2} fill="none" />
47
- {data.points.map((p, i) => (
48
- <circle key={p.date} cx={x(i)} cy={y(p.pending)} r={2.5} className="fill-primary">
49
- <title>{`${p.date}: ${p.pending}`}</title>
50
- </circle>
51
- ))}
52
- <text x={PAD} y={H - 8} className="fill-muted-foreground text-[10px]">{data.points[0].date}</text>
53
- <text x={W - PAD} y={H - 8} textAnchor="end" className="fill-muted-foreground text-[10px]">{data.points.at(-1)!.date}</text>
54
- </svg>
81
+ <Chart definition={definition} height={200} ariaLabel={m.iterations_burndown_title()} />
82
+ <div className="flex justify-between text-xs text-muted-foreground">
83
+ <span>{first.date}</span>
84
+ <span>{last.date}</span>
85
+ </div>
55
86
  <div className="flex gap-4 text-xs text-muted-foreground">
56
87
  <span className="flex items-center gap-1"><span className="inline-block h-0.5 w-4 bg-primary" />{m.iterations_burndown_pending()}</span>
57
88
  <span className="flex items-center gap-1"><span className="inline-block h-0.5 w-4 bg-muted-foreground" style={{ backgroundImage: "linear-gradient(90deg, currentColor 50%, transparent 50%)", backgroundSize: "4px 2px" }} />{m.iterations_burndown_ideal()}</span>
@@ -108,6 +108,31 @@ vi.mock('@/components/shared-kit/auth/auth-client', () => ({
108
108
  useSession: mocks.useSession,
109
109
  }))
110
110
 
111
+ // KnowledgeDocEditor 挂载 use-unsaved-guard(任务 2):无 RouterProvider 的
112
+ // 单测环境把 useBlocker 替换为 idle resolver(真实 blocker 注册属 router
113
+ // 集成行为,见 hooks/__tests__/use-unsaved-guard.test.tsx);opts 捕获槽供
114
+ // dirty 信号回归测试直接驱动 shouldBlockFn
115
+ const blockerCapture = vi.hoisted(() => ({
116
+ opts: null as null | {
117
+ shouldBlockFn: (args: unknown) => boolean | Promise<boolean>
118
+ enableBeforeUnload: boolean | (() => boolean)
119
+ },
120
+ }))
121
+ vi.mock('@tanstack/react-router', async (importOriginal) => ({
122
+ ...(await importOriginal<Record<string, unknown>>()),
123
+ useBlocker: (opts: NonNullable<typeof blockerCapture.opts>) => {
124
+ blockerCapture.opts = opts
125
+ return {
126
+ status: 'idle' as const,
127
+ current: undefined,
128
+ next: undefined,
129
+ action: undefined,
130
+ proceed: undefined,
131
+ reset: undefined,
132
+ }
133
+ },
134
+ }))
135
+
111
136
  // 预热 lazy 的 NotionDocEditor 动态 import(模块图大,渲染前先解析完)
112
137
  const notionLikeReady = import('@/components/tiptap/notion-like')
113
138
 
@@ -540,3 +565,74 @@ function bannerText(testId: string): string | null {
540
565
  }
541
566
  const screenBannerText = () => bannerText('collab-offline-banner')
542
567
  const connectingBannerText = () => bannerText('collab-connecting-banner')
568
+
569
+ // ---------------------------------------------------------------------------
570
+ // 未保存拦截 dirty 信号(任务 2 审查修复回归)
571
+ // ① NotionLikeEditor onChange 经 EDITOR_CHANGE_DEBOUNCE_MS=500 防抖才通知
572
+ // 父级 setDirty——最后一次击键后 500ms 内导航 guard 读到 false,内容静默
573
+ // 丢失。修复:击键同步置位 pendingChangeRef,guard 读「同步 ref || dirty」。
574
+ // ② 文档刚打开时 UniqueID onCreate 的初始化 ID 归一化触发一次 onChange →
575
+ // setDirty(true),init 保存(~2.5s)完成前导航误弹「未保存」。修复:
576
+ // 无同步击键信号的 init 批次免 dirty(防抖保存照常,ID 持久化保留)。
577
+ // ---------------------------------------------------------------------------
578
+
579
+ describe('未保存拦截 dirty 信号(审查修复回归)', () => {
580
+ const SHOULD_BLOCK_ARGS = { action: 'PUSH', current: {}, next: {} }
581
+
582
+ it('① 击键后 500ms onChange 防抖窗口内导航必须 blocked(同步置位),落库完成后归零', async () => {
583
+ const { editor } = await renderUntilEditorReady({ collab: false })
584
+ expect(editor).not.toBeNull()
585
+ // init 批次已随 ready 轮询处理完(无击键不拦——② 的断言前提)
586
+ expect(blockerCapture.opts?.shouldBlockFn(SHOULD_BLOCK_ARGS)).toBe(false)
587
+
588
+ await insertText(editor!, 'X')
589
+ // 关键断言:onChange 500ms 防抖尚未到期,guard 已须拦截(同步置位)
590
+ expect(blockerCapture.opts?.shouldBlockFn(SHOULD_BLOCK_ARGS)).toBe(true)
591
+
592
+ // 防抖落库完成后归零(保存成功清 dirty + 同步 ref,无缝衔接)
593
+ await act(async () => {
594
+ await vi.advanceTimersByTimeAsync(500)
595
+ await vi.advanceTimersByTimeAsync(2000)
596
+ await Promise.resolve()
597
+ })
598
+ expect(mockUpdateDocumentFn).toHaveBeenCalled()
599
+ expect(blockerCapture.opts?.shouldBlockFn(SHOULD_BLOCK_ARGS)).toBe(false)
600
+ })
601
+
602
+ it('② 打开后 init 归一化(UniqueID)保存完成前导航必须不 blocked', async () => {
603
+ const { editor } = await renderUntilEditorReady({ collab: false })
604
+ expect(editor).not.toBeNull()
605
+ expect(blockerCapture.opts?.shouldBlockFn(SHOULD_BLOCK_ARGS)).toBe(false)
606
+ // init onChange 交付(500ms 防抖到期)→ init 保存(2s 防抖)完成前:
607
+ // 纯 init 批次免 dirty,guard 不拦(修复前此处 dirty=true 误拦——审查②)
608
+ await act(async () => {
609
+ await vi.advanceTimersByTimeAsync(500)
610
+ await Promise.resolve()
611
+ })
612
+ expect(blockerCapture.opts?.shouldBlockFn(SHOULD_BLOCK_ARGS)).toBe(false)
613
+ // init 防抖保存照常落库(ID 持久化行为保留),期间仍不拦
614
+ await act(async () => {
615
+ await vi.advanceTimersByTimeAsync(2000)
616
+ await Promise.resolve()
617
+ })
618
+ expect(mockUpdateDocumentFn).toHaveBeenCalled()
619
+ expect(blockerCapture.opts?.shouldBlockFn(SHOULD_BLOCK_ARGS)).toBe(false)
620
+ })
621
+
622
+ it('④ 降级期击键(未落库)→ 恢复协同:pendingChangeRef 归零,guard 不 dirty', async () => {
623
+ // offline 降级:DB 链路接管,击键同步置位 pendingChangeRef(collabActive=false)
624
+ fakeProvider = buildFakeProvider('offline')
625
+ const { editor } = await renderUntilEditorReady({ collab: true, collabSnapshot: null })
626
+ expect(editor).not.toBeNull()
627
+
628
+ await insertText(editor!, '降级期编辑')
629
+ expect(blockerCapture.opts?.shouldBlockFn(SHOULD_BLOCK_ARGS)).toBe(true)
630
+
631
+ // 2s 防抖保存到期前恢复协同(审查④:恢复分支漏归零 → 该实例存活期
632
+ // guard 恒 dirty——协同态保存被 guard 短路,唯一归零点永不执行)
633
+ await act(async () => {
634
+ fakeProvider!.__setStatus('synced')
635
+ })
636
+ expect(blockerCapture.opts?.shouldBlockFn(SHOULD_BLOCK_ARGS)).toBe(false)
637
+ })
638
+ })
@@ -31,6 +31,7 @@ import { useEditorWidth, type EditorWidth, EDITOR_WIDTH_MAX_WIDTH } from '@/comp
31
31
  import { DOC_EDITOR_WIDTH_VARS } from './knowledge-doc-editor-vars'
32
32
  import { DocHeader } from '@/components/knowledge/editor/doc-header'
33
33
  import { useAutoSaveChannels } from '@/components/knowledge/editor/use-auto-save-channels'
34
+ import { useUnsavedGuard } from '@/hooks/use-unsaved-guard'
34
35
  import { DocumentProperties, type PropertyField, type PropertyValues } from '@/components/knowledge/doc-properties'
35
36
  import type { NotionDocEditorProps, DocumentSuggestion } from '@/components/tiptap/notion-like'
36
37
  import type { MindmapDocEditorProps } from '@/components/knowledge/editor/mindmap-doc-editor'
@@ -304,7 +305,7 @@ function KnowledgeDocEditorBody(bodyProps: KnowledgeDocEditorProps & { collabEna
304
305
  },
305
306
  }), [document.kbId])
306
307
 
307
- const { setDirty, triggerDebouncedSave } = useAutoSaveChannels<{ html: string; markdown: string }>({
308
+ const { dirty, setDirty, triggerDebouncedSave, saveIfContent } = useAutoSaveChannels<{ html: string; markdown: string }>({
308
309
  getContent: () => latestContent.current,
309
310
  // 协同写手活跃时 DB 保存一律短路(Y 是唯一写手;降级期恢复前绝不双写)
310
311
  guard: () => collabActiveRef.current,
@@ -314,6 +315,8 @@ function KnowledgeDocEditorBody(bodyProps: KnowledgeDocEditorProps & { collabEna
314
315
  const saver = saveHandlers?.content ?? ((c: { html: string; markdown: string }) => updateDocumentFn({ data: { id: docId.current, content: c } }))
315
316
  await saver(content)
316
317
  setDirty(false)
318
+ // 同步击键信号与 dirty 同点归零(任务 2 审查修复①):无接缝盲区
319
+ pendingChangeRef.current = false
317
320
  setLastSavedAt(new Date())
318
321
  } catch (e) {
319
322
  console.error('Auto-save failed:', e instanceof Error ? e.message : String(e))
@@ -323,6 +326,38 @@ function KnowledgeDocEditorBody(bodyProps: KnowledgeDocEditorProps & { collabEna
323
326
  },
324
327
  })
325
328
 
329
+ // ── 未保存拦截 dirty 信号(任务 2 审查修复①②)──────────────────────
330
+ // pendingChangeRef:击键同步置位(NotionDocEditor.onDirtySignal,先于
331
+ // 500ms onChange 防抖),保存成功时与 dirty 同点归零——消灭「最后一次
332
+ // 击键后 500ms 内导航不拦」的数据丢失窗口。协同态不置位(Y 是唯一写手)。
333
+ // sawFirstChangeRef:挂载后首个 onChange 若无同步击键信号,即 UniqueID
334
+ // 初始化 ID 归一化批次(appendTransaction 插件事务,非用户编辑)——照常
335
+ // 刷新 latestContent 并武装防抖保存(ID 持久化行为不变),但不置 dirty,
336
+ // init 保存 settle 前 guard 保持 clean(审查修复②:打开即误拦)。
337
+ const pendingChangeRef = useRef(false)
338
+ const sawFirstChangeRef = useRef(false)
339
+
340
+ // SPA 导航未保存拦截(任务 2):dirty 时弹 AlertDialog,「离开」先 flush
341
+ // 尽力保存(saveIfContent)再放行——比现状(SPA 导航静默丢防抖窗口内容)
342
+ // 更安全;浏览器关闭/刷新的通道三尽力保存仍由 useAutoSaveChannels 保留
343
+ const unsavedGuard = useUnsavedGuard({
344
+ isDirty: () => pendingChangeRef.current || dirty,
345
+ onSaveAndLeave: () => {
346
+ // flush 前从编辑器实例取回防抖窗口内未交付的最新内容(可能含
347
+ // <500ms 击键),否则「离开」保存的是 stale latestContent
348
+ const ed = editorRef.current
349
+ if (ed && !ed.isDestroyed) {
350
+ latestContent.current = {
351
+ html: ed.getHTML(),
352
+ markdown:
353
+ (ed.storage.markdown as { manager?: { serialize?: (json: unknown) => string } } | undefined)
354
+ ?.manager?.serialize?.(ed.getJSON()) ?? '',
355
+ }
356
+ }
357
+ return saveIfContent()
358
+ },
359
+ })
360
+
326
361
  // 降级/恢复切换语义(核心不变式:任一时刻只有一条保存链路活跃):
327
362
  // 切 offline 瞬间——把编辑器当前内容(含协同期 Y 独占、未落库的编辑)
328
363
  // 搬进 DB 保存链路的 latestContent 并置 dirty,同时武装 2s 防抖保存,
@@ -347,6 +382,10 @@ function KnowledgeDocEditorBody(bodyProps: KnowledgeDocEditorProps & { collabEna
347
382
  } else {
348
383
  latestContent.current = null
349
384
  setDirty(false)
385
+ // 恢复协同必须同点归零同步击键信号(审查修复④):降级期击键置位后
386
+ // 若防抖保存到期前恢复,协同态保存被短路、保存成功归零点永不执行,
387
+ // 该实例存活期 guard 恒 dirty
388
+ pendingChangeRef.current = false
350
389
  }
351
390
  }, [collabState.collabActive, collabEnabled, setDirty, triggerDebouncedSave])
352
391
 
@@ -356,6 +395,16 @@ function KnowledgeDocEditorBody(bodyProps: KnowledgeDocEditorProps & { collabEna
356
395
  // 通道一守卫:协同态(含远端 update 触发的 onChange)不进 DB 保存链路
357
396
  if (collabActiveRef.current) return
358
397
  latestContent.current = content
398
+ // init 批次豁免(任务 2 审查修复②):首个 onChange 且无同步击键信号
399
+ // = 初始化归一化(非用户编辑),只刷新内容 + 照常武装防抖保存,不置
400
+ // dirty(守卫 clean);真实击键(含 init 窗口内)走正常 dirty 路径
401
+ if (!sawFirstChangeRef.current) {
402
+ sawFirstChangeRef.current = true
403
+ if (!pendingChangeRef.current) {
404
+ triggerDebouncedSave()
405
+ return
406
+ }
407
+ }
359
408
  setDirty(true)
360
409
  triggerDebouncedSave()
361
410
  },
@@ -449,6 +498,7 @@ function KnowledgeDocEditorBody(bodyProps: KnowledgeDocEditorProps & { collabEna
449
498
  {m.docEditor_saving()}
450
499
  </div>
451
500
  )}
501
+ {unsavedGuard.dialog}
452
502
  <div className="flex flex-col h-full" style={DOC_EDITOR_WIDTH_VARS[editorWidth] as React.CSSProperties}>
453
503
  <div className="px-20 pt-6 pb-2 mx-auto w-full" style={{ maxWidth: EDITOR_WIDTH_MAX_WIDTH[editorWidth] }}>
454
504
  <DocHeader
@@ -493,6 +543,11 @@ function KnowledgeDocEditorBody(bodyProps: KnowledgeDocEditorProps & { collabEna
493
543
  <NotionDocEditor
494
544
  content={collabState.config ? undefined : content}
495
545
  onChange={handleContentChange}
546
+ onDirtySignal={() => {
547
+ // 同步击键信号(审查修复①):协同态不置位(Y 是唯一写手,
548
+ // 与通道一守卫同口径)
549
+ if (!collabActiveRef.current) pendingChangeRef.current = true
550
+ }}
496
551
  placeholder={m.docEditor_contentPlaceholder()}
497
552
  showTocSidebar
498
553
  kbId={document.kbId}
@@ -23,6 +23,7 @@ import { DocHeader } from "@/components/knowledge/editor/doc-header"
23
23
  import { parseMindmapContent, createDefaultMindmapData } from "@/components/tiptap/mindmap/lib/default-mindmap-data"
24
24
  import type { MindElixirData } from "mind-elixir"
25
25
  import { m } from "@/paraglide/messages.js";
26
+ import { useUnsavedGuard } from "@/hooks/use-unsaved-guard";
26
27
 
27
28
  const MindMapLazy = lazy(() =>
28
29
  import("@/components/tiptap/mindmap/mind-map").then((m) => ({ default: m.MindMap })),
@@ -93,6 +94,16 @@ export function MindmapDocEditor({ document, kbName }: MindmapDocEditorProps) {
93
94
  { wait: EDITOR_AUTO_SAVE_MS },
94
95
  )
95
96
 
97
+ // SPA 导航未保存拦截(任务 2):「离开」先 flush 当前内容(doSaveRef)再
98
+ // 放行;下方 beforeunload 尽力保存保留(浏览器关闭/刷新兜底)
99
+ const unsavedGuard = useUnsavedGuard({
100
+ isDirty: () => dirty,
101
+ onSaveAndLeave: () => {
102
+ const c = latestContent.current
103
+ if (c) return doSaveRef.current(c)
104
+ },
105
+ })
106
+
96
107
  const handleChange = useCallback((newData: MindElixirData, _operation: unknown) => {
97
108
  latestContent.current = newData
98
109
  setMindmapData(newData)
@@ -143,6 +154,7 @@ export function MindmapDocEditor({ document, kbName }: MindmapDocEditorProps) {
143
154
  {m.mindmap_saving()}
144
155
  </div>
145
156
  )}
157
+ {unsavedGuard.dialog}
146
158
  <div className="px-20 pt-6 pb-2 mx-auto w-full" style={{ maxWidth: headerMaxWidth }}>
147
159
  <DocHeader
148
160
  title={displayTitle}
@@ -89,5 +89,7 @@ export function useAutoSaveChannels<T>(opts: AutoSaveChannelsOptions<T>) {
89
89
  return () => window.removeEventListener("beforeunload", handleBeforeUnload)
90
90
  }, [saveIfContent])
91
91
 
92
- return { dirty, setDirty, triggerDebouncedSave }
92
+ // saveIfContent 一并透出:use-unsaved-guard 的「离开」flush(任务 2)与
93
+ // 通道三复用同一保存入口
94
+ return { dirty, setDirty, triggerDebouncedSave, saveIfContent }
93
95
  }
@@ -53,10 +53,6 @@ vi.mock("@/lib/utils", () => ({
53
53
  cn: (...classes: string[]) => classes.filter(Boolean).join(" "),
54
54
  }));
55
55
 
56
- vi.mock("@/lib/collections/reference/workspaces-collection", () => ({
57
- WORKSPACES_QUERY_KEY: ["workspaces"],
58
- }));
59
-
60
56
  vi.mock("@/server/serverFns/workspace", () => ({
61
57
  listPinnedWorkspacesBriefFn: vi.fn().mockResolvedValue([]),
62
58
  toggleWorkspacePinFn: vi.fn().mockResolvedValue({ success: true }),