@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
@@ -16,6 +16,7 @@
16
16
  // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
17
 
18
18
  import { useEffect, useRef, useState, type ComponentType } from "react"
19
+ import { useHotkeys } from "@tanstack/react-hotkeys"
19
20
  import {
20
21
  ArrowRight,
21
22
  ChevronDown,
@@ -148,26 +149,19 @@ export function useCanvasDrawing(shapeVariant?: string | null) {
148
149
  setTool(id)
149
150
  }
150
151
 
151
- useEffect(() => {
152
- if (!enabled) return
153
- const hotkeyMap = new Map<string, ToolId>()
154
- for (const definition of TOOL_DEFINITIONS) {
155
- const hotkey = definition.hotkey
156
- if (!hotkey) continue
157
- hotkeyMap.set(hotkey.toLowerCase(), definition.id)
158
- }
159
- const handler = (e: KeyboardEvent) => {
160
- if (isTypingTarget(e.target as HTMLElement)) return
161
- if (e.ctrlKey || e.metaKey || e.altKey) return
162
- const id = hotkeyMap.get(e.key.toLowerCase())
163
- if (id) {
164
- e.preventDefault()
165
- selectTool(id)
166
- }
167
- }
168
- document.addEventListener("keydown", handler)
169
- return () => document.removeEventListener("keydown", handler)
170
- }, [enabled])
152
+ // 收编手写 keydown(审计「canvas 工具单键」项):TOOL_DEFINITIONS 单键 →
153
+ // useHotkeys 数组注册(动态长度安全,不违反 hooks 规则)。enabled 门控、
154
+ // 输入框守卫(ignoreInputs 等价旧 isTypingTarget 判断,范围更宽更安全)、
155
+ // preventDefault 均由官方通道承担;无修饰键 chord 与旧「仅拦 ctrl/meta/alt、
156
+ // 放行 shift」的差异是官方精确匹配修饰键(shift+单键不再触发,Figma 同款语义)
157
+ useHotkeys(
158
+ TOOL_DEFINITIONS.flatMap((definition) =>
159
+ definition.hotkey
160
+ ? [{ hotkey: definition.hotkey, callback: () => selectTool(definition.id) }]
161
+ : [],
162
+ ),
163
+ { enabled, ignoreInputs: true, preventDefault: true },
164
+ )
171
165
 
172
166
  useEffect(() => {
173
167
  if (!enabled || tool === "select") return
@@ -216,7 +210,10 @@ export function useCanvasDrawing(shapeVariant?: string | null) {
216
210
  window.addEventListener("pointerdown", onPointerDown, true)
217
211
  window.addEventListener("pointermove", onPointerMove)
218
212
  window.addEventListener("dblclick", onDoubleClick)
219
- // keydown 捕获相:同钢笔分支,先于 document 冒泡相的删除监听
213
+ // keydown 捕获相:同钢笔分支,先于 document 冒泡相的删除监听。
214
+ // 豁免收编(audit 2026-10-31):捕获相时序敏感——绘制态的删除键必须先于
215
+ // useDeleteSelection(useHotkeys,document 冒泡相)拦截,官方 API 无
216
+ // 「绘制态条件捕获相」的表达,保持手写监听
220
217
  window.addEventListener("keydown", onKeyDown, true)
221
218
  return () => {
222
219
  window.removeEventListener("pointerdown", onPointerDown, true)
@@ -272,7 +269,9 @@ export function useCanvasDrawing(shapeVariant?: string | null) {
272
269
  window.addEventListener("dblclick", onDoubleClick)
273
270
  // keydown 必须捕获相:useDeleteSelection 在 document 冒泡相(冒泡顺序
274
271
  // document 先于 window),冒泡相注册的 stopPropagation 来不及拦截——
275
- // 「选中 A → 切钢笔 → 绘制中按 Backspace」会同时删 A 并回退锚点
272
+ // 「选中 A → 切钢笔 → 绘制中按 Backspace」会同时删 A 并回退锚点。
273
+ // 豁免收编(audit 2026-10-31):捕获相时序敏感,官方 API 无
274
+ // 「绘制态条件捕获相」的表达,保持手写监听
276
275
  window.addEventListener("keydown", onKeyDown, true)
277
276
  return () => {
278
277
  window.removeEventListener("pointerdown", onPointerDown, true)
@@ -0,0 +1,199 @@
1
+ // @vitest-environment jsdom
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ /**
5
+ * CanvasProvider loadDoc 回显豁免回归(任务 2 审查修复轮 2 / 缺陷③):
6
+ *
7
+ * engine.loadDoc → notifyDocChange → pushDoc → 父层 onDocChange(即
8
+ * useDesignAutoSave.handleDocChange → setCanvasDirty(true))——打开/切页后
9
+ * ~2s 内 canvasDirty 幻影 true,未保存 guard 误拦(旧 beforeunload 同样误报)。
10
+ * 修复:pushDoc 在 loadDoc 同步执行窗口内只刷新 store 快照,不通知 React
11
+ * 父层;窗口 = loadDoc 调用栈,用户事件无法插入(无吞真实编辑的竞态)。
12
+ *
13
+ * 断言落到「父层通知 → handleDocChange → canvasDirty」链(真实
14
+ * useDesignAutoSave + 真实 CanvasProvider 接线,LeaferEngine 以替身驱动)。
15
+ * 引擎本体在 jsdom 不可真实驱动(Leafer WebGL),按审查许可取 notify 层
16
+ * 断言——取舍:不覆盖引擎内部 applyScene 的通知次数,但豁免按「同步窗口」
17
+ * 而非一次性开关实现,窗口内任意次回显通知都被吞掉,不依赖次数假设。
18
+ */
19
+
20
+ import { describe, expect, it, vi, beforeEach } from "vitest"
21
+ import { renderHook, act } from "@testing-library/react"
22
+ import { render } from "@testing-library/react"
23
+ import { useContext, useState } from "react"
24
+
25
+ const engineSlots = vi.hoisted(() => ({
26
+ create: vi.fn(),
27
+ }))
28
+
29
+ vi.mock("@/design-engine/engine/app", () => ({
30
+ LeaferEngine: {
31
+ create: (opts: unknown) => engineSlots.create(opts),
32
+ },
33
+ }))
34
+
35
+ import { CanvasContext, CanvasProvider } from "../canvas-context"
36
+ import { LeaferEngine } from "@/design-engine/engine/app"
37
+ import { useDesignAutoSave } from "@/components/design/editor/layout/hooks/use-design-auto-save"
38
+ import { createEmptyDoc, createBoardNode, type SceneDocument } from "@/components/design/canvas/document/types"
39
+ import type { DocContent } from "@/components/design/editor/layout/design-editor-doc-utils"
40
+ import { updateDesignPageFn } from "@/server/serverFns/design/pages"
41
+
42
+ vi.mock("@/server/serverFns/design/pages", () => ({
43
+ updateDesignPageFn: vi.fn(async () => ({ id: "p1" })),
44
+ }))
45
+
46
+ type HookArgs = Parameters<typeof useDesignAutoSave>[0]
47
+
48
+ const emptyScene: SceneDocument = createEmptyDoc("desktop")
49
+
50
+ function userScene(): SceneDocument {
51
+ return {
52
+ version: 1,
53
+ meta: { device: "desktop" },
54
+ nodes: [
55
+ createBoardNode("desktop"),
56
+ {
57
+ id: "n_1",
58
+ kind: "shape",
59
+ type: "rect",
60
+ parentId: "board",
61
+ x: 10,
62
+ y: 10,
63
+ width: 40,
64
+ height: 40,
65
+ style: {},
66
+ props: {},
67
+ },
68
+ ],
69
+ } as unknown as SceneDocument
70
+ }
71
+
72
+ function baseArgs(over: Partial<HookArgs> = {}): HookArgs {
73
+ return {
74
+ activePageId: "page_1",
75
+ sceneDoc: emptyScene,
76
+ setSceneDoc: vi.fn(),
77
+ docContent: { html: "", markdown: "" },
78
+ setDocContent: vi.fn(),
79
+ canvasDirty: false,
80
+ setCanvasDirty: vi.fn(),
81
+ docDirty: false,
82
+ setDocDirty: vi.fn(),
83
+ isPreviewingRef: { current: false },
84
+ canvasLoadedRef: { current: true },
85
+ lastLoadedDocRef: { current: null },
86
+ timezone: "Asia/Shanghai",
87
+ setCanvasAutoSaveTime: vi.fn(),
88
+ setDocAutoSaveTime: vi.fn(),
89
+ docSaveFnRef: { current: async () => {} },
90
+ docDebouncer: { maybeExecute: vi.fn(), cancel: vi.fn() },
91
+ ...over,
92
+ }
93
+ }
94
+
95
+ type FakeEngine = {
96
+ mount: ReturnType<typeof vi.fn>
97
+ dispose: ReturnType<typeof vi.fn>
98
+ remount: ReturnType<typeof vi.fn>
99
+ setKeyEventEnabled: ReturnType<typeof vi.fn>
100
+ getZoom: () => number
101
+ history: { canUndo: boolean; canRedo: boolean }
102
+ onDocChange: (cb: (doc: SceneDocument) => void) => () => void
103
+ onSelectionChange: (cb: unknown) => () => void
104
+ onToolChange: (cb: unknown) => () => void
105
+ loadDoc: (doc: SceneDocument) => void
106
+ }
107
+
108
+ function installFakeEngine() {
109
+ let docListener: ((doc: SceneDocument) => void) | null = null
110
+ const loadDocSpy = vi.fn()
111
+ const fakeEngine = {
112
+ mount: vi.fn(),
113
+ dispose: vi.fn(),
114
+ remount: vi.fn(),
115
+ setKeyEventEnabled: vi.fn(),
116
+ getZoom: () => 1,
117
+ history: { canUndo: false, canRedo: false },
118
+ onDocChange: vi.fn((cb: (doc: SceneDocument) => void) => {
119
+ docListener = cb
120
+ return () => {}
121
+ }),
122
+ onSelectionChange: vi.fn(() => () => {}),
123
+ onToolChange: vi.fn(() => () => {}),
124
+ // 真实引擎 loadDoc 末尾同步 notifyDocChange(engine-doc.ts:164)——回显
125
+ loadDoc: vi.fn((doc: SceneDocument) => {
126
+ loadDocSpy(doc)
127
+ docListener?.(doc)
128
+ }),
129
+ }
130
+ engineSlots.create.mockResolvedValue(fakeEngine)
131
+ return { fakeEngine: fakeEngine as unknown as LeaferEngine & FakeEngine, notifyDoc: (doc: SceneDocument) => docListener?.(doc), loadDocSpy }
132
+ }
133
+
134
+ /** 把画布 host 挂进 provider(真实挂载链:attachHost → hostRef → create effect) */
135
+ function HostMount() {
136
+ const canvas = useContext(CanvasContext)
137
+ return <div ref={(node) => canvas?.attachHost(node)} data-testid="canvas-host" />
138
+ }
139
+
140
+ beforeEach(() => {
141
+ vi.clearAllMocks()
142
+ vi.mocked(updateDesignPageFn).mockClear()
143
+ })
144
+
145
+ describe("CanvasProvider loadDoc 回显豁免(审查修复③)", () => {
146
+ it("loadDoc 回显(挂载 + 切页)不置 canvasDirty;真实用户编辑同步置 dirty", async () => {
147
+ const { notifyDoc, loadDocSpy } = installFakeEngine()
148
+
149
+ // hook 持真实 state(dirty 断言目标),非可 mock 的 setter
150
+ const { result } = renderHook(() => {
151
+ const [canvasDirty, setCanvasDirty] = useState(false)
152
+ const [docDirty, setDocDirty] = useState(false)
153
+ const [sceneDoc, setSceneDoc] = useState(emptyScene)
154
+ const [docContent, setDocContent] = useState<DocContent>({ html: "", markdown: "" })
155
+ return useDesignAutoSave({
156
+ ...baseArgs(),
157
+ sceneDoc,
158
+ setSceneDoc,
159
+ docContent,
160
+ setDocContent,
161
+ canvasDirty,
162
+ setCanvasDirty,
163
+ docDirty,
164
+ setDocDirty,
165
+ })
166
+ })
167
+
168
+ const view = render(
169
+ <CanvasProvider initialDoc={emptyScene} onDocChange={(doc) => result.current.handleDocChange(doc)}>
170
+ <HostMount />
171
+ </CanvasProvider>,
172
+ )
173
+ // flush 引擎 create promise → mount → loadDoc(挂载回显)
174
+ await act(async () => {})
175
+
176
+ expect(loadDocSpy).toHaveBeenCalledTimes(1)
177
+ // 关键断言①:挂载 loadDoc 回显未置 dirty(修复前此处 true——幻影 dirty)
178
+ expect(result.current.canvasDirty).toBe(false)
179
+
180
+ // 切页:initialDoc 换新 → engine.loadDoc(新页) → 回显
181
+ await act(async () => {
182
+ view.rerender(
183
+ <CanvasProvider initialDoc={userScene()} onDocChange={(doc) => result.current.handleDocChange(doc)}>
184
+ <HostMount />
185
+ </CanvasProvider>,
186
+ )
187
+ })
188
+ expect(loadDocSpy).toHaveBeenCalledTimes(2)
189
+ // 关键断言②:切页 loadDoc 回显同样不置 dirty
190
+ expect(result.current.canvasDirty).toBe(false)
191
+
192
+ // 真实用户编辑一笔(引擎编辑路径:load 窗口外的 docChanged notify)
193
+ await act(async () => {
194
+ notifyDoc(userScene())
195
+ })
196
+ // 关键断言③:真实编辑同步置 dirty(豁免不吞编辑)
197
+ expect(result.current.canvasDirty).toBe(true)
198
+ })
199
+ })
@@ -789,7 +789,16 @@ describe("canvas hooks", () => {
789
789
  await waitFor(() => expect(engine.mount).toHaveBeenCalledTimes(1))
790
790
  expect(screen.getByTestId("pan-mode").textContent).toBe("false")
791
791
 
792
- fireEvent.keyDown(screen.getByTestId("plain-input"), { key: " " })
792
+ // 真实浏览器中 keydown 恒派发给聚焦元素(e.target === activeElement);
793
+ // 收编后的输入框门控走 typingFocus(focusin/focusout 事实源),故先
794
+ // 聚焦输入框再派发空格键
795
+ const input = screen.getByTestId("plain-input")
796
+ act(() => {
797
+ input.focus()
798
+ })
799
+ fireEvent.keyDown(input, { key: " " })
800
+ expect(screen.getByTestId("pan-mode").textContent).toBe("false")
801
+ fireEvent.keyUp(input, { key: " " })
793
802
  expect(screen.getByTestId("pan-mode").textContent).toBe("false")
794
803
 
795
804
  unmount()
@@ -808,7 +817,10 @@ describe("canvas hooks", () => {
808
817
  Object.defineProperty(editable, "isContentEditable", { value: true, configurable: true })
809
818
  document.body.appendChild(editable)
810
819
  try {
811
- editable.focus()
820
+ // focus/blur 触发 focusin/focusout → CanvasProvider typingFocus setState,须包 act
821
+ act(() => {
822
+ editable.focus()
823
+ })
812
824
  fireEvent.keyDown(editable, { key: " " })
813
825
  expect(screen.getByTestId("pan-mode").textContent).toBe("false")
814
826
  // keyUp 也不应触发(panMode 从未变为 true)
@@ -816,7 +828,9 @@ describe("canvas hooks", () => {
816
828
  expect(screen.getByTestId("pan-mode").textContent).toBe("false")
817
829
  } finally {
818
830
  editable.remove()
819
- if (document.activeElement instanceof HTMLElement) document.activeElement.blur()
831
+ act(() => {
832
+ if (document.activeElement instanceof HTMLElement) document.activeElement.blur()
833
+ })
820
834
  }
821
835
 
822
836
  unmount()
@@ -17,12 +17,16 @@
17
17
 
18
18
  import {
19
19
  createContext,
20
+ useCallback,
20
21
  useEffect,
21
22
  useRef,
23
+ useState,
22
24
  type ReactNode,
23
25
  type RefObject,
24
26
  } from "react"
25
27
 
28
+ import { useHotkey, useKeyHold } from "@tanstack/react-hotkeys"
29
+
26
30
  import { isTypingTarget } from "../tools/dom"
27
31
  import { LeaferEngine } from "@/design-engine/engine/app"
28
32
  import { createEmptyDoc, type DeviceType, type SceneDocument, type ToolId } from "../document/types"
@@ -116,6 +120,21 @@ export function CanvasProvider({
116
120
  const engineRef = useRef<LeaferEngine | null>(null)
117
121
  const lastEmittedDocRef = useRef<SceneDocument | null>(null)
118
122
  const selectionNotifyRafRef = useRef<number | null>(null)
123
+ // loadDoc 回显豁免(任务 2 审查修复③):loadDoc 末尾同步 notifyDocChange
124
+ // → pushDoc → 父层 onDocChange → handleDocChange 误置 canvasDirty(打开/
125
+ // 切页后幻影 dirty,未保存 guard 误拦)。窗口 = loadDoc 同步调用栈(用户
126
+ // 事件无法插入,无吞真实编辑竞态);窗口内 pushDoc 仍刷新 store 快照
127
+ //(lastEmittedDocRef 供 initialDoc effect 判等、doc 供画布消费),仅跳过
128
+ // 父层通知。trackChanges/编辑路径不受影响(发生在窗口外,照常同步置 dirty)。
129
+ const loadEchoGateRef = useRef(false)
130
+ const loadDocSilently = useCallback((eng: LeaferEngine, doc: SceneDocument): void => {
131
+ loadEchoGateRef.current = true
132
+ try {
133
+ eng.loadDoc(doc)
134
+ } finally {
135
+ loadEchoGateRef.current = false
136
+ }
137
+ }, [])
119
138
 
120
139
  flagsRef.current = { readOnly }
121
140
  callbacksRef.current = { initialDoc, onDocChange }
@@ -182,7 +201,10 @@ export function CanvasProvider({
182
201
  lastEmittedDocRef.current = doc
183
202
  state.doc = doc
184
203
  if (doc.meta?.device) state.device = doc.meta.device
185
- callbacksRef.current.onDocChange?.(doc)
204
+ // loadDoc 回显窗口内只刷新快照、不通知父层(注释见 loadEchoGateRef)
205
+ if (!loadEchoGateRef.current) {
206
+ callbacksRef.current.onDocChange?.(doc)
207
+ }
186
208
  refreshMetrics()
187
209
  rebuild()
188
210
  },
@@ -248,7 +270,7 @@ export function CanvasProvider({
248
270
  created.onToolChange(store.pushTool),
249
271
  )
250
272
  const initial = callbacksRef.current.initialDoc
251
- if (initial) created.loadDoc(initial)
273
+ if (initial) loadDocSilently(created, initial)
252
274
  store.attach(created)
253
275
  // 诊断埋点:dev 环境暴露引擎引用,供真实页面逐层调试命中/选中链路
254
276
  if (import.meta.env.DEV) {
@@ -298,30 +320,24 @@ export function CanvasProvider({
298
320
  const eng = engineRef.current
299
321
  if (!eng || !initialDoc) return
300
322
  if (initialDoc === lastEmittedDocRef.current) return
301
- eng.loadDoc(initialDoc)
302
- }, [initialDoc])
323
+ loadDocSilently(eng, initialDoc)
324
+ }, [initialDoc, loadDocSilently])
303
325
 
304
- // 空格键:按下进入 pan 模式(手型光标 + 左键可拖动),松开退出。
305
- // 输入框/textarea/contenteditable 聚焦时不响应(否则会打断用户输入)。
306
- // 与右键拖动并列:任一持有时 panMode 即为 true。
326
+ // 空格键(收编手写 keydown/keyup,审计「空格 pan」项):按住进入 pan 模式
327
+ //(手型光标 + 左键可拖动),松开退出;与右键拖动并列,任一持有 panMode 即 true。
328
+ // hold 状态来自官方 KeyStateTracker(useKeyHold("Space")——tracker 归一化
329
+ // " "→"Space",勿传字面空格),窗口失焦自动清位(旧实现 keyup 丢失会卡 pan)。
330
+ // tracker 无输入框概念,输入框聚焦不响应由下方 typingFocus 门控承担。
331
+ // 官方 API 缺口:useKeyHold 不做 preventDefault,空格的页面滚动/焦点控件
332
+ // 激活抑制由并行 "Space" chord 注册(preventDefault)承担;官方对聚焦
333
+ // button/link 的空格激活按 ignoreInputs 跳过(无障碍语义,旧实现一律拦死,
334
+ // 此处为对齐官方语义的有意放宽)。
335
+ const spaceHeld = useKeyHold("Space")
336
+ const [typingFocus, setTypingFocus] = useState(false)
307
337
  useEffect(() => {
308
- const onKeyDown = (e: KeyboardEvent) => {
309
- if (e.key !== " ") return
310
- if (isTypingTarget(e.target as HTMLElement)) return
311
- store.setSpaceHeld(true)
312
- e.preventDefault()
313
- }
314
- const onKeyUp = (e: KeyboardEvent) => {
315
- if (e.key !== " ") return
316
- store.setSpaceHeld(false)
317
- }
318
- document.addEventListener("keydown", onKeyDown)
319
- document.addEventListener("keyup", onKeyUp)
320
- return () => {
321
- document.removeEventListener("keydown", onKeyDown)
322
- document.removeEventListener("keyup", onKeyUp)
323
- }
324
- }, [store])
338
+ store.setSpaceHeld(spaceHeld && !typingFocus)
339
+ }, [spaceHeld, typingFocus, store])
340
+ useHotkey("Space", () => {}, { ignoreInputs: true, preventDefault: true })
325
341
 
326
342
  // 输入框聚焦时关闭 leafer-editor 的键盘监听(leafer-editor 官方方案)。
327
343
  // 从 leafer-editor 2.3.0 源码定位:
@@ -339,12 +355,17 @@ export function CanvasProvider({
339
355
  return isTypingTarget(el as HTMLElement | null)
340
356
  }
341
357
  const onDocFocus = (): void => {
342
- engineRef.current?.setKeyEventEnabled(!isInsideTyping())
358
+ const inside = isInsideTyping()
359
+ // 焦点位置提升为 React 状态:空格 pan 的 typingFocus 门控复用同一事实源
360
+ setTypingFocus(inside)
361
+ engineRef.current?.setKeyEventEnabled(!inside)
343
362
  }
344
363
  document.addEventListener("focusin", onDocFocus)
345
364
  document.addEventListener("focusout", onDocFocus)
346
365
  // 初始状态同步一次(防止 mount 时焦点已在输入框)
347
- engineRef.current?.setKeyEventEnabled(!isInsideTyping())
366
+ const initialInside = isInsideTyping()
367
+ setTypingFocus(initialInside)
368
+ engineRef.current?.setKeyEventEnabled(!initialInside)
348
369
  return () => {
349
370
  document.removeEventListener("focusin", onDocFocus)
350
371
  document.removeEventListener("focusout", onDocFocus)
@@ -16,7 +16,7 @@
16
16
  // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
17
 
18
18
  import { useCallback, useEffect, type RefObject } from "react"
19
- import { useHotkey } from "@tanstack/react-hotkeys"
19
+ import { useHotkey, useHotkeys } from "@tanstack/react-hotkeys"
20
20
 
21
21
  import { toast } from "@/components/ui/toast"
22
22
  import { m } from "@/paraglide/messages.js"
@@ -158,19 +158,17 @@ export function useDeleteSelection() {
158
158
  const engine = snapshot.engine
159
159
  const enabled = Boolean(engine) && !snapshot.readOnly
160
160
 
161
- useEffect(() => {
162
- if (!enabled || !engine) return
163
- const handler = (e: KeyboardEvent) => {
164
- if (e.key !== "Delete" && e.key !== "Backspace") return
165
- if (e.ctrlKey || e.metaKey || e.altKey) return
166
- const target = e.target as HTMLElement
167
- if (isTypingTarget(target)) return
168
- engine.deleteSelectedNodes()
169
- e.preventDefault()
170
- }
171
- document.addEventListener("keydown", handler)
172
- return () => document.removeEventListener("keydown", handler)
173
- }, [enabled, engine])
161
+ // 收编手写 keydown(审计第 4 处):Delete/Backspace 删除选中,改官方
162
+ // useHotkeys 双 chord——enabled 门控、ignoreInputs 输入框守卫、preventDefault
163
+ // 均由官方通道承担。仍为 document 冒泡相(useHotkeys 默认 target=document),
164
+ // toolbar 绘制中的捕获相 stopPropagation 拦截先于本注册生效,时序与旧实现一致
165
+ useHotkeys(
166
+ [
167
+ { hotkey: "Delete", callback: () => engine?.deleteSelectedNodes() },
168
+ { hotkey: "Backspace", callback: () => engine?.deleteSelectedNodes() },
169
+ ],
170
+ { enabled, ignoreInputs: true, preventDefault: true },
171
+ )
174
172
  }
175
173
 
176
174
  // 编组/解组快捷键(FS-4.2):Mod+G 建组、Shift+Mod+G 解组;
@@ -252,34 +250,39 @@ export function useEditCommands(): void {
252
250
  }
253
251
 
254
252
  // 画布剪贴板快捷键:mod+A 全选 / mod+X 剪切 / mod+C 复制 / mod+V 粘贴;
255
- // 输入框聚焦时不拦截(属文本编辑器自身行为)
253
+ // 输入框聚焦时不拦截(属文本编辑器自身行为)。
254
+ // 收编手写 keydown(审计「Mod+C」项):改官方 useHotkeys 四 chord 注册,
255
+ // enabled 门控、ignoreInputs 守卫、preventDefault 拦截浏览器默认行为均由
256
+ // 官方通道承担
256
257
  export function useCanvasClipboard(): void {
257
258
  const snapshot = useCanvasSnapshot()
258
259
  const engine = snapshot.engine
259
260
  const enabled = Boolean(engine) && !snapshot.readOnly
260
261
 
261
- useEffect(() => {
262
- if (!enabled || !engine) return
263
- const handler = (e: KeyboardEvent) => {
264
- if (!(e.metaKey || e.ctrlKey) || e.altKey) return
265
- const key = e.key.toLowerCase()
266
- if (key !== "a" && key !== "x" && key !== "c" && key !== "v") return
267
- const target = e.target as HTMLElement
268
- if (isTypingTarget(target)) return
269
- if (key === "a") engine.selectAll()
270
- else if (key === "x") engine.cutSelection()
271
- else if (key === "c") engine.copySelection()
272
- else engine.pasteClipboard()
273
- e.preventDefault()
274
- }
275
- document.addEventListener("keydown", handler)
276
- return () => document.removeEventListener("keydown", handler)
277
- }, [enabled, engine])
262
+ const clipboardEdit = useCallback(
263
+ (op: "selectAll" | "cutSelection" | "copySelection" | "pasteClipboard") => {
264
+ engine?.[op]()
265
+ },
266
+ [engine],
267
+ )
268
+
269
+ useHotkeys(
270
+ [
271
+ { hotkey: "Mod+A", callback: () => clipboardEdit("selectAll") },
272
+ { hotkey: "Mod+X", callback: () => clipboardEdit("cutSelection") },
273
+ { hotkey: "Mod+C", callback: () => clipboardEdit("copySelection") },
274
+ { hotkey: "Mod+V", callback: () => clipboardEdit("pasteClipboard") },
275
+ ],
276
+ { enabled, ignoreInputs: true, preventDefault: true },
277
+ )
278
278
  }
279
279
 
280
280
  // Esc 逐级取消(Figma/Sketch 惯例):路径锚点编辑 → 退出编辑;绘制工具 →
281
281
  // 退回选择工具;选择模式 → 清空选中(全选/多选/单选一律取消)。钢笔绘制中
282
- // 的 Esc 由 toolbar 的捕获相处理(结束路径 + 退工具),此处冒泡相同事件幂等
282
+ // 的 Esc 由 toolbar 的捕获相处理(结束路径 + 退工具),此处冒泡相同事件幂等。
283
+ // 豁免收编(audit 2026-10-31「捕获相时序敏感」类):三态分支依赖 engine/工具
284
+ // 状态的判断顺序,且收编即与全局 close-panel(Escape)形成同 chord 运行时
285
+ // 注册冲突(findHotkeyConflicts 会告警)——保持手写监听
283
286
  export function useEscapeTool() {
284
287
  const { tool, setTool } = useTool()
285
288
  const engine = useCanvasSnapshotOptional()?.engine ?? null
@@ -309,7 +312,9 @@ export function useEscapeTool() {
309
312
  }, [tool, setTool, engine])
310
313
  }
311
314
 
312
- // Enter 进入/退出路径锚点编辑(单选 path/pen-path 时;Sketch 惯例)
315
+ // Enter 进入/退出路径锚点编辑(单选 path/pen-path 时;Sketch 惯例)。
316
+ // 豁免收编:Enter 是输入提交语义主键(表单/对话框/测试清单 case-checklist
317
+ // 同用 Enter),收编即扩大同 chord 运行时冲突面,收益低——保持手写监听
313
318
  export function usePathEditHotkey() {
314
319
  const engine = useCanvasSnapshotOptional()?.engine ?? null
315
320
  const selection = useCanvasSnapshotOptional()?.selection ?? []
@@ -53,6 +53,7 @@ vi.mock('@/components/design/canvas/plugins/presence/presence-layer', () => ({
53
53
 
54
54
  vi.mock('@tanstack/react-hotkeys', () => ({
55
55
  useHotkey: () => {},
56
+ useAppHotkeys: () => appHotkeys,
56
57
  }))
57
58
 
58
59
  vi.mock('@/components/design/editor/layout/design-left-sidebar', () => ({
@@ -100,7 +101,23 @@ vi.mock('@/server/serverFns/design/designs', () => ({
100
101
  updateDesignFn: vi.fn(),
101
102
  }))
102
103
 
104
+ // DesignEditorLayout 挂载 use-unsaved-guard(任务 2):无 RouterProvider 的
105
+ // 单测环境把 useBlocker 替换为 idle resolver(真实 blocker 注册属 router
106
+ // 集成行为,见 hooks/__tests__/use-unsaved-guard.test.tsx)
107
+ vi.mock('@tanstack/react-router', async (importOriginal) => ({
108
+ ...(await importOriginal<Record<string, unknown>>()),
109
+ useBlocker: () => ({
110
+ status: 'idle' as const,
111
+ current: undefined,
112
+ next: undefined,
113
+ action: undefined,
114
+ proceed: undefined,
115
+ reset: undefined,
116
+ }),
117
+ }))
118
+
103
119
  import { DesignEditorLayout } from '../design-editor-layout'
120
+ import { appHotkeys } from '@/lib/hotkeys/registry'
104
121
  import type { Design } from '@allbluecn/kernel'
105
122
 
106
123
  const designFixture = {
@@ -28,6 +28,7 @@ import { describe, expect, it, vi, beforeEach } from "vitest";
28
28
  import { render, fireEvent, waitFor, screen } from "@testing-library/react";
29
29
 
30
30
  import { CanvasContextMenu } from "../context-menu";
31
+ import { appHotkeys } from "@/lib/hotkeys/registry";
31
32
  import { buildContextMenuItems, type ContextMenuItem } from "../context-menu-items";
32
33
  import { DesignEditorLayout } from "../design-editor-layout";
33
34
  import type { Design } from "@allbluecn/kernel";
@@ -105,7 +106,22 @@ vi.mock("@/server/serverFns/design/pages", () => ({
105
106
  vi.mock("@/server/serverFns/design/designs", () => ({
106
107
  updateDesignFn: vi.fn(() => Promise.resolve({})),
107
108
  }));
108
- vi.mock("@tanstack/react-hotkeys", () => ({ useHotkey: () => {} }));
109
+ vi.mock("@tanstack/react-hotkeys", () => ({ useHotkey: () => {}, useAppHotkeys: () => appHotkeys }));
110
+
111
+ // DesignEditorLayout 挂载 use-unsaved-guard(任务 2):无 RouterProvider 的
112
+ // 单测环境把 useBlocker 替换为 idle resolver(真实 blocker 注册属 router
113
+ // 集成行为,见 hooks/__tests__/use-unsaved-guard.test.tsx)
114
+ vi.mock("@tanstack/react-router", async (importOriginal) => ({
115
+ ...(await importOriginal<Record<string, unknown>>()),
116
+ useBlocker: () => ({
117
+ status: "idle" as const,
118
+ current: undefined,
119
+ next: undefined,
120
+ action: undefined,
121
+ proceed: undefined,
122
+ reset: undefined,
123
+ }),
124
+ }));
109
125
 
110
126
 
111
127
  describe("CanvasContextMenu 组件", () => {
@@ -21,6 +21,22 @@ vi.mock("@tanstack/react-hotkeys", () => ({
21
21
  useHotkey: (_key: string, handler: () => void) => {
22
22
  hotkeyHandler = handler;
23
23
  },
24
+ useAppHotkeys: () => appHotkeys,
25
+ }));
26
+
27
+ // DesignEditorLayout 挂载 use-unsaved-guard(任务 2):无 RouterProvider 的
28
+ // 单测环境把 useBlocker 替换为 idle resolver(真实 blocker 注册属 router
29
+ // 集成行为,见 hooks/__tests__/use-unsaved-guard.test.tsx)
30
+ vi.mock("@tanstack/react-router", async (importOriginal) => ({
31
+ ...(await importOriginal<Record<string, unknown>>()),
32
+ useBlocker: () => ({
33
+ status: "idle" as const,
34
+ current: undefined,
35
+ next: undefined,
36
+ action: undefined,
37
+ proceed: undefined,
38
+ reset: undefined,
39
+ }),
24
40
  }));
25
41
 
26
42
  // 单机模式前提钉住(任务 30 装配后布局按 isCollabEnabled 门控保存让位):
@@ -99,6 +115,7 @@ import { toast } from "@/components/ui/toast";
99
115
  import { getDesignPageFn, updateDesignPageFn } from "@/server/serverFns/design/pages";
100
116
  import { m } from "@/paraglide/messages.js";
101
117
  import { DesignEditorLayout } from "../design-editor-layout";
118
+ import { appHotkeys } from "@/lib/hotkeys/registry";
102
119
  import type { Design } from "@allbluecn/kernel";
103
120
 
104
121
  const designFixture = {