@nocobase/portal-template-default 1.0.9 → 1.0.10

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 (304) hide show
  1. package/.env.example +5 -0
  2. package/.github/workflows/publish.yml +106 -0
  3. package/.gitignore +33 -0
  4. package/AGENTS.md +19 -0
  5. package/README.MD +61 -180
  6. package/package.json +3 -5
  7. package/scripts/build-html.mjs +12 -0
  8. package/src/App.css +2 -0
  9. package/src/App.tsx +35 -186
  10. package/src/app/development.tsx +154 -0
  11. package/src/app/extension.ts +5 -0
  12. package/src/app/extensions.tsx +18 -4
  13. package/src/app/routes.tsx +65 -0
  14. package/src/components/access-control/acl-bootstrap.tsx +11 -3
  15. package/src/components/access-control/acl-gate.tsx +7 -6
  16. package/src/components/access-control/can-access.tsx +17 -0
  17. package/src/components/access-control/navigate-to-accessible-resource.tsx +8 -3
  18. package/src/components/app-shell/sidebar.tsx +21 -5
  19. package/src/components/auth/auto-redirect-provider.tsx +2 -1
  20. package/src/extensions/nocobase-acl/README.md +74 -0
  21. package/{registry → src/extensions}/nocobase-acl/components/acl-boundary.tsx +22 -25
  22. package/{registry → src/extensions}/nocobase-acl/components/role-switcher.tsx +26 -23
  23. package/src/extensions/nocobase-acl/demo/acl-preview-provider.tsx +81 -0
  24. package/{registry → src/extensions}/nocobase-acl/demo/boundary-api.tsx +58 -0
  25. package/{registry → src/extensions}/nocobase-acl/demo/index.tsx +243 -105
  26. package/{registry → src/extensions}/nocobase-acl/demo/prompt-generator.tsx +3 -1
  27. package/src/extensions/nocobase-acl/extension.tsx +73 -0
  28. package/{registry → src/extensions}/nocobase-ai/README.md +21 -5
  29. package/{registry → src/extensions}/nocobase-ai/components/chat/chat-composer.tsx +3 -1
  30. package/{registry → src/extensions}/nocobase-ai/components/chat/chat-messages.tsx +38 -3
  31. package/{registry → src/extensions}/nocobase-ai/components/chat/tool-call-card.tsx +1 -9
  32. package/{registry → src/extensions}/nocobase-ai/components/index.ts +5 -0
  33. package/src/extensions/nocobase-ai/components/surfaces/chat-dialog.tsx +17 -0
  34. package/src/extensions/nocobase-ai/components/surfaces/chat-side-panel.tsx +35 -0
  35. package/src/extensions/nocobase-ai/components/surfaces/chat-surface.tsx +108 -0
  36. package/{registry → src/extensions}/nocobase-ai/components/triggers/ai-chat-floating-trigger.tsx +2 -2
  37. package/{registry → src/extensions}/nocobase-ai/demo/configuration-gate.tsx +4 -1
  38. package/{registry → src/extensions}/nocobase-ai/demo/container-showcase.tsx +2 -2
  39. package/{registry → src/extensions}/nocobase-ai/demo/floating.tsx +45 -15
  40. package/{registry → src/extensions}/nocobase-ai/demo/index.tsx +116 -74
  41. package/{registry → src/extensions}/nocobase-ai/demo/prompt-generator.tsx +3 -3
  42. package/{registry → src/extensions}/nocobase-ai/demo/tool-cards.tsx +2 -3
  43. package/src/extensions/nocobase-ai/extension.tsx +160 -0
  44. package/{registry → src/extensions}/nocobase-ai/global-ai-chat.tsx +12 -28
  45. package/{registry → src/extensions}/nocobase-ai/providers/ai-provider.tsx +14 -1
  46. package/{registry → src/extensions}/nocobase-ai/providers/chat-context.tsx +1 -0
  47. package/{registry → src/extensions}/nocobase-ai/providers/chat-provider.tsx +29 -8
  48. package/{registry → src/extensions}/nocobase-ai/providers/use-chat-attachments.ts +6 -1
  49. package/{registry → src/extensions}/nocobase-ai/providers/use-chat-work-context.ts +4 -1
  50. package/{registry → src/extensions}/nocobase-route-surfaces/README.md +2 -1
  51. package/{registry → src/extensions}/nocobase-route-surfaces/demo/index.tsx +33 -7
  52. package/{registry → src/extensions}/nocobase-route-surfaces/demo/scenarios.ts +20 -14
  53. package/{registry → src/extensions}/nocobase-route-surfaces/extension.tsx +55 -52
  54. package/src/{pages/users → extensions/nocobase-users-example}/create.tsx +4 -4
  55. package/src/{pages/users → extensions/nocobase-users-example}/edit.tsx +5 -9
  56. package/src/extensions/nocobase-users-example/extension.tsx +109 -0
  57. package/src/{pages/users → extensions/nocobase-users-example}/form-context.ts +11 -9
  58. package/src/extensions/nocobase-users-example/layout.tsx +16 -0
  59. package/src/{pages/users → extensions/nocobase-users-example}/list.tsx +1 -1
  60. package/src/extensions/nocobase-users-example/locales/en-US.ts +77 -0
  61. package/src/extensions/nocobase-users-example/locales/index.ts +8 -0
  62. package/src/extensions/nocobase-users-example/locales/zh-CN.ts +73 -0
  63. package/src/extensions/nocobase-users-example/optional-ai.ts +67 -0
  64. package/src/{pages/users → extensions/nocobase-users-example}/role-detail.tsx +4 -15
  65. package/src/{pages/users → extensions/nocobase-users-example}/routes.ts +1 -4
  66. package/src/extensions/nocobase-users-example/show.tsx +266 -0
  67. package/src/lib/nocobase/acl/context.ts +32 -0
  68. package/src/lib/nocobase/acl/evaluator.ts +186 -0
  69. package/src/lib/nocobase/acl/hooks.ts +40 -0
  70. package/src/lib/nocobase/acl/index.ts +4 -1
  71. package/src/lib/nocobase/acl/menu.ts +6 -6
  72. package/src/lib/nocobase/acl/record-permissions.ts +37 -7
  73. package/src/lib/nocobase/acl/store-provider.tsx +14 -0
  74. package/src/lib/nocobase/acl/store.ts +92 -86
  75. package/src/lib/nocobase/acl/types.ts +52 -32
  76. package/src/lib/nocobase/auth-session.ts +128 -0
  77. package/src/lib/nocobase/client.ts +48 -50
  78. package/src/locales/en-US.ts +0 -78
  79. package/src/locales/index.ts +2 -7
  80. package/src/locales/zh-CN.ts +0 -74
  81. package/src/providers/access-control.ts +43 -8
  82. package/src/providers/auth-callback.ts +1 -0
  83. package/src/providers/auth.ts +24 -26
  84. package/src/providers/constants.ts +0 -4
  85. package/src/providers/data.ts +14 -26
  86. package/src/providers/runtime-config.ts +40 -0
  87. package/src/vite-env.d.ts +9 -0
  88. package/tests/auth-session-regression.mjs +101 -0
  89. package/tsconfig.json +2 -1
  90. package/vite.config.ts +9 -1
  91. package/dist/assets/CoordinateSystem-C8P-Cleg.js +0 -1
  92. package/dist/assets/LegendVisualProvider-Bdgkckq-.js +0 -1
  93. package/dist/assets/LineDraw-C0m1GONT.js +0 -1
  94. package/dist/assets/SymbolDraw-CCospe5G.js +0 -1
  95. package/dist/assets/VisualMapping-Cbv1v7Cd.js +0 -1
  96. package/dist/assets/ai-employee-shortcut-B253WWow.js +0 -1
  97. package/dist/assets/calendar-B3dXXyIv.js +0 -1
  98. package/dist/assets/chat-inline-xpQ9iq4p.js +0 -1
  99. package/dist/assets/chat-side-panel-layout-BVWsp5rr.js +0 -1
  100. package/dist/assets/components-ChhJaNRc.js +0 -20
  101. package/dist/assets/configuration-gate-Dwdq_lxf.js +0 -1
  102. package/dist/assets/createClipPathFromCoordSys-uEftn1Mq.js +0 -1
  103. package/dist/assets/createSeriesDataSimply-CI7l7Ufv.js +0 -1
  104. package/dist/assets/customGraphicKeyframeAnimation-DE3jSfMd.js +0 -3
  105. package/dist/assets/echarts-preview-C-cNUtJF.js +0 -1
  106. package/dist/assets/echarts-runtime-CEgJbv68.js +0 -58
  107. package/dist/assets/echarts-runtime-advanced-Buhm2BHz.js +0 -1
  108. package/dist/assets/echarts-runtime-common-DLAdBAIa.js +0 -1
  109. package/dist/assets/echarts-runtime-components-ypnyLkYx.js +0 -1
  110. package/dist/assets/echarts-runtime-hierarchy-ClStquCF.js +0 -1
  111. package/dist/assets/floating-BarTmn-b.js +0 -13
  112. package/dist/assets/index-BJIcYTdc.js +0 -53
  113. package/dist/assets/index-C6xNKxpg.js +0 -656
  114. package/dist/assets/index-D4JQi1TM.js +0 -54
  115. package/dist/assets/index-YA7-AUO8.css +0 -1
  116. package/dist/assets/index-g8zWVpP9.js +0 -23
  117. package/dist/assets/labelHelper-C137W5gU.js +0 -1
  118. package/dist/assets/nocobase-ai-chat-VTMaZFQB.svg +0 -50
  119. package/dist/assets/page-context-Cxx11gzg.js +0 -73
  120. package/dist/assets/prompt-card-BsYMW_rq.js +0 -1
  121. package/dist/assets/sectorHelper-Fqgz3uEk.js +0 -1
  122. package/dist/assets/shortcut-xtghDP63.js +0 -47
  123. package/dist/assets/static.browser-CfQQzERe.js +0 -30
  124. package/dist/assets/switch-DrxImI8F.js +0 -1
  125. package/dist/assets/tool-cards-BGfsY8JF.js +0 -41
  126. package/dist/favicon.ico +0 -0
  127. package/dist/index.html +0 -40
  128. package/dist/index.raw.html +0 -46
  129. package/dist/logo-dark.png +0 -0
  130. package/dist/logo-mark-dark.png +0 -0
  131. package/dist/logo-mark.png +0 -0
  132. package/dist/logo.png +0 -0
  133. package/registry/nocobase-acl/README.md +0 -19
  134. package/registry/nocobase-acl/extension.tsx +0 -71
  135. package/registry/nocobase-acl/tests/acl-regression.mjs +0 -314
  136. package/registry/nocobase-ai/components/surfaces/chat-dialog.tsx +0 -31
  137. package/registry/nocobase-ai/components/surfaces/chat-side-panel.tsx +0 -74
  138. package/registry/nocobase-ai/extension.tsx +0 -158
  139. package/registry/nocobase-ai/tests/business-report-regression.mjs +0 -70
  140. package/registry/nocobase-ai/tests/form-filler-regression.mjs +0 -129
  141. package/registry/nocobase-ai/tests/frontend-tool-regression.mjs +0 -128
  142. package/registry/nocobase-ai/tests/page-context-regression.mjs +0 -48
  143. package/registry/nocobase-ai/tests/stream-regression.mjs +0 -207
  144. package/registry/nocobase-auth-cas/README.md +0 -6
  145. package/registry/nocobase-auth-cas/cas-sign-in-button.tsx +0 -41
  146. package/registry/nocobase-auth-cas/demo.tsx +0 -20
  147. package/registry/nocobase-auth-cas/extension.tsx +0 -46
  148. package/registry/nocobase-auth-cas/index.ts +0 -1
  149. package/registry/nocobase-auth-cas/use-cas-sign-in.ts +0 -29
  150. package/registry/nocobase-auth-dingtalk/README.md +0 -7
  151. package/registry/nocobase-auth-dingtalk/auto-login-provider.tsx +0 -154
  152. package/registry/nocobase-auth-dingtalk/demo.tsx +0 -23
  153. package/registry/nocobase-auth-dingtalk/dingtalk-sign-in-button.tsx +0 -45
  154. package/registry/nocobase-auth-dingtalk/extension.tsx +0 -55
  155. package/registry/nocobase-auth-dingtalk/index.ts +0 -4
  156. package/registry/nocobase-auth-dingtalk/use-dingtalk-sign-in.ts +0 -64
  157. package/registry/nocobase-auth-ldap/README.md +0 -6
  158. package/registry/nocobase-auth-ldap/demo.tsx +0 -21
  159. package/registry/nocobase-auth-ldap/extension.tsx +0 -46
  160. package/registry/nocobase-auth-ldap/index.ts +0 -1
  161. package/registry/nocobase-auth-ldap/ldap-sign-in-form.tsx +0 -69
  162. package/registry/nocobase-auth-ldap/use-ldap-sign-in.ts +0 -5
  163. package/registry/nocobase-auth-oidc/README.md +0 -11
  164. package/registry/nocobase-auth-oidc/auto-redirect-provider.tsx +0 -13
  165. package/registry/nocobase-auth-oidc/demo.tsx +0 -20
  166. package/registry/nocobase-auth-oidc/extension.tsx +0 -51
  167. package/registry/nocobase-auth-oidc/index.ts +0 -1
  168. package/registry/nocobase-auth-oidc/oidc-sign-in-button.tsx +0 -44
  169. package/registry/nocobase-auth-oidc/use-oidc-sign-in.ts +0 -58
  170. package/registry/nocobase-auth-saml/README.md +0 -6
  171. package/registry/nocobase-auth-saml/auto-redirect-provider.tsx +0 -13
  172. package/registry/nocobase-auth-saml/demo.tsx +0 -20
  173. package/registry/nocobase-auth-saml/extension.tsx +0 -51
  174. package/registry/nocobase-auth-saml/index.ts +0 -1
  175. package/registry/nocobase-auth-saml/saml-sign-in-button.tsx +0 -43
  176. package/registry/nocobase-auth-saml/use-saml-sign-in.ts +0 -51
  177. package/registry/nocobase-auth-sms/README.md +0 -7
  178. package/registry/nocobase-auth-sms/demo.tsx +0 -25
  179. package/registry/nocobase-auth-sms/extension.tsx +0 -46
  180. package/registry/nocobase-auth-sms/index.ts +0 -1
  181. package/registry/nocobase-auth-sms/sms-sign-in-form.tsx +0 -107
  182. package/registry/nocobase-auth-sms/use-sms-sign-in.ts +0 -88
  183. package/registry/nocobase-auth-wecom/README.md +0 -6
  184. package/registry/nocobase-auth-wecom/auto-login-provider.tsx +0 -21
  185. package/registry/nocobase-auth-wecom/demo.tsx +0 -23
  186. package/registry/nocobase-auth-wecom/extension.tsx +0 -49
  187. package/registry/nocobase-auth-wecom/index.ts +0 -1
  188. package/registry/nocobase-auth-wecom/use-wecom-sign-in.ts +0 -62
  189. package/registry/nocobase-auth-wecom/wecom-sign-in-button.tsx +0 -70
  190. package/registry/nocobase-i18n/README.md +0 -44
  191. package/registry/nocobase-i18n/components/index.ts +0 -1
  192. package/registry/nocobase-i18n/components/language-switcher.tsx +0 -138
  193. package/registry/nocobase-i18n/demo/index.tsx +0 -209
  194. package/registry/nocobase-i18n/demo/prompt-generator.tsx +0 -132
  195. package/registry/nocobase-i18n/extension.tsx +0 -45
  196. package/registry/nocobase-i18n/index.ts +0 -7
  197. package/registry/nocobase-i18n/locales/en-US.ts +0 -39
  198. package/registry/nocobase-i18n/locales/index.ts +0 -8
  199. package/registry/nocobase-i18n/locales/zh-CN.ts +0 -35
  200. package/registry/nocobase-i18n/provider.tsx +0 -44
  201. package/registry/nocobase-i18n/server-resources.ts +0 -166
  202. package/registry/nocobase-i18n/tests/i18n-regression.mjs +0 -51
  203. package/registry.config.json +0 -302
  204. package/scripts/registry.mjs +0 -164
  205. package/src/components/access-control/resource-access-guard.tsx +0 -21
  206. package/src/components/access-control/use-acl-runtime.ts +0 -28
  207. package/src/lib/nocobase/acl/action.ts +0 -147
  208. package/src/pages/users/layout.tsx +0 -15
  209. package/src/pages/users/show.tsx +0 -278
  210. /package/{registry → src/extensions}/nocobase-acl/components/index.ts +0 -0
  211. /package/{registry → src/extensions}/nocobase-acl/components/role-options.ts +0 -0
  212. /package/{registry → src/extensions}/nocobase-acl/demo/components.tsx +0 -0
  213. /package/{registry → src/extensions}/nocobase-acl/demo/lazy-route.tsx +0 -0
  214. /package/{registry → src/extensions}/nocobase-acl/demo/role-switcher-prompt-generator.tsx +0 -0
  215. /package/{registry → src/extensions}/nocobase-acl/demo/scenario-section.tsx +0 -0
  216. /package/{registry → src/extensions}/nocobase-acl/index.ts +0 -0
  217. /package/{registry → src/extensions}/nocobase-ai/adapters/react-hook-form.ts +0 -0
  218. /package/{registry → src/extensions}/nocobase-ai/assets/nocobase-ai-chat.svg +0 -0
  219. /package/{registry → src/extensions}/nocobase-ai/components/ai-root-provider.tsx +0 -0
  220. /package/{registry → src/extensions}/nocobase-ai/components/chat/ai-employee-avatar.tsx +0 -0
  221. /package/{registry → src/extensions}/nocobase-ai/components/chat/chat-attachment.tsx +0 -0
  222. /package/{registry → src/extensions}/nocobase-ai/components/chat/chat-compact.tsx +0 -0
  223. /package/{registry → src/extensions}/nocobase-ai/components/chat/chat-empty-state.tsx +0 -0
  224. /package/{registry → src/extensions}/nocobase-ai/components/chat/chat-header.tsx +0 -0
  225. /package/{registry → src/extensions}/nocobase-ai/components/chat/chat-history-dialog.tsx +0 -0
  226. /package/{registry → src/extensions}/nocobase-ai/components/chat/chat-message.tsx +0 -0
  227. /package/{registry → src/extensions}/nocobase-ai/components/chat/chat-window.tsx +0 -0
  228. /package/{registry → src/extensions}/nocobase-ai/components/chat/conversation-list.tsx +0 -0
  229. /package/{registry → src/extensions}/nocobase-ai/components/chat/markdown-message.tsx +0 -0
  230. /package/{registry → src/extensions}/nocobase-ai/components/chat/model-select-options.tsx +0 -0
  231. /package/{registry → src/extensions}/nocobase-ai/components/chat/reasoning-panel.tsx +0 -0
  232. /package/{registry → src/extensions}/nocobase-ai/components/chat/sub-agent-conversation.tsx +0 -0
  233. /package/{registry → src/extensions}/nocobase-ai/components/chat/user-prompt-editor.tsx +0 -0
  234. /package/{registry → src/extensions}/nocobase-ai/components/chat/work-context-chip.tsx +0 -0
  235. /package/{registry → src/extensions}/nocobase-ai/components/page-elements/ai-form.tsx +0 -0
  236. /package/{registry → src/extensions}/nocobase-ai/components/page-elements/page-element-provider.tsx +0 -0
  237. /package/{registry → src/extensions}/nocobase-ai/components/surfaces/chat-inline.tsx +0 -0
  238. /package/{registry → src/extensions}/nocobase-ai/components/surfaces/chat-page.tsx +0 -0
  239. /package/{registry → src/extensions}/nocobase-ai/components/surfaces/chat-side-panel-layout.tsx +0 -0
  240. /package/{registry → src/extensions}/nocobase-ai/components/surfaces/chat-surface-actions.tsx +0 -0
  241. /package/{registry → src/extensions}/nocobase-ai/components/tools/builtin-tool-renderers.tsx +0 -0
  242. /package/{registry → src/extensions}/nocobase-ai/components/tools/business-report-dialog.tsx +0 -0
  243. /package/{registry → src/extensions}/nocobase-ai/components/tools/business-report-renderer.tsx +0 -0
  244. /package/{registry → src/extensions}/nocobase-ai/components/tools/business-report-utils.tsx +0 -0
  245. /package/{registry → src/extensions}/nocobase-ai/components/tools/chart-renderer.tsx +0 -0
  246. /package/{registry → src/extensions}/nocobase-ai/components/tools/echarts-preview.tsx +0 -0
  247. /package/{registry → src/extensions}/nocobase-ai/components/tools/echarts-runtime-advanced.ts +0 -0
  248. /package/{registry → src/extensions}/nocobase-ai/components/tools/echarts-runtime-common.ts +0 -0
  249. /package/{registry → src/extensions}/nocobase-ai/components/tools/echarts-runtime-components.ts +0 -0
  250. /package/{registry → src/extensions}/nocobase-ai/components/tools/echarts-runtime-hierarchy.ts +0 -0
  251. /package/{registry → src/extensions}/nocobase-ai/components/tools/echarts-runtime.ts +0 -0
  252. /package/{registry → src/extensions}/nocobase-ai/components/tools/sub-agent-renderer.tsx +0 -0
  253. /package/{registry → src/extensions}/nocobase-ai/components/tools/suggestions-renderer.tsx +0 -0
  254. /package/{registry → src/extensions}/nocobase-ai/components/tools/tool-renderer-provider.tsx +0 -0
  255. /package/{registry → src/extensions}/nocobase-ai/components/tools/tool-renderer-utils.ts +0 -0
  256. /package/{registry → src/extensions}/nocobase-ai/components/tools/workflow-renderer.tsx +0 -0
  257. /package/{registry → src/extensions}/nocobase-ai/components/triggers/ai-employee-shortcut.tsx +0 -0
  258. /package/{registry → src/extensions}/nocobase-ai/demo/interaction-showcase.tsx +0 -0
  259. /package/{registry → src/extensions}/nocobase-ai/demo/page-context-prompt-generator.tsx +0 -0
  260. /package/{registry → src/extensions}/nocobase-ai/demo/page-context.tsx +0 -0
  261. /package/{registry → src/extensions}/nocobase-ai/demo/page-element-showcase.tsx +0 -0
  262. /package/{registry → src/extensions}/nocobase-ai/demo/prompt-card.tsx +0 -0
  263. /package/{registry → src/extensions}/nocobase-ai/demo/shortcut.tsx +0 -0
  264. /package/{registry → src/extensions}/nocobase-ai/index.ts +0 -0
  265. /package/{registry → src/extensions}/nocobase-ai/providers/avatars.ts +0 -0
  266. /package/{registry → src/extensions}/nocobase-ai/providers/chat-controller.ts +0 -0
  267. /package/{registry → src/extensions}/nocobase-ai/providers/chat-message-utils.ts +0 -0
  268. /package/{registry → src/extensions}/nocobase-ai/providers/chat-reducer.ts +0 -0
  269. /package/{registry → src/extensions}/nocobase-ai/providers/chat-task-utils.ts +0 -0
  270. /package/{registry → src/extensions}/nocobase-ai/providers/chat-transport.ts +0 -0
  271. /package/{registry → src/extensions}/nocobase-ai/providers/form-registry.tsx +0 -0
  272. /package/{registry → src/extensions}/nocobase-ai/providers/frontend-tool-registry.tsx +0 -0
  273. /package/{registry → src/extensions}/nocobase-ai/providers/index.ts +0 -0
  274. /package/{registry → src/extensions}/nocobase-ai/providers/model.ts +0 -0
  275. /package/{registry → src/extensions}/nocobase-ai/providers/page-context.tsx +0 -0
  276. /package/{registry → src/extensions}/nocobase-ai/providers/stream-coalescer.ts +0 -0
  277. /package/{registry → src/extensions}/nocobase-ai/providers/stream-event-utils.ts +0 -0
  278. /package/{registry → src/extensions}/nocobase-ai/providers/stream-parser.ts +0 -0
  279. /package/{registry → src/extensions}/nocobase-ai/providers/sub-agent-stream.ts +0 -0
  280. /package/{registry → src/extensions}/nocobase-ai/providers/types.ts +0 -0
  281. /package/{registry → src/extensions}/nocobase-ai/providers/ui-message-stream.ts +0 -0
  282. /package/{registry → src/extensions}/nocobase-ai/providers/use-automatic-tool-approval.ts +0 -0
  283. /package/{registry → src/extensions}/nocobase-ai/providers/use-chat-message-actions.ts +0 -0
  284. /package/{registry → src/extensions}/nocobase-ai/providers/use-chat-runtime.ts +0 -0
  285. /package/{registry → src/extensions}/nocobase-ai/providers/use-conversation-catalog.ts +0 -0
  286. /package/{registry → src/extensions}/nocobase-ai/providers/use-conversation-history.ts +0 -0
  287. /package/{registry → src/extensions}/nocobase-ai/services/index.ts +0 -0
  288. /package/{registry → src/extensions}/nocobase-ai/services/nocobase-ai-service.ts +0 -0
  289. /package/{registry → src/extensions}/nocobase-ai/services/types.ts +0 -0
  290. /package/{registry → src/extensions}/nocobase-route-surfaces/demo/lazy-route.tsx +0 -0
  291. /package/{registry → src/extensions}/nocobase-route-surfaces/demo/prompt-generator.tsx +0 -0
  292. /package/{registry → src/extensions}/nocobase-route-surfaces/index.ts +0 -0
  293. /package/{registry → src/extensions}/nocobase-route-surfaces/route-dialog.tsx +0 -0
  294. /package/{registry → src/extensions}/nocobase-route-surfaces/route-drawer.tsx +0 -0
  295. /package/{registry → src/extensions}/nocobase-route-surfaces/route-page.tsx +0 -0
  296. /package/{registry → src/extensions}/nocobase-route-surfaces/route-surface-context.ts +0 -0
  297. /package/{registry → src/extensions}/nocobase-route-surfaces/use-refine-unsaved-changes.tsx +0 -0
  298. /package/{registry → src/extensions}/nocobase-route-surfaces/use-route-surface-close.ts +0 -0
  299. /package/{registry → src/extensions}/nocobase-route-surfaces/use-route-surface-state.ts +0 -0
  300. /package/src/{pages/users → extensions/nocobase-users-example}/form-fields.tsx +0 -0
  301. /package/src/{pages/users → extensions/nocobase-users-example}/index.ts +0 -0
  302. /package/src/{pages/users → extensions/nocobase-users-example}/role-badges.tsx +0 -0
  303. /package/src/{pages/users → extensions/nocobase-users-example}/role-utils.ts +0 -0
  304. /package/src/{pages/users → extensions/nocobase-users-example}/types.ts +0 -0
@@ -1,8 +1,7 @@
1
1
  import {
2
2
  AIChatWindow,
3
- ChatDialog,
3
+ ChatSurface,
4
4
  ChatSurfaceActions,
5
- ChatSidePanelLayout,
6
5
  useAIPageElementPicker,
7
6
  type AIChatComposerAction,
8
7
  type AIChatWindowProps,
@@ -19,7 +18,7 @@ import {
19
18
  } from "@/components/ui/table";
20
19
  import { AIChatProvider, useAIChatBase } from "../providers";
21
20
  import { Globe2, MousePointer2 } from "lucide-react";
22
- import { useMemo, useState } from "react";
21
+ import { useMemo, useState, type CSSProperties } from "react";
23
22
  import { ContainerShowcase, type ChatContainer } from "./container-showcase";
24
23
  import { InteractionShowcase } from "./interaction-showcase";
25
24
  import { AIConfigurationGate } from "./configuration-gate";
@@ -106,6 +105,46 @@ const propRows = [
106
105
  ],
107
106
  ];
108
107
 
108
+ const surfacePropRows = [
109
+ [
110
+ "variant",
111
+ '"side-panel" | "dialog"',
112
+ "required",
113
+ "Changes only the outer presentation while keeping the same chat window mounted.",
114
+ ],
115
+ ["open", "boolean", "required", "Controls whether the surface is open."],
116
+ [
117
+ "onOpenChange",
118
+ "(open: boolean) => void",
119
+ "required",
120
+ "Receives close requests from Escape, the dialog backdrop, or surface actions.",
121
+ ],
122
+ [
123
+ "side",
124
+ '"left" | "right"',
125
+ '"right"',
126
+ "Chooses the side used by the side-panel variant.",
127
+ ],
128
+ [
129
+ "width",
130
+ "number | string",
131
+ "450",
132
+ "Sets the width used by the side-panel variant.",
133
+ ],
134
+ [
135
+ "closeOnEscape",
136
+ "boolean",
137
+ "true",
138
+ "Allows Escape to close the active surface.",
139
+ ],
140
+ [
141
+ "showCloseHandle",
142
+ "boolean",
143
+ "false",
144
+ "Shows an outside close handle for the side-panel variant.",
145
+ ],
146
+ ];
147
+
109
148
  export function AIChatPage() {
110
149
  return (
111
150
  <AIConfigurationGate>
@@ -118,8 +157,7 @@ export function AIChatPage() {
118
157
 
119
158
  function AIChatPageContent() {
120
159
  const [container, setContainer] = useState<ChatContainer>("embedded");
121
- const [sidePanelOpen, setSidePanelOpen] = useState(false);
122
- const [dialogOpen, setDialogOpen] = useState(false);
160
+ const [surfaceOpen, setSurfaceOpen] = useState(false);
123
161
  const [webSearch, setWebSearch] = useState(false);
124
162
  const { id: chatId, addWorkContext, focusComposer } = useAIChatBase();
125
163
  const { registeredCount, startPicking } = useAIPageElementPicker();
@@ -132,9 +170,7 @@ function AIChatPageContent() {
132
170
  icon: <MousePointer2 />,
133
171
  disabled: registeredCount === 0,
134
172
  onClick: () => {
135
- if (dialogOpen) {
136
- setDialogOpen(false);
137
- setSidePanelOpen(true);
173
+ if (surfaceOpen && container === "dialog") {
138
174
  setContainer("side-panel");
139
175
  }
140
176
  startPicking({
@@ -159,10 +195,11 @@ function AIChatPageContent() {
159
195
  [
160
196
  addWorkContext,
161
197
  chatId,
162
- dialogOpen,
198
+ container,
163
199
  focusComposer,
164
200
  registeredCount,
165
201
  startPicking,
202
+ surfaceOpen,
166
203
  webSearch,
167
204
  ]
168
205
  );
@@ -178,35 +215,16 @@ function AIChatPageContent() {
178
215
  );
179
216
 
180
217
  const closeSurface = () => {
181
- setSidePanelOpen(false);
182
- setDialogOpen(false);
218
+ setSurfaceOpen(false);
183
219
  };
184
220
 
185
- const sidePanelWindowProps: AIChatWindowProps = {
221
+ const surfaceWindowProps: AIChatWindowProps = {
186
222
  ...windowProps,
187
223
  headerActions: (
188
224
  <ChatSurfaceActions
189
- expanded={false}
225
+ expanded={container === "dialog"}
190
226
  onExpandedChange={(expanded) => {
191
- if (!expanded) return;
192
- setSidePanelOpen(false);
193
- setDialogOpen(true);
194
- }}
195
- onClose={closeSurface}
196
- />
197
- ),
198
- };
199
-
200
- const dialogWindowProps: AIChatWindowProps = {
201
- ...windowProps,
202
- headerActions: (
203
- <ChatSurfaceActions
204
- expanded={true}
205
- onExpandedChange={(nextExpanded) => {
206
- if (nextExpanded) return;
207
- setDialogOpen(false);
208
- setSidePanelOpen(true);
209
- setContainer("side-panel");
227
+ setContainer(expanded ? "dialog" : "side-panel");
210
228
  }}
211
229
  onClose={closeSurface}
212
230
  />
@@ -215,17 +233,21 @@ function AIChatPageContent() {
215
233
 
216
234
  const selectContainer = (nextContainer: ChatContainer) => {
217
235
  setContainer(nextContainer);
218
- setSidePanelOpen(nextContainer === "side-panel");
219
- setDialogOpen(nextContainer === "dialog");
236
+ setSurfaceOpen(
237
+ nextContainer === "side-panel" || nextContainer === "dialog"
238
+ );
220
239
  };
221
240
 
222
241
  return (
223
- <ChatSidePanelLayout
224
- open={sidePanelOpen}
225
- onOpenChange={setSidePanelOpen}
226
- width={450}
227
- panel={<AIChatWindow {...sidePanelWindowProps} />}
228
- showCloseHandle={false}
242
+ <div
243
+ data-open={surfaceOpen && container === "side-panel"}
244
+ data-side="right"
245
+ className="chat-side-panel-layout @container min-w-0"
246
+ style={
247
+ {
248
+ "--chat-side-panel-width": "450px",
249
+ } as CSSProperties
250
+ }
229
251
  >
230
252
  <div className="space-y-14 pb-12">
231
253
  <section className="flex flex-wrap items-start justify-between gap-5 border-b pb-8">
@@ -277,49 +299,69 @@ function AIChatPageContent() {
277
299
  <PromptGenerator />
278
300
  </section>
279
301
 
302
+ <section className="space-y-5">
303
+ <SectionTitle
304
+ eyebrow="Component API"
305
+ title="ChatSurface props"
306
+ description="Use variant as the single presentation switch. The child AIChatWindow remains the same React instance while the surface changes shape."
307
+ />
308
+ <PropsTable rows={surfacePropRows} />
309
+ </section>
310
+
280
311
  <section className="space-y-5">
281
312
  <SectionTitle
282
313
  eyebrow="Component API"
283
314
  title="AIChatWindow props"
284
315
  description="The core window stays reusable while business pages provide placement, composer actions, and tool-approval behavior."
285
316
  />
286
- <Card className="gap-0 overflow-hidden py-0">
287
- <Table>
288
- <TableHeader>
289
- <TableRow>
290
- <TableHead>Prop</TableHead>
291
- <TableHead>Type</TableHead>
292
- <TableHead>Default</TableHead>
293
- <TableHead>Description</TableHead>
294
- </TableRow>
295
- </TableHeader>
296
- <TableBody>
297
- {propRows.map(([name, type, defaultValue, description]) => (
298
- <TableRow key={name}>
299
- <TableCell className="font-mono text-xs font-medium">
300
- {name}
301
- </TableCell>
302
- <TableCell className="font-mono text-xs text-muted-foreground">
303
- {type}
304
- </TableCell>
305
- <TableCell className="font-mono text-xs text-muted-foreground">
306
- {defaultValue}
307
- </TableCell>
308
- <TableCell className="min-w-80 whitespace-normal text-muted-foreground">
309
- {description}
310
- </TableCell>
311
- </TableRow>
312
- ))}
313
- </TableBody>
314
- </Table>
315
- </Card>
317
+ <PropsTable rows={propRows} />
316
318
  </section>
317
319
 
318
- <ChatDialog open={dialogOpen} onOpenChange={setDialogOpen}>
319
- <AIChatWindow {...dialogWindowProps} />
320
- </ChatDialog>
321
320
  </div>
322
- </ChatSidePanelLayout>
321
+ <ChatSurface
322
+ open={surfaceOpen}
323
+ variant={container === "dialog" ? "dialog" : "side-panel"}
324
+ onOpenChange={setSurfaceOpen}
325
+ width={450}
326
+ >
327
+ <AIChatWindow {...surfaceWindowProps} />
328
+ </ChatSurface>
329
+ </div>
330
+ );
331
+ }
332
+
333
+ function PropsTable({ rows }: { rows: string[][] }) {
334
+ return (
335
+ <Card className="gap-0 overflow-hidden py-0">
336
+ <Table>
337
+ <TableHeader>
338
+ <TableRow>
339
+ <TableHead>Prop</TableHead>
340
+ <TableHead>Type</TableHead>
341
+ <TableHead>Default</TableHead>
342
+ <TableHead>Description</TableHead>
343
+ </TableRow>
344
+ </TableHeader>
345
+ <TableBody>
346
+ {rows.map(([name, type, defaultValue, description]) => (
347
+ <TableRow key={name}>
348
+ <TableCell className="font-mono text-xs font-medium">
349
+ {name}
350
+ </TableCell>
351
+ <TableCell className="font-mono text-xs text-muted-foreground">
352
+ {type}
353
+ </TableCell>
354
+ <TableCell className="font-mono text-xs text-muted-foreground">
355
+ {defaultValue}
356
+ </TableCell>
357
+ <TableCell className="min-w-80 whitespace-normal text-muted-foreground">
358
+ {description}
359
+ </TableCell>
360
+ </TableRow>
361
+ ))}
362
+ </TableBody>
363
+ </Table>
364
+ </Card>
323
365
  );
324
366
  }
325
367
 
@@ -141,9 +141,9 @@ function PromptGeneratorContent() {
141
141
  embedded:
142
142
  "Render ChatInline inside the target content region and keep its width fluid within the parent container.",
143
143
  page: "Create a dedicated route and render ChatPage with AIChatWindow filling the available page content.",
144
- "side-panel": `Use ChatSidePanelLayout on the right with a ${panelWidth}px panel. Opening chat must push the page narrower instead of covering it on desktop; mobile may use an overlay.`,
144
+ "side-panel": `Use ChatSurface with variant="side-panel" on the right and a ${panelWidth}px width. Opening chat must push the page narrower instead of covering it on desktop; mobile may use an overlay.`,
145
145
  dialog:
146
- "Open ChatDialog from a page action. Keep conversation state in AIProvider/AIChatProvider so closing the dialog does not discard it.",
146
+ "Open ChatSurface with variant=\"dialog\" from a page action. Keep conversation state in AIProvider/AIChatProvider so closing the dialog does not discard it.",
147
147
  mobile:
148
148
  "Render ChatInline in the target region with a mobile-first width around 390px and preserve the same responsive AIChatWindow API.",
149
149
  }[placement];
@@ -181,7 +181,7 @@ Implementation requirements:
181
181
  - Wrap the application content with AIPageElementProvider when page-element picking is enabled.
182
182
  - Register selectable React components with useAIPageElement and return serializable business context from getContext.
183
183
  - Reuse AIChatWindow and the existing providers; do not duplicate message, streaming, reasoning, or tool-call state.
184
- - When the right panel is expandable, keep one AIChatProvider mounted while switching between ChatSidePanel and ChatDialog. Put ChatSurfaceActions in the window header.
184
+ - When the right panel is expandable, render one AIChatWindow inside ChatSurface and change only its variant between "side-panel" and "dialog". Put ChatSurfaceActions in the window header.
185
185
  - Keep the generic Tool Call shell responsible for status, ASK approval, errors, and disclosure. Register business-specific bodies through AIToolRendererProvider.
186
186
  - Include the built-in specialized Tool Card renderers. This is part of the standard AI chat capability, not an optional page setting.
187
187
  - Register browser-side tool implementations through AIProvider.toolInvokers. After all interrupted calls are approved, the provider executes these handlers and sends their results when resuming the NocoBase conversation.
@@ -402,9 +402,8 @@ Implementation requirements:
402
402
  - Export any public renderer types or components from registry/nocobase-ai/components/index.ts when application code needs them.
403
403
 
404
404
  Verification:
405
- - Run pnpm registry:preview.
406
- - Run pnpm exec tsc --noEmit.
407
- - Run the scoped ESLint check and pnpm registry:build.
405
+ - Run pnpm build from the Registry source repository.
406
+ - Run the relevant regression checks and a scoped ESLint check.
408
407
  - Verify the card in light and dark themes, narrow chat panels, expanded dialogs, pending approval, completed, and error states.`;
409
408
 
410
409
  return (
@@ -0,0 +1,160 @@
1
+ import { NocoBaseAIExtensionProvider } from "./global-ai-chat";
2
+ import type { AppExtension } from "@/app/extensions";
3
+ import { LoadingState } from "@/components/app-shell/loading-state";
4
+ import {
5
+ Bot,
6
+ MessageSquare,
7
+ MousePointer2,
8
+ PanelRight,
9
+ Sparkles,
10
+ Wrench,
11
+ } from "lucide-react";
12
+ import { lazy, Suspense, type ReactNode } from "react";
13
+ import { Outlet, Route } from "react-router";
14
+
15
+ const AIChatPage = lazy(() =>
16
+ import("./demo").then((module) => ({ default: module.AIChatPage }))
17
+ );
18
+ const FloatingChatPage = lazy(() =>
19
+ import("./demo/floating").then((module) => ({
20
+ default: module.FloatingChatPage,
21
+ }))
22
+ );
23
+ const ShortcutPage = lazy(() =>
24
+ import("./demo/shortcut").then((module) => ({
25
+ default: module.ShortcutPage,
26
+ }))
27
+ );
28
+ const PageContextPage = lazy(() =>
29
+ import("./demo/page-context").then((module) => ({
30
+ default: module.PageContextPage,
31
+ }))
32
+ );
33
+ const ToolCardsPage = lazy(() =>
34
+ import("./demo/tool-cards").then((module) => ({
35
+ default: module.ToolCardsPage,
36
+ }))
37
+ );
38
+
39
+ function LazyDemoRoute({ children }: { children: ReactNode }) {
40
+ return (
41
+ <Suspense fallback={<LoadingState className="min-h-[320px]" />}>
42
+ {children}
43
+ </Suspense>
44
+ );
45
+ }
46
+
47
+ const nocobaseAIExtension: AppExtension = {
48
+ id: "nocobase-ai",
49
+ Provider: NocoBaseAIExtensionProvider,
50
+ dev: {
51
+ resources: [
52
+ {
53
+ name: "ai-components",
54
+ meta: {
55
+ label: "AI Components",
56
+ icon: <Bot />,
57
+ acl: { type: "authenticated" },
58
+ },
59
+ },
60
+ {
61
+ name: "ai-chat-window",
62
+ list: "ai-chat",
63
+ meta: {
64
+ parent: "ai-components",
65
+ label: "Chat window",
66
+ icon: <MessageSquare />,
67
+ description:
68
+ "Build freely with AI while NocoBase keeps the application reliable.",
69
+ acl: { type: "authenticated" },
70
+ },
71
+ },
72
+ {
73
+ name: "ai-floating-chat",
74
+ list: "ai-chat/floating",
75
+ meta: {
76
+ parent: "ai-components",
77
+ label: "Floating chat",
78
+ icon: <PanelRight />,
79
+ acl: { type: "authenticated" },
80
+ },
81
+ },
82
+ {
83
+ name: "ai-employee-tasks",
84
+ list: "ai-chat/shortcut",
85
+ meta: {
86
+ parent: "ai-components",
87
+ label: "Employee tasks",
88
+ icon: <Sparkles />,
89
+ acl: { type: "authenticated" },
90
+ },
91
+ },
92
+ {
93
+ name: "ai-page-context",
94
+ list: "ai-chat/context",
95
+ meta: {
96
+ parent: "ai-components",
97
+ label: "Page context",
98
+ icon: <MousePointer2 />,
99
+ acl: { type: "authenticated" },
100
+ },
101
+ },
102
+ {
103
+ name: "ai-tool-cards",
104
+ list: "ai-chat/tools",
105
+ meta: {
106
+ parent: "ai-components",
107
+ label: "Tool cards",
108
+ icon: <Wrench />,
109
+ acl: { type: "authenticated" },
110
+ },
111
+ },
112
+ ],
113
+ routes: (
114
+ <Route key="nocobase-ai" path="ai-chat" element={<Outlet />}>
115
+ <Route
116
+ index
117
+ element={
118
+ <LazyDemoRoute>
119
+ <AIChatPage />
120
+ </LazyDemoRoute>
121
+ }
122
+ />
123
+ <Route
124
+ path="floating"
125
+ element={
126
+ <LazyDemoRoute>
127
+ <FloatingChatPage />
128
+ </LazyDemoRoute>
129
+ }
130
+ />
131
+ <Route
132
+ path="shortcut"
133
+ element={
134
+ <LazyDemoRoute>
135
+ <ShortcutPage />
136
+ </LazyDemoRoute>
137
+ }
138
+ />
139
+ <Route
140
+ path="context"
141
+ element={
142
+ <LazyDemoRoute>
143
+ <PageContextPage />
144
+ </LazyDemoRoute>
145
+ }
146
+ />
147
+ <Route
148
+ path="tools"
149
+ element={
150
+ <LazyDemoRoute>
151
+ <ToolCardsPage />
152
+ </LazyDemoRoute>
153
+ }
154
+ />
155
+ </Route>
156
+ ),
157
+ },
158
+ };
159
+
160
+ export default nocobaseAIExtension;
@@ -14,8 +14,7 @@ import {
14
14
  type AIChatComposerAction,
15
15
  } from "./components";
16
16
  import { AIChatWindow } from "./components/chat/chat-window";
17
- import { ChatDialog } from "./components/surfaces/chat-dialog";
18
- import { ChatSidePanel } from "./components/surfaces/chat-side-panel";
17
+ import { ChatSurface } from "./components/surfaces/chat-surface";
19
18
  import { ChatSurfaceActions } from "./components/surfaces/chat-surface-actions";
20
19
  import {
21
20
  AIChatProvider,
@@ -51,8 +50,7 @@ function NocoBaseAIGlobalEntry({ children }: PropsWithChildren) {
51
50
  const [expanded, setExpanded] = useState(false);
52
51
  const ready =
53
52
  ai.configurationStatus === "ready" &&
54
- ai.employees.length > 0 &&
55
- ai.hasEnabledModels;
53
+ ai.employees.length > 0;
56
54
 
57
55
  return (
58
56
  <div
@@ -166,38 +164,24 @@ function StarterGlobalAIChat({
166
164
  setExpanded(false);
167
165
  onOpenChange(false);
168
166
  };
167
+ const handleSurfaceOpenChange = (nextOpen: boolean) => {
168
+ if (!nextOpen) setExpanded(false);
169
+ onOpenChange(nextOpen);
170
+ };
169
171
 
170
172
  return (
171
173
  <>
172
- <ChatSidePanel
173
- open={open && !expanded}
174
- onOpenChange={onOpenChange}
174
+ <ChatSurface
175
+ open={open}
176
+ variant={expanded ? "dialog" : "side-panel"}
177
+ onOpenChange={handleSurfaceOpenChange}
175
178
  width={DEFAULT_SIDE_PANEL_WIDTH}
176
179
  showCloseHandle={false}
177
180
  >
178
181
  <AIChatWindow
179
182
  headerActions={
180
183
  <ChatSurfaceActions
181
- expanded={false}
182
- onExpandedChange={setExpanded}
183
- onClose={closeChat}
184
- />
185
- }
186
- composerActions={composerActions}
187
- enableAttachments
188
- attachmentActionIndex={1}
189
- />
190
- </ChatSidePanel>
191
- <ChatDialog
192
- open={open && expanded}
193
- onOpenChange={(nextOpen) => {
194
- if (!nextOpen) closeChat();
195
- }}
196
- >
197
- <AIChatWindow
198
- headerActions={
199
- <ChatSurfaceActions
200
- expanded
184
+ expanded={expanded}
201
185
  onExpandedChange={setExpanded}
202
186
  onClose={closeChat}
203
187
  />
@@ -206,7 +190,7 @@ function StarterGlobalAIChat({
206
190
  enableAttachments
207
191
  attachmentActionIndex={1}
208
192
  />
209
- </ChatDialog>
193
+ </ChatSurface>
210
194
  <AIChatFloatingTrigger unreadCount={unreadCount} />
211
195
  </>
212
196
  );
@@ -125,7 +125,20 @@ function AIProviderRuntime({
125
125
  status: AIConfigurationStatus;
126
126
  error?: Error;
127
127
  modelError?: Error;
128
- }>({ employees: [], models: [], status: "loading" });
128
+ }>(() =>
129
+ providedEmployees && providedModels
130
+ ? {
131
+ employees: providedEmployees,
132
+ models: providedModels.length
133
+ ? providedModels
134
+ : [UNCONFIGURED_MODEL],
135
+ status: "ready",
136
+ modelError: providedModels.length
137
+ ? undefined
138
+ : new Error("No enabled AI models were provided."),
139
+ }
140
+ : { employees: [], models: [], status: "loading" }
141
+ );
129
142
  const internalGlobalController = useAIChatController();
130
143
  const globalController = providedGlobalController ?? internalGlobalController;
131
144
 
@@ -20,6 +20,7 @@ export type AIChatContextValue = {
20
20
  models: AIModel[];
21
21
  currentEmployee: AIEmployee;
22
22
  currentModel: AIModel;
23
+ canSend: boolean;
23
24
  activeConversation?: AIConversation;
24
25
  activeConversationId: string;
25
26
  conversations: AIConversation[];
@@ -52,14 +52,27 @@ import {
52
52
  type AIChatMessage,
53
53
  type AIChatTaskRuntime,
54
54
  type AIConversation,
55
+ type AIEmployee,
55
56
  type AIEmployeeTask,
56
57
  type AIEmployeeTasks,
57
58
  type AIEmployeeTaskTrigger,
59
+ type AIModel,
58
60
  type AIWorkContextItem,
59
61
  } from "./types";
60
62
 
61
63
  const EMPTY_TASKS: AIEmployeeTask[] = [];
62
64
  const EMPTY_EMPLOYEE_TASKS: AIEmployeeTasks = {};
65
+ const UNAVAILABLE_EMPLOYEE: AIEmployee = {
66
+ username: "__unavailable__",
67
+ nickname: "AI employee",
68
+ position: "Not configured",
69
+ greeting: "Configure an AI employee and model to start chatting.",
70
+ };
71
+ const UNAVAILABLE_MODEL: AIModel = {
72
+ value: "__unavailable__",
73
+ label: "No enabled model",
74
+ configured: false,
75
+ };
63
76
 
64
77
  export type AIChatProviderProps = PropsWithChildren<{
65
78
  id: string;
@@ -197,16 +210,19 @@ export function AIChatProvider({
197
210
  dispatch,
198
211
  });
199
212
 
200
- const currentEmployee =
213
+ const configuredEmployee =
201
214
  ai.employees.find(
202
215
  (employee) => employee.username === state.selectedEmployeeUsername
203
216
  ) ?? ai.employees[0];
204
- const currentModel =
217
+ const configuredModel =
205
218
  findAIModel(ai.models, state.selectedModel) ?? ai.models[0];
206
-
207
- if (!currentEmployee || !currentModel) {
208
- throw new Error("AIProvider requires at least one employee and model");
209
- }
219
+ const currentEmployee = configuredEmployee ?? UNAVAILABLE_EMPLOYEE;
220
+ const currentModel = configuredModel ?? UNAVAILABLE_MODEL;
221
+ const canSend = Boolean(
222
+ configuredEmployee &&
223
+ configuredModel &&
224
+ configuredModel.configured !== false
225
+ );
210
226
 
211
227
  const getActiveConversationId = useCallback(
212
228
  () => stateRef.current.activeConversationId,
@@ -271,7 +287,7 @@ export function AIChatProvider({
271
287
  const conversation = currentState.conversations.find(
272
288
  (item) => item.id === currentId
273
289
  );
274
- if (!employee || !model) return;
290
+ if (!employee || !model || model.configured === false) return;
275
291
  const currentAttachments = getConversationAttachments(currentId);
276
292
  const unresolvedWorkContext = getConversationWorkContext(currentId);
277
293
  if (
@@ -712,6 +728,7 @@ export function AIChatProvider({
712
728
  models: ai.models,
713
729
  currentEmployee,
714
730
  currentModel,
731
+ canSend,
715
732
  activeConversation,
716
733
  activeConversationId: state.activeConversationId,
717
734
  conversations: state.conversations,
@@ -797,7 +814,9 @@ export function AIChatProvider({
797
814
  startEditingMessage,
798
815
  cancelEditingMessage,
799
816
  saveUserPrompt: (prompt) =>
800
- ai.updateEmployeeUserPrompt(currentEmployee.username, prompt),
817
+ configuredEmployee
818
+ ? ai.updateEmployeeUserPrompt(configuredEmployee.username, prompt)
819
+ : Promise.resolve(),
801
820
  triggerTask,
802
821
  runTask,
803
822
  focusComposer: requestComposerFocus,
@@ -818,6 +837,8 @@ export function AIChatProvider({
818
837
  editingMessageId,
819
838
  currentEmployee,
820
839
  currentModel,
840
+ canSend,
841
+ configuredEmployee,
821
842
  getConfiguredTaskSet,
822
843
  invalidateConversationHistory,
823
844
  invalidatePendingInteraction,
@@ -179,8 +179,13 @@ export function useChatAttachments(activeConversationId: string) {
179
179
  }),
180
180
  []
181
181
  );
182
+ // Falls back to the shared EMPTY_ATTACHMENTS rather than a fresh `[]`: callers compare the
183
+ // result by identity across an await to detect "the user changed the attachments mid-send", and
184
+ // a new array each call would make that check fire on every send from a conversation with no
185
+ // attachments.
182
186
  const getConversationAttachments = useCallback(
183
- (conversationId: string) => draftsRef.current[conversationId] ?? [],
187
+ (conversationId: string) =>
188
+ draftsRef.current[conversationId] ?? EMPTY_ATTACHMENTS,
184
189
  []
185
190
  );
186
191
  const attachments = drafts[activeConversationId] ?? EMPTY_ATTACHMENTS;