@salsita-ai/sdk 0.50.0-develop.f6390cb584 → 0.50.0-develop.fbe43b9a2d

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 (293) hide show
  1. package/dist/application-spec/errors/diagnostic-codes.d.ts +9 -0
  2. package/dist/application-spec/errors/diagnostic-codes.js +1 -1
  3. package/dist/application-spec/errors/diagnostic-messages.d.ts +10 -1
  4. package/dist/application-spec/errors/diagnostic-messages.js +1 -1
  5. package/dist/application-spec/generate-application/code/domain-runtime-files.js +1 -1
  6. package/dist/application-spec/generate-application/code/templates/application-types.js +6 -6
  7. package/dist/application-spec/generate-application/code/templates/common/doc-comment.d.ts +2 -0
  8. package/dist/application-spec/generate-application/code/templates/common/doc-comment.js +1 -0
  9. package/dist/application-spec/generate-application/code/templates/configurator-globals.d.ts +2 -1
  10. package/dist/application-spec/generate-application/code/templates/configurator-globals.js +19 -6
  11. package/dist/application-spec/generate-application/code/templates/global-definition/generate-action-types.js +7 -7
  12. package/dist/application-spec/generate-application/code/templates/global-definition/generate-assistant-form-element-types.d.ts +12 -0
  13. package/dist/application-spec/generate-application/code/templates/global-definition/generate-assistant-form-element-types.js +7 -0
  14. package/dist/application-spec/generate-application/code/templates/global-definition/generate-phx-types.js +24 -22
  15. package/dist/application-spec/generate-application/code/templates/global-definition/index.d.ts +1 -0
  16. package/dist/application-spec/generate-application/code/templates/global-definition/index.js +1 -1
  17. package/dist/application-spec/generate-application/code/templates/skills/sdk-content.d.ts +2 -1
  18. package/dist/application-spec/generate-application/code/templates/skills/sdk-content.js +2 -2
  19. package/dist/application-spec/parse-application-files/raw-spec/raw-assistant.d.ts +33 -3
  20. package/dist/application-spec/parse-application-files/raw-spec/raw-assistant.js +1 -1
  21. package/dist/application-spec/parse-application-files/spec/sections/assistant/assistant-form-elements.d.ts +9 -0
  22. package/dist/application-spec/parse-application-files/spec/sections/assistant/assistant-form-elements.js +1 -0
  23. package/dist/application-spec/parse-application-files/spec/sections/assistant/assistant-schema.d.ts +4 -1
  24. package/dist/application-spec/parse-application-files/spec/sections/assistant/assistant-schema.js +1 -1
  25. package/dist/configurator/application-runtime/extensions/selectors.js +1 -1
  26. package/dist/configurator/application-runtime/extensions/systems/assistant-system/assistant-system.js +1 -1
  27. package/dist/configurator/application-runtime/extensions/systems/assistant-system/graphql-tools.d.ts +12 -2
  28. package/dist/configurator/application-runtime/extensions/systems/assistant-system/graphql-tools.js +1 -1
  29. package/dist/configurator/application-runtime/extensions/systems/canvas-system.d.ts +8 -0
  30. package/dist/configurator/application-runtime/extensions/systems/canvas-system.js +1 -1
  31. package/dist/configurator/application-runtime/extensions/systems/export-system/export-busy-error.d.ts +8 -0
  32. package/dist/configurator/application-runtime/extensions/systems/export-system/export-busy-error.js +1 -0
  33. package/dist/configurator/application-runtime/extensions/systems/export-system/export-system.d.ts +23 -1
  34. package/dist/configurator/application-runtime/extensions/systems/export-system/export-system.js +1 -1
  35. package/dist/configurator/application-runtime/extensions/systems/graphql-system/entity-meta.d.ts +14 -1
  36. package/dist/configurator/application-runtime/extensions/systems/graphql-system/entity-meta.js +1 -1
  37. package/dist/configurator/application-runtime/extensions/systems/graphql-system/graphql-system.d.ts +45 -4
  38. package/dist/configurator/application-runtime/extensions/systems/graphql-system/graphql-system.js +1 -1
  39. package/dist/configurator/application-runtime/extensions/systems/image-rendering-system/image-rendering-system.d.ts +14 -5
  40. package/dist/configurator/application-runtime/extensions/systems/image-rendering-system/image-rendering-system.js +1 -1
  41. package/dist/configurator/application-runtime/extensions/systems/index.d.ts +1 -1
  42. package/dist/configurator/common/configurator-interface.d.ts +33 -2
  43. package/dist/configurator/common/configurator-interface.js +1 -1
  44. package/dist/configurator/configurator-context/configurator-hooks/layout-hooks.d.ts +4 -0
  45. package/dist/configurator/configurator-context/configurator-routing/ConfiguratorRoutingProvider.js +1 -1
  46. package/dist/configurator/pages/default-configurator-elements.d.ts +3 -2
  47. package/dist/configurator/pages/default-configurator-elements.js +1 -1
  48. package/dist/configurator/pages/default-elements/index.d.ts +1 -0
  49. package/dist/configurator/pages/default-elements/index.js +1 -1
  50. package/dist/configurator/pages/default-elements/scene-controls/BuiltInAddEntityFab.d.ts +3 -0
  51. package/dist/configurator/pages/default-elements/scene-controls/BuiltInAddEntityFab.js +1 -0
  52. package/dist/configurator/pages/default-elements/scene-controls/BuiltInDeleteEntityFab.d.ts +3 -0
  53. package/dist/configurator/pages/default-elements/scene-controls/BuiltInDeleteEntityFab.js +1 -0
  54. package/dist/configurator/pages/default-elements/scene-controls/BuiltInNavigationHotspot.d.ts +3 -0
  55. package/dist/configurator/pages/default-elements/scene-controls/BuiltInNavigationHotspot.js +1 -0
  56. package/dist/configurator/pages/default-elements/scene-controls/BuiltInSelectionHotspot.d.ts +3 -0
  57. package/dist/configurator/pages/default-elements/scene-controls/BuiltInSelectionHotspot.js +1 -0
  58. package/dist/configurator/pages/default-elements/scene-controls/BuiltInToolMenuFab.d.ts +3 -0
  59. package/dist/configurator/pages/default-elements/scene-controls/BuiltInToolMenuFab.js +1 -0
  60. package/dist/configurator/pages/default-elements/scene-controls/index.d.ts +6 -0
  61. package/dist/configurator/pages/default-elements/scene-controls/index.js +1 -0
  62. package/dist/configurator/pages/default-elements/scene-controls/scene-control-elements.d.ts +6 -0
  63. package/dist/configurator/pages/default-elements/scene-controls/scene-control-elements.js +1 -0
  64. package/dist/configurator/scene/index.d.ts +1 -0
  65. package/dist/configurator/scene/index.js +1 -1
  66. package/dist/configurator/scene/scene-controls/ToolFab.d.ts +4 -0
  67. package/dist/configurator/scene/scene-controls/ToolFab.js +1 -1
  68. package/dist/configurator/scene/scene-controls/use-scene-control-tooltip.d.ts +2 -1
  69. package/dist/configurator/scene/scene-entity/scene-controls/SceneEntityHtmlHotspots.js +1 -1
  70. package/dist/configurator/toolbar-actions/index.d.ts +1 -0
  71. package/dist/configurator/toolbar-actions/index.js +1 -1
  72. package/dist/configurator/toolbar-actions/use-toolbar-actions.d.ts +100 -0
  73. package/dist/configurator/toolbar-actions/use-toolbar-actions.js +1 -0
  74. package/dist/configurator/toolbar-actions/use-toolbar-help.d.ts +7 -0
  75. package/dist/configurator/toolbar-actions/use-toolbar-help.js +1 -0
  76. package/dist/configurator/ui/assistant/AssistantChips.d.ts +3 -2
  77. package/dist/configurator/ui/assistant/AssistantChips.js +1 -1
  78. package/dist/configurator/ui/assistant/AssistantMarkdown.d.ts +1 -1
  79. package/dist/configurator/ui/assistant/AssistantMarkdown.js +1 -1
  80. package/dist/configurator/ui/assistant/alert/AssistantAlert.d.ts +8 -0
  81. package/dist/configurator/ui/assistant/alert/AssistantAlert.js +1 -0
  82. package/dist/configurator/ui/assistant/alert/index.d.ts +2 -0
  83. package/dist/configurator/ui/assistant/alert/index.js +1 -0
  84. package/dist/configurator/ui/assistant/alert/remark-alerts.d.ts +10 -0
  85. package/dist/configurator/ui/assistant/alert/remark-alerts.js +1 -0
  86. package/dist/configurator/ui/assistant/form/AssistantForm.js +1 -1
  87. package/dist/configurator/ui/assistant/form/compose-answer.d.ts +1 -1
  88. package/dist/configurator/ui/assistant/form/compose-answer.js +1 -1
  89. package/dist/configurator/ui/assistant/form/form-model.d.ts +3 -4
  90. package/dist/configurator/ui/assistant/form/form-state.d.ts +1 -1
  91. package/dist/configurator/ui/assistant/form/index.d.ts +2 -0
  92. package/dist/configurator/ui/assistant/form/index.js +1 -1
  93. package/dist/configurator/ui/assistant/form/parse-form-block.d.ts +3 -2
  94. package/dist/configurator/ui/assistant/form/parse-form-block.js +1 -1
  95. package/dist/configurator/ui/assistant/form/read-form-attributes.d.ts +1 -1
  96. package/dist/configurator/ui/assistant/form/read-form-attributes.js +1 -1
  97. package/dist/configurator/ui/assistant/form/registry/built-in-form-elements.d.ts +3 -0
  98. package/dist/configurator/ui/assistant/form/registry/built-in-form-elements.js +1 -0
  99. package/dist/configurator/ui/assistant/form/registry/create-assistant-form-element.d.ts +31 -0
  100. package/dist/configurator/ui/assistant/form/registry/create-assistant-form-element.js +1 -0
  101. package/dist/configurator/ui/assistant/form/{form-element-definition.d.ts → registry/form-element-definition.d.ts} +3 -1
  102. package/dist/configurator/ui/assistant/form/registry/form-element-definition.js +1 -0
  103. package/dist/configurator/ui/assistant/form/{form-registry.d.ts → registry/form-registry.d.ts} +2 -2
  104. package/dist/configurator/ui/assistant/form/registry/form-registry.js +1 -0
  105. package/dist/configurator/ui/assistant/form/registry/index.d.ts +6 -0
  106. package/dist/configurator/ui/assistant/form/registry/index.js +1 -0
  107. package/dist/configurator/ui/assistant/form/registry/use-form-registry.d.ts +8 -0
  108. package/dist/configurator/ui/assistant/form/registry/use-form-registry.js +1 -0
  109. package/dist/configurator/ui/assistant/form-elements/FormCard.d.ts +9 -0
  110. package/dist/configurator/ui/assistant/form-elements/FormCard.js +1 -0
  111. package/dist/configurator/ui/assistant/form-elements/FormConfirm.d.ts +9 -0
  112. package/dist/configurator/ui/assistant/form-elements/FormConfirm.js +1 -0
  113. package/dist/configurator/ui/assistant/form-elements/FormGroup.d.ts +4 -1
  114. package/dist/configurator/ui/assistant/form-elements/FormNumber.d.ts +9 -0
  115. package/dist/configurator/ui/assistant/form-elements/FormNumber.js +1 -0
  116. package/dist/configurator/ui/assistant/form-elements/FormOptions.d.ts +4 -1
  117. package/dist/configurator/ui/assistant/form-elements/FormReply.d.ts +4 -1
  118. package/dist/configurator/ui/assistant/form-elements/FormReply.js +1 -1
  119. package/dist/configurator/ui/assistant/form-elements/FormSwatch.d.ts +10 -0
  120. package/dist/configurator/ui/assistant/form-elements/FormSwatch.js +1 -0
  121. package/dist/configurator/ui/assistant/form-elements/FormText.d.ts +4 -1
  122. package/dist/configurator/ui/assistant/form-elements/FormText.js +1 -1
  123. package/dist/configurator/ui/assistant/form-elements/clamp-form-number.d.ts +10 -0
  124. package/dist/configurator/ui/assistant/form-elements/clamp-form-number.js +1 -0
  125. package/dist/configurator/ui/assistant/form-elements/form-element-props.d.ts +20 -16
  126. package/dist/configurator/ui/assistant/form-elements/index.d.ts +4 -0
  127. package/dist/configurator/ui/assistant/form-elements/index.js +1 -1
  128. package/dist/configurator/ui/assistant/index.d.ts +3 -0
  129. package/dist/configurator/ui/assistant/index.js +1 -1
  130. package/dist/configurator/ui/assistant/resource-image/index.d.ts +1 -1
  131. package/dist/configurator/ui/assistant/resource-image/index.js +1 -1
  132. package/dist/configurator/ui/assistant/resource-image/resource-path.d.ts +0 -9
  133. package/dist/configurator/ui/assistant/resource-image/resource-path.js +1 -1
  134. package/dist/configurator/ui/assistant/screenshots/ScreenshotThumbnails.d.ts +9 -0
  135. package/dist/configurator/ui/assistant/screenshots/ScreenshotThumbnails.js +1 -0
  136. package/dist/configurator/ui/assistant/screenshots/index.d.ts +2 -0
  137. package/dist/configurator/ui/assistant/screenshots/index.js +1 -0
  138. package/dist/configurator/ui/assistant/screenshots/tool-screenshots.d.ts +14 -0
  139. package/dist/configurator/ui/assistant/screenshots/tool-screenshots.js +1 -0
  140. package/dist/configurator/ui/assistant/streamed-markdown.d.ts +2 -2
  141. package/dist/configurator/ui/assistant/streamed-markdown.js +2 -2
  142. package/dist/configurator/ui/assistant/tool-chip.js +1 -1
  143. package/dist/configurator/ui/configurator-layout/universal/ExportingOverlay.d.ts +5 -0
  144. package/dist/configurator/ui/configurator-layout/universal/ExportingOverlay.js +1 -0
  145. package/dist/configurator/ui/configurator-layout/universal/Layout.js +1 -1
  146. package/dist/configurator/ui/index.d.ts +1 -0
  147. package/dist/configurator/ui/index.js +1 -1
  148. package/dist/docs/complex-use-cases/focus-mode.mdx +2 -0
  149. package/dist/docs/fabs/basic.mdx +4 -0
  150. package/dist/docs/index.json +62 -1
  151. package/dist/docs/layouts/custom-layout.mdx +3 -1
  152. package/dist/docs/layouts/gallery/ardent-compressor.mdx +3515 -0
  153. package/dist/docs/layouts/gallery/fenwick-window.mdx +2732 -0
  154. package/dist/docs/layouts/gallery/ferrule-bar-station.mdx +6567 -0
  155. package/dist/docs/layouts/gallery/kestrel-kettle.mdx +1858 -0
  156. package/dist/docs/layouts/gallery/sunfold-solar.mdx +2671 -0
  157. package/dist/docs/other-features/client-side-rendering/declarative.mdx +1 -1
  158. package/dist/docs/other-features/navigation-hotspot.mdx +2 -0
  159. package/dist/docs/phoenix/configurator-interface.mdx +64 -8
  160. package/dist/docs/phoenix/sdk-builder-workflow.mdx +1 -1
  161. package/dist/docs/selection-and-highlight/hotspots.mdx +2 -0
  162. package/dist/domain/application/assistant/assistant-manifest.d.ts +16 -1
  163. package/dist/domain/application/assistant/assistant-resolved-manifest.d.ts +8 -1
  164. package/dist/domain/application/assistant/build-assistant-manifest.js +1 -1
  165. package/dist/domain/application/assistant/form/assistant-form-element-name.d.ts +11 -0
  166. package/dist/domain/application/assistant/form/assistant-form-element-name.js +8 -0
  167. package/dist/domain/application/assistant/form/assistant-form-elements.d.ts +174 -0
  168. package/dist/domain/application/assistant/form/assistant-form-elements.js +1 -0
  169. package/dist/domain/application/assistant/form/assistant-resource-path.d.ts +10 -0
  170. package/dist/domain/application/assistant/form/assistant-resource-path.js +1 -0
  171. package/dist/domain/application/assistant/form/index.d.ts +4 -0
  172. package/dist/domain/application/assistant/form/index.js +1 -0
  173. package/dist/domain/application/assistant/generate-assistant-prompt.d.ts +3 -0
  174. package/dist/domain/application/assistant/generate-assistant-prompt.js +1 -1
  175. package/dist/domain/application/assistant/index.d.ts +2 -3
  176. package/dist/domain/application/assistant/index.js +1 -1
  177. package/dist/domain/application/assistant/session-setup/apply-assistant-setup.js +1 -1
  178. package/dist/domain/application/assistant/session-setup/assistant-setup-manifest.d.ts +2 -0
  179. package/dist/domain/application/assistant/session-setup/assistant-setup-manifest.js +1 -1
  180. package/dist/domain/available-system-components.d.ts +1 -1
  181. package/dist/domain/available-system-components.js +1 -1
  182. package/dist/domain/domain-context/store-system/store-system.d.ts +5 -0
  183. package/dist/domain/domain-context/store-system/store-system.js +1 -1
  184. package/dist/domain/domain-runtime/domain-runtime.d.ts +4 -0
  185. package/dist/domain/domain-runtime/environment/types.d.ts +1 -1
  186. package/dist/domain/domain-runtime/spec/index.d.ts +1 -1
  187. package/dist/domain/domain-runtime/spec/index.js +1 -1
  188. package/dist/domain/domain-runtime/spec/initialize.js +1 -1
  189. package/dist/domain/domain-runtime/spec/module-checks/element-registration.d.ts +15 -1
  190. package/dist/domain/domain-runtime/spec/module-checks/element-registration.js +1 -1
  191. package/dist/domain/domain-runtime/spec/module-checks/find-assistant-form-element-errors.d.ts +12 -0
  192. package/dist/domain/domain-runtime/spec/module-checks/find-assistant-form-element-errors.js +1 -0
  193. package/dist/domain/domain-runtime/spec/module-checks/index.d.ts +2 -1
  194. package/dist/domain/domain-runtime/spec/module-checks/index.js +1 -1
  195. package/dist/domain/domain-runtime/spec/selectors.d.ts +3 -1
  196. package/dist/domain/domain-runtime/spec/selectors.js +1 -1
  197. package/dist/domain/extensions/export-images/agent-screenshot.d.ts +24 -0
  198. package/dist/domain/extensions/export-images/agent-screenshot.js +1 -0
  199. package/dist/domain/extensions/{client-side-image.d.ts → export-images/client-side-image.d.ts} +2 -2
  200. package/dist/domain/extensions/export-images/client-side-image.js +1 -0
  201. package/dist/domain/extensions/export-images/index.d.ts +3 -0
  202. package/dist/domain/extensions/export-images/index.js +1 -0
  203. package/dist/domain/extensions/{save-template-thumbnail.d.ts → export-images/save-template-thumbnail.d.ts} +2 -2
  204. package/dist/domain/extensions/export-images/save-template-thumbnail.js +1 -0
  205. package/dist/domain/extensions/index.d.ts +1 -2
  206. package/dist/domain/extensions/index.js +1 -1
  207. package/dist/domain/mocks/create-mock-domain-runtime.d.ts +2 -0
  208. package/dist/editor/domain-editors/DomainSpecContext.js +1 -1
  209. package/dist/editor/domain-editors/apply-spec-update.d.ts +8 -1
  210. package/dist/editor/domain-editors/apply-spec-update.js +1 -1
  211. package/dist/editor/domain-editors/use-implementation-files.js +1 -1
  212. package/dist/editor/screenshot/ScreenshotHandler.js +1 -1
  213. package/dist/i18n/locales/cs/messages.js +1 -1
  214. package/dist/i18n/locales/da/messages.js +1 -1
  215. package/dist/i18n/locales/de/messages.js +1 -1
  216. package/dist/i18n/locales/de-AT/messages.js +1 -1
  217. package/dist/i18n/locales/en/messages.js +1 -1
  218. package/dist/i18n/locales/en-AU/messages.js +1 -1
  219. package/dist/i18n/locales/en-GB/messages.js +1 -1
  220. package/dist/i18n/locales/es/messages.js +1 -1
  221. package/dist/i18n/locales/fr/messages.js +1 -1
  222. package/dist/i18n/locales/hu/messages.js +1 -1
  223. package/dist/i18n/locales/it/messages.js +1 -1
  224. package/dist/i18n/locales/nl/messages.js +1 -1
  225. package/dist/i18n/locales/pl/messages.js +1 -1
  226. package/dist/i18n/locales/pt/messages.js +1 -1
  227. package/dist/i18n/locales/ro/messages.js +1 -1
  228. package/dist/i18n/locales/sk/messages.js +1 -1
  229. package/dist/i18n/locales/tr/messages.js +1 -1
  230. package/dist/legacy/domain-spec/__generated/domain-schema.d.ts +16 -0
  231. package/dist/legacy/domain-spec/spec/domain-spec.js +1 -1
  232. package/dist/react-sdk/application-runtime/hooks/use-phoenix-element-slots.d.ts +2 -1
  233. package/dist/react-sdk/application-runtime/integration/initialize.d.ts +7 -6
  234. package/dist/react-sdk/application-runtime/integration/initialize.js +1 -1
  235. package/dist/react-sdk/application-runtime/integration/selectors.d.ts +11 -2
  236. package/dist/react-sdk/application-runtime/integration/selectors.js +1 -1
  237. package/dist/react-sdk/application-runtime/integration/types.d.ts +5 -2
  238. package/dist/react-sdk/application-runtime/mocks/create-mock-application-runtime.js +1 -1
  239. package/dist/react-sdk/application-runtime/routing/routing-service.d.ts +34 -2
  240. package/dist/react-sdk/application-runtime/routing/routing-service.js +1 -1
  241. package/dist/react-sdk/application-runtime/runtime/create-application-runtime.d.ts +4 -2
  242. package/dist/react-sdk/application-runtime/runtime/create-application-runtime.js +1 -1
  243. package/dist/react-sdk/domain-context/system-hooks/undo-redo-hooks.js +1 -1
  244. package/dist/react-sdk/index.d.ts +1 -0
  245. package/dist/react-sdk/index.js +1 -1
  246. package/dist/react-sdk/scene-control-elements/AddEntityFabElement.d.ts +36 -0
  247. package/dist/react-sdk/scene-control-elements/AddEntityFabElement.js +1 -0
  248. package/dist/react-sdk/scene-control-elements/DeleteEntityFabElement.d.ts +44 -0
  249. package/dist/react-sdk/scene-control-elements/DeleteEntityFabElement.js +1 -0
  250. package/dist/react-sdk/scene-control-elements/NavigationHotspotElement.d.ts +38 -0
  251. package/dist/react-sdk/scene-control-elements/NavigationHotspotElement.js +1 -0
  252. package/dist/react-sdk/scene-control-elements/SelectionHotspotElement.d.ts +30 -0
  253. package/dist/react-sdk/scene-control-elements/SelectionHotspotElement.js +1 -0
  254. package/dist/react-sdk/scene-control-elements/ToolMenuFabElement.d.ts +30 -0
  255. package/dist/react-sdk/scene-control-elements/ToolMenuFabElement.js +1 -0
  256. package/dist/react-sdk/scene-control-elements/create-add-entity-fab.d.ts +9 -0
  257. package/dist/react-sdk/scene-control-elements/create-add-entity-fab.js +1 -0
  258. package/dist/react-sdk/scene-control-elements/create-delete-entity-fab.d.ts +9 -0
  259. package/dist/react-sdk/scene-control-elements/create-delete-entity-fab.js +1 -0
  260. package/dist/react-sdk/scene-control-elements/create-navigation-hotspot.d.ts +10 -0
  261. package/dist/react-sdk/scene-control-elements/create-navigation-hotspot.js +1 -0
  262. package/dist/react-sdk/scene-control-elements/create-selection-hotspot.d.ts +9 -0
  263. package/dist/react-sdk/scene-control-elements/create-selection-hotspot.js +1 -0
  264. package/dist/react-sdk/scene-control-elements/create-tool-menu-fab.d.ts +9 -0
  265. package/dist/react-sdk/scene-control-elements/create-tool-menu-fab.js +1 -0
  266. package/dist/react-sdk/scene-control-elements/index.d.ts +11 -0
  267. package/dist/react-sdk/scene-control-elements/index.js +1 -0
  268. package/dist/react-sdk/scene-control-elements/scene-control-tooltip-visibility.d.ts +5 -0
  269. package/dist/react-sdk/web-component/create-web-component.js +1 -1
  270. package/dist/react-sdk/web-component/god-mode/god-mode-registry.d.ts +30 -0
  271. package/dist/react-sdk/web-component/god-mode/god-mode-registry.js +1 -0
  272. package/dist/react-sdk/web-component/god-mode/index.d.ts +1 -0
  273. package/dist/react-sdk/web-component/god-mode/index.js +1 -0
  274. package/dist/ui-sdk/icons/__generated/Lightbulb.d.ts +3 -0
  275. package/dist/ui-sdk/icons/__generated/Lightbulb.js +1 -0
  276. package/dist/ui-sdk/icons/__generated/XCircle.d.ts +3 -0
  277. package/dist/ui-sdk/icons/__generated/XCircle.js +1 -0
  278. package/dist/ui-sdk/icons/__generated/index.d.ts +4 -0
  279. package/dist/ui-sdk/icons/__generated/index.js +1 -1
  280. package/dist/ui-sdk/icons/icons-registry.js +1 -1
  281. package/dist/ui-sdk/theme/types.d.ts +5 -1
  282. package/package.json +13 -3
  283. package/schema/domain-spec.json +140 -0
  284. package/dist/configurator/ui/assistant/form/built-in-form-registry.d.ts +0 -10
  285. package/dist/configurator/ui/assistant/form/built-in-form-registry.js +0 -1
  286. package/dist/configurator/ui/assistant/form/form-registry.js +0 -8
  287. package/dist/domain/application/assistant/assistant-form-elements.d.ts +0 -28
  288. package/dist/domain/application/assistant/assistant-form-elements.js +0 -1
  289. package/dist/domain/extensions/client-side-image.js +0 -1
  290. package/dist/domain/extensions/save-template-thumbnail.js +0 -1
  291. package/dist/react-sdk/application-runtime/runtime/god-mode.d.ts +0 -19
  292. package/dist/react-sdk/application-runtime/runtime/god-mode.js +0 -1
  293. /package/dist/{configurator/ui/assistant/form/form-element-definition.js → react-sdk/scene-control-elements/scene-control-tooltip-visibility.js} +0 -0
@@ -46,6 +46,7 @@ Without a lockfile, use the package manager the \`packageManager\` field of \`pa
46
46
  - Messages: mount \`LayoutSlots.Services\` for the built-in snackbars, or show violations, side-effect hints and errors yourself with \`phx.frontend.useConfiguratorMessages(onMessage)\`, not both. Either way mount a snackbar root (\`LayoutSlots.DesktopSnackbarsRoot\` or \`LayoutSlots.MobileSnackbarsRoot\`): save and built-in action errors are always snackbars
47
47
  - Finalize over the spec's \`finalizeAction\` components, each naming a handler registered with \`phx.frontend.createPhoenixHandler\`: run each with \`phx.frontend.useFinalizeAction(handler)\` in a component of its own wrapped in \`PhoenixHandlerBoundary\` (from \`@salsita-ai/sdk/react\`), or mount \`FinalizeActions\` (from \`@salsita-ai/sdk/configurator\`) for the built-in buttons. \`finalizePage\` and \`finalizeButton\` are legacy
48
48
  - Read mode and fullscreen are the layout's job, no hook enforces them: hide what a viewer must not change while \`phx.frontend.useAccessService().accessInfo.viewMode\` is \`true\`, and remove panels while \`phx.frontend.useIsFullscreenActive()\` is set; \`phx.frontend.useSetFullscreen()\` returns its setter
49
+ - Assistant form elements: an element declared under \`assistant.formElements\` (\`description\`, \`role\`, scalar \`attributes\` beside the built-in \`label\`, a \`value\` included, \`required\`) is the default export \`phx.frontend.createAssistantFormElement('name', Component)\` of one file; the name types the component's props by its role (\`AssistantFormElementProps<'name'>\`): an option reports its answer with \`onSelect(answer)\`, an input with \`onChange(state, answer)\`; and the application does not start while a declared element has no registration or a registration no declaration
49
50
  - \`src/handlers/\` — Event & lifecycle handlers
50
51
  - \`src/webhooks/\` — Server-side webhook handlers (\`phx.webhooks.*Builder\`); the webhook is the default export of \`src/webhooks/index.ts\`, which you create when the project needs its own handlers — without it the build uses a built-in inert webhook that answers only anonymous auth, so create the file before setting \`useAuthHook\` or \`useClerk\`
51
52
  - \`src/migrations/\` — Schema version migrations (v*.ts)
@@ -87,7 +88,7 @@ The default export must be what the folder's phx factory returns (for example \`
87
88
  readonly webhookMembership: `## Webhook Membership
88
89
  - User-authored components, admin components, and handlers are frontend-only by default.
89
90
  - A handler that registers \`phx.domain.createAssistantSetupHandler\`, \`createMapPublishedRevisionDataHandler\`, \`createMapPublishedChunkedDataHandler\`, \`createDataRowValidatorHandler\`, or \`createSheetValidatorHandler\` is included in the webhook automatically.
90
- - An assistant setup handler receives the declaration-shaped whitelist and { role, sessionData, access }. Keep the exact entity/alias split from YAML; inherited fields and expanded member entries are absent. Narrow original declarations, then the SDK expands them. Use project-defined role/session data for per-user customization. Keep the handler synchronous, pure and deterministic. The handler runs in the browser and webhook. Return null to disable assistant execution, or omit whitelist entries to restrict them. Button and panel visibility use spec presence; project developers own dynamic UI filtering through normal layout mechanics such as $filter. Leaves expose optional descriptions; writable options allow only validate: true and allowDryRuns: false; recipe groups allow descriptions and allowDryRuns: false. Deletable entries are {}. Names, component bindings, hints and types are internal. Omitted overrides retain baseline values. Existing entity, enhanced-entity and action types constrain the handler; no separate generated manifest types. Browser GraphQL, tools and change recording use the session-derived manifest; rejected setup fails closed. UI reads the pure spec selector. Omitted server instructions or AI fields preserve the baseline. An empty server instructions string clears it.
91
+ - An assistant setup handler receives the declaration-shaped whitelist and { role, sessionData, access }. Keep the exact entity/alias split from YAML; inherited fields and expanded member entries are absent. Narrow original declarations, then the SDK expands them. Use project-defined role/session data for per-user customization. Keep the handler synchronous, pure and deterministic. The handler runs in the browser and webhook. Return null to disable assistant execution, or omit whitelist entries to restrict them. Button and panel visibility use spec presence; project developers own dynamic UI filtering through normal layout mechanics such as $filter. Leaves expose optional descriptions; writable options allow only validate: true and allowDryRuns: false; recipe groups allow descriptions and allowDryRuns: false; screenshot views allow descriptions. Deletable entries are {}. Names, component bindings, hints and types are internal. Omitted overrides retain baseline values. Existing entity, enhanced-entity and action types constrain the handler; no separate generated manifest types. Browser GraphQL, tools and change recording use the session-derived manifest; rejected setup fails closed. UI reads the pure spec selector. Omitted server instructions or AI fields preserve the baseline. An empty server instructions string clears it.
91
92
  - To include another component or handler in the webhook, put \`'use webhook'\` as its first statement (leading comments are allowed).
92
93
  - A webhook-included file and its runtime local-import closure must not import browser SDK entrypoints (\`application\`, \`admin\`, \`configurator\`, \`editor\`, \`react\`, \`ui\`) or style/font/image assets. Spec parsing reports the unsafe import and inclusion reason as a diagnostic.
93
94
  - Keep browser rendering in an unpinned file. Move shared server-safe logic into a separate module when both frontend and webhook code need it.`;
@@ -1,10 +1,10 @@
1
- export const sdkGuidanceSections={packageManager:"## Package Manager\nThe project installs with its own package manager, and the commands in this skill are written for pnpm. Run them through the package manager the project uses, which the lockfile in the project root names:\n- `pnpm-lock.yaml`: `pnpm phoenix <command>`, `pnpm install`, `pnpm add <package>`\n- `package-lock.json`: `npx phoenix <command>`, `npm install`, `npm install <package>`\n- `yarn.lock`: `yarn phoenix <command>`, `yarn install`, `yarn add <package>`\n- `bun.lock` or `bun.lockb`: `bunx phoenix <command>`, `bun install`, `bun add <package>`\n\nWithout a lockfile, use the package manager the `packageManager` field of `package.json` names, or ask the user.",buildCommands:"## Build Commands\n- `pnpm phoenix build` — Full production build\n- `pnpm phoenix dev` — Development mode with hot reload\n- `pnpm phoenix sync` — Sync generated files without full build\n- `pnpm phoenix deps` — Add the SDK's missing peer dependencies to `package.json` and move out-of-range ones into the SDK's range; it installs nothing, so install after it",devLogs:"## Dev Logs\n- `pnpm phoenix dev` writes all logs to `.phoenix/phoenix-dev.log`\n- Contains: spec parse errors, build errors, browser runtime errors, file watcher events\n- After making changes, always check the log for errors: `tail -50 .phoenix/phoenix-dev.log` or `grep ERROR .phoenix/phoenix-dev.log`\n- Rotated automatically at 10MB (`.log.1`, `.log.2` are older rotations)",projectStructure:"## Project Structure\n- `spec/` — Domain YAML specs (entities, relations, expressions, invariants, components, application config)\n- `src/expressions/` — Computed/derived entity properties (`phx.domain.implementCustomExpressions()`)\n- `src/actions/` — Domain operations declared under `actions:` in the spec (`phx.domain.implementActions()`), run by controls and exposed to the assistant\n- `src/invariants/` — Business rule constraints (`phx.domain.implementCustomInvariants()`)\n- `src/transformers/` — Automatic state fixers (`phx.frontend.transformerBuilder`)\n- `src/components/` — ECS system components (`phx.domain.attachComponent.withEntity()`)\n- `src/elements/` — Page-level React components (`phx.frontend.createCustomPhoenixLayoutElement()`)\n - `ConfiguratorLayout`: a layout element with this name replaces everything the built-in layout renders for a loaded configuration, in owner and read mode, finalization included. Prefer arranging the spec's layout components; write one for shell-level changes, such as moving the sidebar or adding an area the built-in layout has no place for\n - Mount `LayoutSlots.SharedCanvas` (from `@salsita-ai/sdk/configurator`) in a positioned root covering the page, or the app loader stays on screen; put the UI above it, positioned and after it in the markup. Call `useSceneSafeArea()` (same import) and put `containerRef` on the element covering the whole canvas and `safeAreaRef` on the region the UI leaves free, with `pointerEvents: 'none'`, so the camera frames the scene there\n - Sidebar moved, contents as the spec configures them: mount `LayoutSlots.DesktopSidebar` in a positioned container that sets its size, with `side=\"start\"` on the left; `docs/layouts/sidebar/in-a-custom-layout.mdx` is the whole layout\n - Your own UI: \"Hooks for a Project Layout\" in `docs/phoenix/configurator-interface.mdx` lists the hooks with what each returns (`pnpm phoenix docs -r docs/phoenix/configurator-interface.mdx`), `docs/phoenix/layout-slots.mdx` the built-in pieces you can still mount, and `docs/layouts/custom-layout.mdx` is a worked example\n - Messages: mount `LayoutSlots.Services` for the built-in snackbars, or show violations, side-effect hints and errors yourself with `phx.frontend.useConfiguratorMessages(onMessage)`, not both. Either way mount a snackbar root (`LayoutSlots.DesktopSnackbarsRoot` or `LayoutSlots.MobileSnackbarsRoot`): save and built-in action errors are always snackbars\n - Finalize over the spec's `finalizeAction` components, each naming a handler registered with `phx.frontend.createPhoenixHandler`: run each with `phx.frontend.useFinalizeAction(handler)` in a component of its own wrapped in `PhoenixHandlerBoundary` (from `@salsita-ai/sdk/react`), or mount `FinalizeActions` (from `@salsita-ai/sdk/configurator`) for the built-in buttons. `finalizePage` and `finalizeButton` are legacy\n - Read mode and fullscreen are the layout's job, no hook enforces them: hide what a viewer must not change while `phx.frontend.useAccessService().accessInfo.viewMode` is `true`, and remove panels while `phx.frontend.useIsFullscreenActive()` is set; `phx.frontend.useSetFullscreen()` returns its setter\n- `src/handlers/` — Event & lifecycle handlers\n- `src/webhooks/` — Server-side webhook handlers (`phx.webhooks.*Builder`); the webhook is the default export of `src/webhooks/index.ts`, which you create when the project needs its own handlers — without it the build uses a built-in inert webhook that answers only anonymous auth, so create the file before setting `useAuthHook` or `useClerk`\n- `src/migrations/` — Schema version migrations (v*.ts)\n- `src/locales/` — Locale catalogs (authored .po sources; compiled .ts, do not edit)\n- `src/admin/` — Admin interface (mirrors main src/ structure)\n- `src/_shared/` — Shared utilities (NOT auto-imported, requires explicit imports)\n- `.phoenix/` — Auto-generated files (DO NOT EDIT, except the option files in `.phoenix/variants/`)\n- `public/` — Static assets (3D models, textures, images, HDRI maps, fonts)\n- `assets/` — Uploadable resources (PDF templates, email assets)",architecturePrinciples:"## Architecture Principles\n- YAML-first: 99% of configuration in spec/*.yaml, custom code is last resort\n- ECS pattern: entities (domain objects) + components (behavior/UI) + systems (SDK runtime)\n- Reactive state: immutable domain state flows through RxJS pipelines\n- Data binding: `$read` (read-only), `$write` (write-only), `$bind` (bidirectional) connect components to domain\n- memoEval: all component implementations must use memoEval for stable object references",keyPatterns:"## Key Patterns\n- Expressions = computed properties on entities (prefix: `$`), implemented per-entity in src/expressions/\n- Actions = named domain operations (`actions:` in YAML with `params` and `result` schemas), implemented in src/actions/, prepared through `actionSystem.prepareAction()` and exposed as assistant mutations through `assistant.actions`\n- Invariants = validation rules returning violations, static or dynamic\n- Transformers = auto-fixers maintaining consistency after state changes\n- Components defined in YAML with `$read`/`$bind` bindings, or custom in src/components/ using attachComponent builder\n- Webhooks = server-side finalization tasks (PDF, email, CSV, image rendering)\n- Filters = contextual component activation via navigationStates, viewStates, modes, selectedEntityIds",fileConventions:`## File Conventions
1
+ export const sdkGuidanceSections={packageManager:"## Package Manager\nThe project installs with its own package manager, and the commands in this skill are written for pnpm. Run them through the package manager the project uses, which the lockfile in the project root names:\n- `pnpm-lock.yaml`: `pnpm phoenix <command>`, `pnpm install`, `pnpm add <package>`\n- `package-lock.json`: `npx phoenix <command>`, `npm install`, `npm install <package>`\n- `yarn.lock`: `yarn phoenix <command>`, `yarn install`, `yarn add <package>`\n- `bun.lock` or `bun.lockb`: `bunx phoenix <command>`, `bun install`, `bun add <package>`\n\nWithout a lockfile, use the package manager the `packageManager` field of `package.json` names, or ask the user.",buildCommands:"## Build Commands\n- `pnpm phoenix build` — Full production build\n- `pnpm phoenix dev` — Development mode with hot reload\n- `pnpm phoenix sync` — Sync generated files without full build\n- `pnpm phoenix deps` — Add the SDK's missing peer dependencies to `package.json` and move out-of-range ones into the SDK's range; it installs nothing, so install after it",devLogs:"## Dev Logs\n- `pnpm phoenix dev` writes all logs to `.phoenix/phoenix-dev.log`\n- Contains: spec parse errors, build errors, browser runtime errors, file watcher events\n- After making changes, always check the log for errors: `tail -50 .phoenix/phoenix-dev.log` or `grep ERROR .phoenix/phoenix-dev.log`\n- Rotated automatically at 10MB (`.log.1`, `.log.2` are older rotations)",projectStructure:"## Project Structure\n- `spec/` — Domain YAML specs (entities, relations, expressions, invariants, components, application config)\n- `src/expressions/` — Computed/derived entity properties (`phx.domain.implementCustomExpressions()`)\n- `src/actions/` — Domain operations declared under `actions:` in the spec (`phx.domain.implementActions()`), run by controls and exposed to the assistant\n- `src/invariants/` — Business rule constraints (`phx.domain.implementCustomInvariants()`)\n- `src/transformers/` — Automatic state fixers (`phx.frontend.transformerBuilder`)\n- `src/components/` — ECS system components (`phx.domain.attachComponent.withEntity()`)\n- `src/elements/` — Page-level React components (`phx.frontend.createCustomPhoenixLayoutElement()`)\n - `ConfiguratorLayout`: a layout element with this name replaces everything the built-in layout renders for a loaded configuration, in owner and read mode, finalization included. Prefer arranging the spec's layout components; write one for shell-level changes, such as moving the sidebar or adding an area the built-in layout has no place for\n - Mount `LayoutSlots.SharedCanvas` (from `@salsita-ai/sdk/configurator`) in a positioned root covering the page, or the app loader stays on screen; put the UI above it, positioned and after it in the markup. Call `useSceneSafeArea()` (same import) and put `containerRef` on the element covering the whole canvas and `safeAreaRef` on the region the UI leaves free, with `pointerEvents: 'none'`, so the camera frames the scene there\n - Sidebar moved, contents as the spec configures them: mount `LayoutSlots.DesktopSidebar` in a positioned container that sets its size, with `side=\"start\"` on the left; `docs/layouts/sidebar/in-a-custom-layout.mdx` is the whole layout\n - Your own UI: \"Hooks for a Project Layout\" in `docs/phoenix/configurator-interface.mdx` lists the hooks with what each returns (`pnpm phoenix docs -r docs/phoenix/configurator-interface.mdx`), `docs/phoenix/layout-slots.mdx` the built-in pieces you can still mount, and `docs/layouts/custom-layout.mdx` is a worked example\n - Messages: mount `LayoutSlots.Services` for the built-in snackbars, or show violations, side-effect hints and errors yourself with `phx.frontend.useConfiguratorMessages(onMessage)`, not both. Either way mount a snackbar root (`LayoutSlots.DesktopSnackbarsRoot` or `LayoutSlots.MobileSnackbarsRoot`): save and built-in action errors are always snackbars\n - Finalize over the spec's `finalizeAction` components, each naming a handler registered with `phx.frontend.createPhoenixHandler`: run each with `phx.frontend.useFinalizeAction(handler)` in a component of its own wrapped in `PhoenixHandlerBoundary` (from `@salsita-ai/sdk/react`), or mount `FinalizeActions` (from `@salsita-ai/sdk/configurator`) for the built-in buttons. `finalizePage` and `finalizeButton` are legacy\n - Read mode and fullscreen are the layout's job, no hook enforces them: hide what a viewer must not change while `phx.frontend.useAccessService().accessInfo.viewMode` is `true`, and remove panels while `phx.frontend.useIsFullscreenActive()` is set; `phx.frontend.useSetFullscreen()` returns its setter\n - Assistant form elements: an element declared under `assistant.formElements` (`description`, `role`, scalar `attributes` beside the built-in `label`, a `value` included, `required`) is the default export `phx.frontend.createAssistantFormElement('name', Component)` of one file; the name types the component's props by its role (`AssistantFormElementProps<'name'>`): an option reports its answer with `onSelect(answer)`, an input with `onChange(state, answer)`; and the application does not start while a declared element has no registration or a registration no declaration\n- `src/handlers/` — Event & lifecycle handlers\n- `src/webhooks/` — Server-side webhook handlers (`phx.webhooks.*Builder`); the webhook is the default export of `src/webhooks/index.ts`, which you create when the project needs its own handlers — without it the build uses a built-in inert webhook that answers only anonymous auth, so create the file before setting `useAuthHook` or `useClerk`\n- `src/migrations/` — Schema version migrations (v*.ts)\n- `src/locales/` — Locale catalogs (authored .po sources; compiled .ts, do not edit)\n- `src/admin/` — Admin interface (mirrors main src/ structure)\n- `src/_shared/` — Shared utilities (NOT auto-imported, requires explicit imports)\n- `.phoenix/` — Auto-generated files (DO NOT EDIT, except the option files in `.phoenix/variants/`)\n- `public/` — Static assets (3D models, textures, images, HDRI maps, fonts)\n- `assets/` — Uploadable resources (PDF templates, email assets)",architecturePrinciples:"## Architecture Principles\n- YAML-first: 99% of configuration in spec/*.yaml, custom code is last resort\n- ECS pattern: entities (domain objects) + components (behavior/UI) + systems (SDK runtime)\n- Reactive state: immutable domain state flows through RxJS pipelines\n- Data binding: `$read` (read-only), `$write` (write-only), `$bind` (bidirectional) connect components to domain\n- memoEval: all component implementations must use memoEval for stable object references",keyPatterns:"## Key Patterns\n- Expressions = computed properties on entities (prefix: `$`), implemented per-entity in src/expressions/\n- Actions = named domain operations (`actions:` in YAML with `params` and `result` schemas), implemented in src/actions/, prepared through `actionSystem.prepareAction()` and exposed as assistant mutations through `assistant.actions`\n- Invariants = validation rules returning violations, static or dynamic\n- Transformers = auto-fixers maintaining consistency after state changes\n- Components defined in YAML with `$read`/`$bind` bindings, or custom in src/components/ using attachComponent builder\n- Webhooks = server-side finalization tasks (PDF, email, CSV, image rendering)\n- Filters = contextual component activation via navigationStates, viewStates, modes, selectedEntityIds",fileConventions:`## File Conventions
2
2
  - Components: PascalCase (.tsx)
3
3
  - Utilities: kebab-case (.ts)
4
4
  - Expressions: entity-name.ts
5
5
  - Migrations: v*.ts (sequential)
6
6
  - Specs: kebab-case.yaml
7
- - Underscore folders (_shared/, _utils/) are NOT auto-imported`,exportRules:'## Export Rules\nAll files in src/expressions/, src/actions/, src/invariants/, src/transformers/, src/components/, src/handlers/, src/elements/, src/webhooks/ MUST use default exports. Can be a single item or an array. Named exports are ignored by the SDK auto-import system.\nThe default export must be what the folder\'s phx factory returns (for example `phx.frontend.createCustomPhoenixLayoutElement()` in src/elements/). Any other value, such as a plain React component, stops the application at start with an "Invalid default export in <file>" error. Put plain components and helpers in an underscore folder.',webhookMembership:"## Webhook Membership\n- User-authored components, admin components, and handlers are frontend-only by default.\n- A handler that registers `phx.domain.createAssistantSetupHandler`, `createMapPublishedRevisionDataHandler`, `createMapPublishedChunkedDataHandler`, `createDataRowValidatorHandler`, or `createSheetValidatorHandler` is included in the webhook automatically.\n- An assistant setup handler receives the declaration-shaped whitelist and { role, sessionData, access }. Keep the exact entity/alias split from YAML; inherited fields and expanded member entries are absent. Narrow original declarations, then the SDK expands them. Use project-defined role/session data for per-user customization. Keep the handler synchronous, pure and deterministic. The handler runs in the browser and webhook. Return null to disable assistant execution, or omit whitelist entries to restrict them. Button and panel visibility use spec presence; project developers own dynamic UI filtering through normal layout mechanics such as $filter. Leaves expose optional descriptions; writable options allow only validate: true and allowDryRuns: false; recipe groups allow descriptions and allowDryRuns: false. Deletable entries are {}. Names, component bindings, hints and types are internal. Omitted overrides retain baseline values. Existing entity, enhanced-entity and action types constrain the handler; no separate generated manifest types. Browser GraphQL, tools and change recording use the session-derived manifest; rejected setup fails closed. UI reads the pure spec selector. Omitted server instructions or AI fields preserve the baseline. An empty server instructions string clears it.\n- To include another component or handler in the webhook, put `'use webhook'` as its first statement (leading comments are allowed).\n- A webhook-included file and its runtime local-import closure must not import browser SDK entrypoints (`application`, `admin`, `configurator`, `editor`, `react`, `ui`) or style/font/image assets. Spec parsing reports the unsafe import and inclusion reason as a diagnostic.\n- Keep browser rendering in an unpinned file. Move shared server-safe logic into a separate module when both frontend and webhook code need it.",doNotEdit:"## Do Not Edit\n- `.phoenix/` directory — completely regenerated on each build. The one exception is `.phoenix/variants/`, where the `phoenix-variants` skill has you write option files; a rebuild keeps it. The project's TypeScript settings come from `.phoenix/tsconfig.json`; the root `tsconfig.json` is the project's and extends it. `phoenix dev`, `phoenix build` and the webhook bundle do not resolve `paths` aliases, so an alias added to the root file passes `phoenix check` and fails the build; use relative imports\n- `src/locales/` .ts files — compiled from .po files\n- `phoenix` and `phoenix-*` folders under `.claude/skills/` and `.agents/skills/` — Phoenix rewrites its skills on every sync, removes anything else inside their folders and removes any other `phoenix-*` skill folder. To customise a skill, copy it to a folder whose name does not start with `phoenix`",modelVersioning:"## Version the Domain Model Once It Is Deployed\nA deployed model version cannot change, because saved configurations depend on it. `pnpm phoenix deploy` is refused, before anything is uploaded, when the model differs from the one already deployed under the same version. The version is not written anywhere: `model.latest` is always the highest numbered version plus one, so a spec with only `latest` is v1, and `applicationVersion` plays no part. `pnpm phoenix dev` accepts every model change, so a working preview does not show that the deploy will pass.\n\nTo change a model that has been deployed as v<N>:\n1. Keep the deployed model, unchanged, as `model.v<N>` in the spec. A file of its own, such as `spec/model-v<N>.yaml`, keeps `latest` readable.\n2. Make the change in `model.latest`, which is now v<N+1>.\n3. Add `src/migrations/v<N+1>.ts` exporting `migrateToV<N+1>`. It receives the saved entities of v<N> and returns entities that are valid in v<N+1>; `ModelV<N>` and `ModelV<N+1>` are generated global namespaces.\n\nThe admin model is versioned on its own, under `src/admin/migrations/`, with its types in `AdminV<N>`.\n\n**Replacing the starter.** When the first deployment of the workspace was the starter, its `Box` model is v1 in production. When the real model replaces it, keep the starter's model as `model.v1` and define the real model in `latest`. Configurations saved from the starter are not worth converting, so the migration returns the starting state of the new model, the `initialDomainEntities` of its preset, and drops the boxes:\n```typescript\n// src/migrations/v2.ts\nimport type { Migration } from '@salsita-ai/sdk/domain'\n\nexport const migrateToV2: Migration<ModelV1.AnyDomainEntity, ModelV2.AnyDomainEntity> = () => [\n // the entities of the new model's preset\n]\n```\nAn entity of a type the new version no longer has must not stay in the returned state. An entity id starts with its type (`Box-1`), so an entity that keeps its data under a new type name needs a new id and every relation pointing at it rewritten. Read `docs/phoenix/migrations.mdx` before writing any other migration.",sdkDocumentation:'## SDK Documentation\n\nAlways search SDK docs for relevant examples before implementing Phoenix features.\n\n### Commands\n- `pnpm phoenix docs` — Full documentation index with descriptions\n- `pnpm phoenix docs -s <term>` — Search all doc content and metadata (case-insensitive)\n- `pnpm phoenix docs --dir` — Print the absolute docs directory for grep or ripgrep\n- `pnpm phoenix docs -r <path>` — Read specific doc (raw text to stdout)\n- `pnpm phoenix docs -c <category>` — List docs in a category\n- `pnpm phoenix docs -l` — List all doc paths with titles\n\n### Key Docs\n- `docs/phoenix/getting-started.mdx` — Setup, prerequisites, first project\n- `docs/phoenix/quickstart.mdx` — End-to-end tutorial building a configurator\n- `docs/phoenix/domain-model.mdx` — Entities, attributes, relations, specs\n- `docs/phoenix/components.mdx` — ECS pattern, YAML components, custom components\n- `docs/phoenix/project-structure.mdx` — Files, folders, conventions\n- `docs/phoenix/best-practices.mdx` — YAML-first, code patterns, common pitfalls\n- `docs/phoenix/state-management.mdx` — Reactive state, RxJS pipelines, data flow\n- `docs/phoenix/expression-context.mdx` — Expression API, context object, helpers\n- `docs/phoenix/webhooks.mdx` — Server-side finalization workflows\n- `docs/phoenix/finalization.mdx` — Finalization system, PDF/email/CSV generation\n- `docs/phoenix/migrations.mdx` — Schema versioning, migration files\n\n### Agent Workflow\n1. Find relevant docs by content: `pnpm phoenix docs -s \'camera\'`\n2. Read matching doc: `pnpm phoenix docs -r docs/3d-scene/cameras/transitions.mdx`\n3. Search within a doc: `pnpm phoenix docs -r <path> | grep "example"`\n4. Search titles only as a fallback: `pnpm phoenix docs -l | grep "camera"`\n5. Search generated files directly when needed: `grep -rn "camera" "$(pnpm phoenix docs --dir)"`\n6. Browse a topic area: `pnpm phoenix docs -c "Layouts"`',assetInspection:"## 3D Asset Inspection\n\nRun `pnpm phoenix inspect:assets` to discover all glTF/GLB files in `public/` and get a summary table. Use `--file <path>` for detailed single-file inspection.\n\n**When to use:** before writing YAML/code that references objectName, materialsToOverride, morphTargetInfluences, or object positioning.\n\n**What it shows per node:**\n- Name, type (Mesh/Light/Camera/Empty), children\n- Transform: `pos=`, `rot=`, `scale=` (only non-default values shown)\n- Per-node morph targets: `morphs=Width,Height`\n- Bounding box: `bounds=WxHxD` (width x height x depth in meters)\n- Origin placement: `origin=center`, `bottom-center`, `top-center`, `bottom-corner`, etc. with numerical offset\n\n**Use cases:**\n- Find exact node names for `objectName` in scene entity components\n- Find material names for `materialsToOverride`\n- Find morph target names for `morphTargetInfluences`\n- Check model dimensions (bounds) for positioning, dimension indicators, or invariants\n- Check origin placement to understand how models anchor (center vs bottom-center)\n- Use `--json` for programmatic access to all data\n\n**Flags:** `--objects`, `--materials`, `--morphs`, `--animations`, `--json`",screenshotCli:"## Screenshot CLI\n\nCapture 3D canvas screenshots from a running dev server via `pnpm phoenix screenshot`.\n\n**Prerequisites:**\n1. `phoenix dev` must be running (starts the WebSocket server on port 3020)\n2. Configurator must be open in a browser (the browser does the actual rendering)\n\n**Options:**\n- `--output, -o` — Output file path (default: `./screenshot.png`)\n- `--width, -w` — Width in pixels (default: 1920)\n- `--height` — Height in pixels (default: 1080)\n- `--format, -f` — Image format: PNG or JPEG (default: PNG)\n- `--port` — WebSocket server port (default: 3020)\n\n**Example workflow:**\n1. Start dev server: `pnpm phoenix dev`\n2. Open configurator in browser (or via Playwright MCP)\n3. Take screenshot: `pnpm phoenix screenshot -o ./render.png -w 2560 --height 1440`\n\n**Use cases:** visual regression testing, generating product images, debugging 3D renders",inspectState:"## Inspect: State\n\nGet the current editor runtime state as JSON via `pnpm phoenix inspect:state`.\n\n**Prerequisites:** same as screenshot (dev server running + browser open)\n\n**Options:**\n- `--output, -o` — Output file path (prints to stdout if omitted)\n- `--sections, -s` — Comma-separated: entities, navigation, validation (default: all)\n- `--pretty` — Pretty-print JSON (default: true)\n- `--port` — WebSocket server port (default: 3020)\n\n**Example:** `pnpm phoenix inspect:state -o ./state.json -s entities,navigation`\n\n**Lifecycle status:** Always included in response. Shows which systems are ready:\n- `domainLoaded` — domain data loaded\n- `domainContextReady` — domain context initialized (entities available)\n- `configuratorReady` — 3D configurator initialized\n- `sessionAuthenticated` — session service active\n- `editorMode` — editor mode enabled\n\n**Tips:** Use `--sections` to limit payload size. Pipe through `head`/`tail` for partial output (e.g., `pnpm phoenix inspect:state | head -50`).\n\n**Use cases:** AI agent context, debugging entity state, inspecting navigation/validation",inspectData:`## Inspect: Data
7
+ - Underscore folders (_shared/, _utils/) are NOT auto-imported`,exportRules:'## Export Rules\nAll files in src/expressions/, src/actions/, src/invariants/, src/transformers/, src/components/, src/handlers/, src/elements/, src/webhooks/ MUST use default exports. Can be a single item or an array. Named exports are ignored by the SDK auto-import system.\nThe default export must be what the folder\'s phx factory returns (for example `phx.frontend.createCustomPhoenixLayoutElement()` in src/elements/). Any other value, such as a plain React component, stops the application at start with an "Invalid default export in <file>" error. Put plain components and helpers in an underscore folder.',webhookMembership:"## Webhook Membership\n- User-authored components, admin components, and handlers are frontend-only by default.\n- A handler that registers `phx.domain.createAssistantSetupHandler`, `createMapPublishedRevisionDataHandler`, `createMapPublishedChunkedDataHandler`, `createDataRowValidatorHandler`, or `createSheetValidatorHandler` is included in the webhook automatically.\n- An assistant setup handler receives the declaration-shaped whitelist and { role, sessionData, access }. Keep the exact entity/alias split from YAML; inherited fields and expanded member entries are absent. Narrow original declarations, then the SDK expands them. Use project-defined role/session data for per-user customization. Keep the handler synchronous, pure and deterministic. The handler runs in the browser and webhook. Return null to disable assistant execution, or omit whitelist entries to restrict them. Button and panel visibility use spec presence; project developers own dynamic UI filtering through normal layout mechanics such as $filter. Leaves expose optional descriptions; writable options allow only validate: true and allowDryRuns: false; recipe groups allow descriptions and allowDryRuns: false; screenshot views allow descriptions. Deletable entries are {}. Names, component bindings, hints and types are internal. Omitted overrides retain baseline values. Existing entity, enhanced-entity and action types constrain the handler; no separate generated manifest types. Browser GraphQL, tools and change recording use the session-derived manifest; rejected setup fails closed. UI reads the pure spec selector. Omitted server instructions or AI fields preserve the baseline. An empty server instructions string clears it.\n- To include another component or handler in the webhook, put `'use webhook'` as its first statement (leading comments are allowed).\n- A webhook-included file and its runtime local-import closure must not import browser SDK entrypoints (`application`, `admin`, `configurator`, `editor`, `react`, `ui`) or style/font/image assets. Spec parsing reports the unsafe import and inclusion reason as a diagnostic.\n- Keep browser rendering in an unpinned file. Move shared server-safe logic into a separate module when both frontend and webhook code need it.",doNotEdit:"## Do Not Edit\n- `.phoenix/` directory — completely regenerated on each build. The one exception is `.phoenix/variants/`, where the `phoenix-variants` skill has you write option files; a rebuild keeps it. The project's TypeScript settings come from `.phoenix/tsconfig.json`; the root `tsconfig.json` is the project's and extends it. `phoenix dev`, `phoenix build` and the webhook bundle do not resolve `paths` aliases, so an alias added to the root file passes `phoenix check` and fails the build; use relative imports\n- `src/locales/` .ts files — compiled from .po files\n- `phoenix` and `phoenix-*` folders under `.claude/skills/` and `.agents/skills/` — Phoenix rewrites its skills on every sync, removes anything else inside their folders and removes any other `phoenix-*` skill folder. To customise a skill, copy it to a folder whose name does not start with `phoenix`",modelVersioning:"## Version the Domain Model Once It Is Deployed\nA deployed model version cannot change, because saved configurations depend on it. `pnpm phoenix deploy` is refused, before anything is uploaded, when the model differs from the one already deployed under the same version. The version is not written anywhere: `model.latest` is always the highest numbered version plus one, so a spec with only `latest` is v1, and `applicationVersion` plays no part. `pnpm phoenix dev` accepts every model change, so a working preview does not show that the deploy will pass.\n\nTo change a model that has been deployed as v<N>:\n1. Keep the deployed model, unchanged, as `model.v<N>` in the spec. A file of its own, such as `spec/model-v<N>.yaml`, keeps `latest` readable.\n2. Make the change in `model.latest`, which is now v<N+1>.\n3. Add `src/migrations/v<N+1>.ts` exporting `migrateToV<N+1>`. It receives the saved entities of v<N> and returns entities that are valid in v<N+1>; `ModelV<N>` and `ModelV<N+1>` are generated global namespaces.\n\nThe admin model is versioned on its own, under `src/admin/migrations/`, with its types in `AdminV<N>`.\n\n**Replacing the starter.** When the first deployment of the workspace was the starter, its `Box` model is v1 in production. When the real model replaces it, keep the starter's model as `model.v1` and define the real model in `latest`. Configurations saved from the starter are not worth converting, so the migration returns the starting state of the new model, the `initialDomainEntities` of its preset, and drops the boxes:\n```typescript\n// src/migrations/v2.ts\nimport type { Migration } from '@salsita-ai/sdk/domain'\n\nexport const migrateToV2: Migration<ModelV1.AnyDomainEntity, ModelV2.AnyDomainEntity> = () => [\n // the entities of the new model's preset\n]\n```\nAn entity of a type the new version no longer has must not stay in the returned state. An entity id starts with its type (`Box-1`), so an entity that keeps its data under a new type name needs a new id and every relation pointing at it rewritten. Read `docs/phoenix/migrations.mdx` before writing any other migration.",sdkDocumentation:'## SDK Documentation\n\nAlways search SDK docs for relevant examples before implementing Phoenix features.\n\n### Commands\n- `pnpm phoenix docs` — Full documentation index with descriptions\n- `pnpm phoenix docs -s <term>` — Search all doc content and metadata (case-insensitive)\n- `pnpm phoenix docs --dir` — Print the absolute docs directory for grep or ripgrep\n- `pnpm phoenix docs -r <path>` — Read specific doc (raw text to stdout)\n- `pnpm phoenix docs -c <category>` — List docs in a category\n- `pnpm phoenix docs -l` — List all doc paths with titles\n\n### Key Docs\n- `docs/phoenix/getting-started.mdx` — Setup, prerequisites, first project\n- `docs/phoenix/quickstart.mdx` — End-to-end tutorial building a configurator\n- `docs/phoenix/domain-model.mdx` — Entities, attributes, relations, specs\n- `docs/phoenix/components.mdx` — ECS pattern, YAML components, custom components\n- `docs/phoenix/project-structure.mdx` — Files, folders, conventions\n- `docs/phoenix/best-practices.mdx` — YAML-first, code patterns, common pitfalls\n- `docs/phoenix/state-management.mdx` — Reactive state, RxJS pipelines, data flow\n- `docs/phoenix/expression-context.mdx` — Expression API, context object, helpers\n- `docs/phoenix/webhooks.mdx` — Server-side finalization workflows\n- `docs/phoenix/finalization.mdx` — Finalization system, PDF/email/CSV generation\n- `docs/phoenix/migrations.mdx` — Schema versioning, migration files\n\n### Agent Workflow\n1. Find relevant docs by content: `pnpm phoenix docs -s \'camera\'`\n2. Read matching doc: `pnpm phoenix docs -r docs/3d-scene/cameras/transitions.mdx`\n3. Search within a doc: `pnpm phoenix docs -r <path> | grep "example"`\n4. Search titles only as a fallback: `pnpm phoenix docs -l | grep "camera"`\n5. Search generated files directly when needed: `grep -rn "camera" "$(pnpm phoenix docs --dir)"`\n6. Browse a topic area: `pnpm phoenix docs -c "Layouts"`',assetInspection:"## 3D Asset Inspection\n\nRun `pnpm phoenix inspect:assets` to discover all glTF/GLB files in `public/` and get a summary table. Use `--file <path>` for detailed single-file inspection.\n\n**When to use:** before writing YAML/code that references objectName, materialsToOverride, morphTargetInfluences, or object positioning.\n\n**What it shows per node:**\n- Name, type (Mesh/Light/Camera/Empty), children\n- Transform: `pos=`, `rot=`, `scale=` (only non-default values shown)\n- Per-node morph targets: `morphs=Width,Height`\n- Bounding box: `bounds=WxHxD` (width x height x depth in meters)\n- Origin placement: `origin=center`, `bottom-center`, `top-center`, `bottom-corner`, etc. with numerical offset\n\n**Use cases:**\n- Find exact node names for `objectName` in scene entity components\n- Find material names for `materialsToOverride`\n- Find morph target names for `morphTargetInfluences`\n- Check model dimensions (bounds) for positioning, dimension indicators, or invariants\n- Check origin placement to understand how models anchor (center vs bottom-center)\n- Use `--json` for programmatic access to all data\n\n**Flags:** `--objects`, `--materials`, `--morphs`, `--animations`, `--json`",screenshotCli:"## Screenshot CLI\n\nCapture 3D canvas screenshots from a running dev server via `pnpm phoenix screenshot`.\n\n**Prerequisites:**\n1. `phoenix dev` must be running (starts the WebSocket server on port 3020)\n2. Configurator must be open in a browser (the browser does the actual rendering)\n\n**Options:**\n- `--output, -o` — Output file path (default: `./screenshot.png`)\n- `--width, -w` — Width in pixels (default: 1920)\n- `--height` — Height in pixels (default: 1080)\n- `--format, -f` — Image format: PNG or JPEG (default: PNG)\n- `--port` — WebSocket server port (default: 3020)\n\n**Example workflow:**\n1. Start dev server: `pnpm phoenix dev`\n2. Open configurator in browser (or via Playwright MCP)\n3. Take screenshot: `pnpm phoenix screenshot -o ./render.png -w 2560 --height 1440`\n\n**Use cases:** visual regression testing, generating product images, debugging 3D renders",inspectState:"## Inspect: State\n\nGet the current editor runtime state as JSON via `pnpm phoenix inspect:state`.\n\n**Prerequisites:** same as screenshot (dev server running + browser open)\n\n**Options:**\n- `--output, -o` — Output file path (prints to stdout if omitted)\n- `--sections, -s` — Comma-separated: entities, navigation, validation (default: all)\n- `--pretty` — Pretty-print JSON (default: true)\n- `--port` — WebSocket server port (default: 3020)\n\n**Example:** `pnpm phoenix inspect:state -o ./state.json -s entities,navigation`\n\n**Lifecycle status:** Always included in response. Shows which systems are ready:\n- `domainLoaded` — domain data loaded\n- `domainContextReady` — domain context initialized (entities available)\n- `configuratorReady` — 3D configurator initialized\n- `sessionAuthenticated` — session service active\n- `editorMode` — editor mode enabled\n\n**Tips:** Use `--sections` to limit payload size. Pipe through `head`/`tail` for partial output (e.g., `pnpm phoenix inspect:state | head -50`).\n\n**Use cases:** AI agent context, debugging entity state, inspecting navigation/validation",inspectData:`## Inspect: Data
8
8
 
9
9
  Get the current editor context data as JSON via \`pnpm phoenix inspect:data\`.
10
10
 
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { type AssistantAiSettings } from '@salsita-ai/contracts/assistant';
3
- import { ASSISTANT_HINTS } from '#package/domain';
3
+ import { ASSISTANT_HINTS, type AssistantFormAttribute, type AssistantFormRole } from '#package/domain';
4
4
  /**
5
5
  * One whitelisted field in the assistant manifest. `description` overrides the JSON Schema
6
6
  * description of the shape property; `as` exposes the field under a different GraphQL name; `lenses`
@@ -12,6 +12,21 @@ export interface RawAssistantField {
12
12
  readonly as?: string;
13
13
  readonly lenses?: readonly string[];
14
14
  }
15
+ /**
16
+ * One of the project's own elements of an assistant form, keyed under `assistant.formElements` by
17
+ * its tag, and implemented by the component a file in `src/elements/` registers under the same name
18
+ * with `phx.frontend.createAssistantFormElement`. `role` decides the props the component gets, how
19
+ * the form collects it into the answer and where the prompt lists it, with `description` after its
20
+ * signature. `attributes` are flat scalars beside the built-in `label`, each a JSON Schema of the
21
+ * keywords `AssistantFormAttribute` allows, and `required` names those the model must write. An
22
+ * option or an input that takes a `value` declares it like any other attribute.
23
+ */
24
+ export interface RawAssistantFormElement {
25
+ readonly description: string;
26
+ readonly role: AssistantFormRole;
27
+ readonly attributes?: Readonly<Record<string, AssistantFormAttribute>>;
28
+ readonly required?: readonly string[];
29
+ }
15
30
  /**
16
31
  * One lens of the assistant `lenses` map, keyed by the name the agent spreads it by: a named
17
32
  * selection made of the `entities` entries tagged with that name. `description` tells the agent when
@@ -90,6 +105,16 @@ export interface RawAssistantAction {
90
105
  readonly description?: string;
91
106
  readonly as?: string;
92
107
  }
108
+ /**
109
+ * One view of the assistant `screenshots` map, keyed by the instance name of an `agentScreenshot`
110
+ * component: it lets the agent take a screenshot of the entity from that view under `meta`.
111
+ * `description` tells the agent what the view shows; `as` is the name the agent asks for the view
112
+ * by, the instance name by default.
113
+ */
114
+ export interface RawAssistantScreenshot {
115
+ readonly description: string;
116
+ readonly as?: string;
117
+ }
93
118
  /**
94
119
  * Experimental assistant manifest authored under the `assistant` root key. `entities` whitelists the
95
120
  * stored fields the generated GraphQL schema exposes, keyed by entity type or alias name and then by
@@ -100,8 +125,11 @@ export interface RawAssistantAction {
100
125
  * entity type or alias, the `childEntityRecipes` groups whose recipes the agent may list and add
101
126
  * with `addChild`. `replaceRecipes` names, per entity type or alias, the `replaceEntityRecipes`
102
127
  * groups whose recipes the agent may list and replace the entity by with `replaceEntity`. `actions`
103
- * names the declared actions the agent may run as custom mutations. Nothing is exposed without an
104
- * entry. `lenses` declares the named selections that `entities` entries are tagged with.
128
+ * names the declared actions the agent may run as custom mutations. `screenshots` names, per entity
129
+ * type or alias, the `agentScreenshot` views the agent may take a screenshot of the entity from.
130
+ * Nothing is exposed without an entry. `lenses` declares the named selections that `entities`
131
+ * entries are tagged with, and `formElements` the project's own elements of the forms the agent
132
+ * asks the user with.
105
133
  *
106
134
  * `ai` and `instructions` are for the server only: the provider, model tier and reasoning level the
107
135
  * deployment's assistant runs with, and the integrator's own guidance for it, appended to the system
@@ -114,7 +142,9 @@ export interface RawAssistant {
114
142
  readonly childRecipes?: Readonly<Record<string, RawAssistantRecipes>>;
115
143
  readonly replaceRecipes?: Readonly<Record<string, RawAssistantRecipes>>;
116
144
  readonly actions?: Readonly<Record<string, RawAssistantAction | null>>;
145
+ readonly screenshots?: Readonly<Record<string, Readonly<Record<string, RawAssistantScreenshot>>>>;
117
146
  readonly lenses?: Readonly<Record<string, RawAssistantLens>>;
147
+ readonly formElements?: Readonly<Record<string, RawAssistantFormElement>>;
118
148
  readonly instructions?: string;
119
149
  readonly ai: AssistantAiSettings;
120
150
  }
@@ -1 +1 @@
1
- import{z as e}from"zod";import{ASSISTANT_AI_PROVIDERS as t,ASSISTANT_MODEL_TIERS as o,ASSISTANT_REASONING_LEVELS as i}from"@salsita-ai/contracts/assistant";import{ASSISTANT_HINTS as n}from"#package/domain";export const ASSISTANT_OPTION_COMPONENT_TYPES=["materialSwatch","attributeSelection","distanceDimensionSelect"];let r=e.string().regex(/^(?!__)[_A-Za-z][_0-9A-Za-z]*$/,'must be a GraphQL name: letters, digits and "_", not starting with a digit or "__"'),s=t=>e.enum(t,{errorMap:()=>({message:`must be one of: ${t.join(", ")}`})}),a=e.object({description:e.string().optional(),as:r.optional(),lenses:e.array(e.string()).optional()}).strict(),l="must be a whole number of at least 1; remove the key to nest tagged relations one level",c=e.object({description:e.string().trim().min(1,"must not be empty; say when the agent should use the lens"),maxDepth:e.number({invalid_type_error:l}).int(l).min(1,l).optional()}).strict(),p=r.refine(e=>!e.includes("__"),'must not contain "__", which names the generated copies of a lens'),m=e.array(s(n)),d=e.object({options:e.object({componentType:s(ASSISTANT_OPTION_COMPONENT_TYPES),instanceName:e.string().min(1,"must not be empty; name the component instance that holds the choices"),as:r.optional(),validate:e.boolean().optional(),allowDryRuns:e.boolean().optional(),hints:m.optional()}).strict().optional()}).strict(),u=e.object({as:e.string().trim().min(1,"must not be empty; remove the key to use the instance name").optional(),description:e.string().optional(),allowDryRuns:e.boolean().optional()}).strict(),h=t=>e.object({hints:m.optional(),groups:e.record(e.string(),u.nullable(),{required_error:`is required: the recipe groups of the type go under "groups", keyed by the instance name of a "${t}" component`})}).strict(),b=e.object({description:e.string().optional(),as:r.optional()}).strict(),g=`is required: declare provider (${t.join(", ")}), model (${o.join(", ")}) and reasoning (${i.join(", ")})`,_="must be a positive whole number of input tokens; remove the key to use the default of the server",y=e.object({provider:s(t),model:s(o),reasoning:s(i),maxThreadSize:e.number({invalid_type_error:_}).int(_).positive(_).optional()},{required_error:g,invalid_type_error:g}).strict();export const rawAssistantSchema=e.object({entities:e.record(e.string(),e.record(e.string(),a.nullable())).optional(),deletable:e.record(e.string(),e.object({}).strict().nullable()).optional(),writable:e.record(e.string(),e.record(e.string(),d.nullable())).optional(),childRecipes:e.record(e.string(),h("childEntityRecipes")).optional(),replaceRecipes:e.record(e.string(),h("replaceEntityRecipes")).optional(),actions:e.record(e.string(),b.nullable()).optional(),lenses:e.record(p,c).optional(),instructions:e.string().trim().min(1,"must not be empty; remove the key when there are no instructions").optional(),ai:y}).strict();
1
+ import{z as e}from"zod";import{ASSISTANT_AI_PROVIDERS as t,ASSISTANT_MODEL_TIERS as n,ASSISTANT_REASONING_LEVELS as o}from"@salsita-ai/contracts/assistant";import{ASSISTANT_FORM_NAME_PATTERN as i,ASSISTANT_HINTS as r}from"#package/domain";export const ASSISTANT_OPTION_COMPONENT_TYPES=["materialSwatch","attributeSelection","distanceDimensionSelect"];let s=e.string().regex(/^(?!__)[_A-Za-z][_0-9A-Za-z]*$/,'must be a GraphQL name: letters, digits and "_", not starting with a digit or "__"'),a=t=>e.enum(t,{errorMap:()=>({message:`must be one of: ${t.join(", ")}`})}),m=e.object({description:e.string().optional(),as:s.optional(),lenses:e.array(e.string()).optional()}).strict(),l="must be a whole number of at least 1; remove the key to nest tagged relations one level",p=e.object({description:e.string().trim().min(1,"must not be empty; say when the agent should use the lens"),maxDepth:e.number({invalid_type_error:l}).int(l).min(1,l).optional()}).strict(),c=s.refine(e=>!e.includes("__"),'must not contain "__", which names the generated copies of a lens'),u=e.array(a(r)),d=e.object({options:e.object({componentType:a(ASSISTANT_OPTION_COMPONENT_TYPES),instanceName:e.string().min(1,"must not be empty; name the component instance that holds the choices"),as:s.optional(),validate:e.boolean().optional(),allowDryRuns:e.boolean().optional(),hints:u.optional()}).strict().optional()}).strict(),g=e.object({as:e.string().trim().min(1,"must not be empty; remove the key to use the instance name").optional(),description:e.string().optional(),allowDryRuns:e.boolean().optional()}).strict(),h=t=>e.object({hints:u.optional(),groups:e.record(e.string(),g.nullable(),{required_error:`is required: the recipe groups of the type go under "groups", keyed by the instance name of a "${t}" component`})}).strict(),b=e.object({description:e.string().optional(),as:s.optional()}).strict(),y=["enum","pattern","minimum","maximum","minLength","maxLength"],f=["enum","pattern","minLength","maxLength"],_=["enum","minimum","maximum"],v=e.object({type:a(["string","number","integer","boolean"]),enum:e.array(e.union([e.string(),e.number()])).min(1,"must list at least one value").optional(),pattern:e.string().refine(e=>{try{return RegExp(e,"u"),!0}catch{return!1}},"must be a regular expression, as in JSON Schema").optional(),minimum:e.number().optional(),maximum:e.number().optional(),minLength:e.number().int().nonnegative().optional(),maxLength:e.number().int().nonnegative().optional()}).strict().superRefine((t,n)=>{let o="string"===t.type?f:"boolean"===t.type?[]:_;for(let i of y)void 0===t[i]||o.includes(i)||n.addIssue({code:e.ZodIssueCode.custom,path:[i],message:`does not apply to an attribute of type "${t.type}"; remove it or change the type`});let i="string"===t.type?"string":"number";for(let[o,r]of("boolean"!==t.type&&t.enum?.some(e=>typeof e!==i||"integer"===t.type&&!Number.isInteger(e))&&n.addIssue({code:e.ZodIssueCode.custom,path:["enum"],message:`must list only ${"integer"===t.type?"whole number":i}s, since the attribute is of type "${t.type}"`}),[["minimum","maximum"],["minLength","maxLength"]])){let[i,s]=[t[o],t[r]];void 0!==i&&void 0!==s&&i>s&&n.addIssue({code:e.ZodIssueCode.custom,path:[o],message:`must not be greater than "${r}", or no value could satisfy the attribute`})}}),w=e.string().regex(i,'must start with a lowercase letter and hold only lowercase letters, digits and "-", since the panel reads form markup in lowercase'),j=e.object({description:e.string().trim().min(1,"must not be empty; say what the element is for and where the agent may use it"),role:a(["group","choice","choices","option","input"]),attributes:e.record(w,v).optional(),required:e.array(e.string()).optional()}).strict(),x=e.object({description:e.string().trim().min(1,"must not be empty; say what the view shows"),as:s.optional()}).strict(),S=`is required: declare provider (${t.join(", ")}), model (${n.join(", ")}) and reasoning (${o.join(", ")})`,T="must be a positive whole number of input tokens; remove the key to use the default of the server",I=e.object({provider:a(t),model:a(n),reasoning:a(o),maxThreadSize:e.number({invalid_type_error:T}).int(T).positive(T).optional()},{required_error:S,invalid_type_error:S}).strict();export const rawAssistantSchema=e.object({entities:e.record(e.string(),e.record(e.string(),m.nullable())).optional(),deletable:e.record(e.string(),e.object({}).strict().nullable()).optional(),writable:e.record(e.string(),e.record(e.string(),d.nullable())).optional(),childRecipes:e.record(e.string(),h("childEntityRecipes")).optional(),replaceRecipes:e.record(e.string(),h("replaceEntityRecipes")).optional(),actions:e.record(e.string(),b.nullable()).optional(),screenshots:e.record(e.string(),e.record(e.string(),x)).optional(),lenses:e.record(c,p).optional(),formElements:e.record(w,j).optional(),instructions:e.string().trim().min(1,"must not be empty; remove the key when there are no instructions").optional(),ai:I}).strict();
@@ -0,0 +1,9 @@
1
+ import { type AssistantFormElementDescriptor } from '#package/domain';
2
+ import type { ParseContext } from '../../../../core/index.js';
3
+ import type { RawAssistantFormElement } from '../../../raw-spec/index.js';
4
+ /**
5
+ * Checks the project's own form elements and turns them into the descriptors the built-ins use, in
6
+ * YAML order. Whether each has an implementation is not known here, since module analysis does not
7
+ * see the name a file registers; the application checks that pairing when it starts.
8
+ */
9
+ export declare const resolveFormElements: (formElements: Readonly<Record<string, RawAssistantFormElement>>, ctx: ParseContext) => readonly AssistantFormElementDescriptor[];
@@ -0,0 +1 @@
1
+ import{ASSISTANT_BUILT_IN_FORM_ELEMENTS as e,isHtmlElementName as r}from"#package/domain";import{DiagnosticCode as E,diagnosticMessages as _}from"../../../../errors/index.js";let T=e.map(({name:e})=>e),t=new Set(["label"]);export const resolveFormElements=(e,N)=>Object.entries(e).map(([e,S])=>{let A=N.at(e);return T.includes(e)?A.error(E.ASSISTANT_FORM_ELEMENT_BUILT_IN_NAME,_.ASSISTANT_FORM_ELEMENT_BUILT_IN_NAME(e,T)):r(e)&&A.error(E.ASSISTANT_FORM_ELEMENT_HTML_NAME,_.ASSISTANT_FORM_ELEMENT_HTML_NAME(e)),((e,{attributes:r={},required:T=[]},N)=>{let S=Object.keys(r);for(let r of S)t.has(r)&&N.at("attributes",r).error(E.ASSISTANT_FORM_ATTRIBUTE_RESERVED_NAME,_.ASSISTANT_FORM_ATTRIBUTE_RESERVED_NAME(e,r));for(let t of T)Object.hasOwn(r,t)||N.at("required").error(E.ASSISTANT_FORM_ELEMENT_UNKNOWN_REQUIRED,_.ASSISTANT_FORM_ELEMENT_UNKNOWN_REQUIRED(e,t,S))})(e,S,A),{name:e,role:S.role,description:S.description,attributes:S.attributes??{},...void 0===S.required?{}:{required:S.required}}});
@@ -29,7 +29,10 @@ export type AssistantSchema = Required<AssistantManifest> & {
29
29
  /** What the assistant schema is built from, besides the manifest. */
30
30
  export interface AssistantSchemaInput {
31
31
  readonly model: ModelVersion;
32
- readonly states: AssistantStates;
32
+ /** The declared UI states; the export states only check the screenshot views, the schema never shows them. */
33
+ readonly states: AssistantStates & {
34
+ readonly exportStates: readonly string[];
35
+ };
33
36
  /** Resolves `phx:` refs, so a field's nullability is read through phoenix definitions. */
34
37
  readonly phxDefinitions: PhoenixDefinitions;
35
38
  /** The parsed expressions, which `$` manifest entries whitelist by their declared schema. */
@@ -1 +1 @@
1
- import{isArray as e}from"@salsita-ai/common/utils";import{ASSISTANT_HINTS as t,AssistantFieldClashError as i,buildAssistantManifest as r,describeAssistantValues as n,mergeAssistantFields as s,resolveAssistantField as o}from"#package/domain";import{DiagnosticCode as a,diagnosticMessages as l}from"../../../../errors/index.js";import{createSchemaRefResolver as S,isNullableSchema as p,phxDefinitionRefName as N}from"../../common/index.js";import{resolveAssistantFieldType as T}from"./assistant-field-type.js";let c="AnyDomainEntity",d="RecipeGroup",f="Recipe",A="DryRun",E="FieldOption",m={section:"childRecipes",component:"childEntityRecipes",mutation:"addChild"},_={section:"replaceRecipes",component:"replaceEntityRecipes",mutation:"replaceEntity"},u=new Set(["id","type","meta"]),O=new Set(["Query","Mutation","Navigation",d,f,A,"DryRunError",E,"JSON","String","Int","Float","Boolean","ID"]),I=new Set([d,f,E,A]),y=new Set(["id","type"]),b="A string, a message descriptor or a multilingual object; passed without translation.",h=(e,t)=>Object.entries(e).find(([e])=>e===t)?.[1],g=(e,{property:t,writableKey:i},r,{resolveRef:s,exposed:o,sources:S},p)=>{let N=Object.entries(h(S,e)??{}),c=o.get(e)?.find(({source:e})=>e===t)?.name??N.find(([,{source:e}])=>e===t)?.[0],d=void 0===c?N.find(([e])=>e===t)?.[1]:void 0;if(d)return void p.at(i,t).error(a.ASSISTANT_FIELD_NAME_CONFLICT,l.ASSISTANT_FIELD_NAME_CONFLICT(e,t,[d.source,t],`${i}.${t}`));let{type:f,enumValues:A,localized:E,description:m}=T(r,s),_={...f,nonNull:!1},u=c??t,O=void 0===c?n(m,A):void 0;return E?{name:u,property:t,type:{kind:"scalar",name:"JSON",nonNull:!1},description:O?`${O} ${b}`:b}:{name:u,property:t,type:_,...void 0===O?{}:{description:O}}},v=(e,t,{model:i,components:r})=>{let n=new Set([e,...[...i.aliases].filter(([,t])=>t.includes(e)).map(([e])=>e)]);return r.filter(({componentName:e,typeOrAlias:i})=>e===t&&n.has(i)).map(({instanceName:e})=>String(e))},R=(...e)=>t.filter(t=>e.some(e=>e?.includes(t))),j=(e,t,i,r)=>{let{model:n}=i,s=new Map,o=new Map;for(let[S,{groups:p,hints:N}]of Object.entries(e)){let e=n.entityTypes.find(e=>e===S),T=e?[e]:[...n.aliases].find(([e])=>e===S)?.[1];if(!T){r.at(S).error(a.ASSISTANT_UNKNOWN_TYPE,l.ASSISTANT_UNKNOWN_TYPE(t.section,S));continue}for(let[e,n]of(T.forEach(e=>o.set(e,R(o.get(e),N))),Object.entries(p))){let o=r.at(S,"groups",e),p=`${S}.groups.${e}`,N={groupId:n?.as??e,instanceName:e,...n?.description===void 0?{}:{description:n.description},dryRuns:n?.allowDryRuns??!0};for(let r of T){let n=v(r,t.component,i),T=s.get(r)??[],c=T.find(e=>e.group.groupId===N.groupId);n.includes(e)?c?o.error(a.ASSISTANT_DUPLICATE_RECIPE_GROUP,l.ASSISTANT_DUPLICATE_RECIPE_GROUP(t,r,N.groupId,[c.entry,p])):s.set(r,[...T,{group:N,entry:p,entryCtx:o}]):o.error(a.ASSISTANT_UNKNOWN_RECIPE_GROUP,l.ASSISTANT_UNKNOWN_RECIPE_GROUP(t,S,e,r,n))}}}let S=n.entityTypes.flatMap(e=>{let t=s.get(e);return t?[{entityType:e,groups:t}]:[]});return{groups:Object.fromEntries(S.map(({entityType:e,groups:t})=>[e,t.map(({group:e})=>e)])),hints:o,metaEntries:new Map(S.flatMap(({entityType:e,groups:[t]})=>t?[[e,t.entryCtx]]:[]))}},M=e=>`${e}Meta`,D=e=>`${e.charAt(0).toUpperCase()+e.slice(1)}Result`,C=e=>{let t=(({anyOf:e,oneOf:t})=>[...e??[],...t??[]].filter(e=>"null"!==e.type))(e);return e.$ref||1!==t.length?e:t[0]},P=(e,t)=>{let{type:i,enumValues:r,localized:s,description:o}=T(e,t),a={...i,nonNull:!1},l=s?`${n(o,r)??""} ${b}`.trim():n(o,r);return{type:{...s?{kind:"scalar",name:"JSON",nonNull:!1}:a,nonNull:i.nonNull},...void 0===l?{}:{description:l}}},L=(t,i)=>{let r=(t=>{let i=C(t),r=i?.type==="array"&&i.items&&!e(i.items)?C(i.items):void 0,n=r??i;if(n?.$ref!==void 0&&"$DomainEntity"===N(n.$ref))return{target:n.references??c,list:void 0!==r}})(t);if(r)return{kind:"edge",...r,nullable:p(t,i),...void 0===t.description?{}:{description:t.description}};let{type:s,enumValues:o,localized:a,description:l}=T(t,i),S=n(l,o);return a?{kind:"value",type:{kind:"scalar",name:"JSON",nonNull:s.nonNull},description:`${l??""} ${b}`.trim()}:{kind:"value",type:s,...void 0===S?{}:{description:S}}},U=(e,t)=>{let{type:i,enumValues:r,localized:n,description:s}=T(e,t);return{type:{kind:"value",type:i},localized:!0===n,description:s,enumValues:r}},w=(e,t,i,r,s)=>{let o=t.startsWith("$")?((e,t,i,r)=>{let n=((e,t,{expressions:i,entityTypes:r})=>{let n=r.find(t=>t===e);for(let r of void 0===n?[e]:i.entityExpressions.get(n)?.shapeGroups??[]){let e=[...i.expressions.get(r)?.values()??[]].find(e=>e.name===t);if(e?.declaredSchema)return{implementation:e.implementation,declaredSchema:e.declaredSchema}}})(e.name,t,i);if(!n)return void r.error(a.ASSISTANT_UNKNOWN_FIELD,l.ASSISTANT_UNKNOWN_FIELD("entities",e.name,t));let{implementation:s}=n;if("violations"===s.kind)return void r.error(a.ASSISTANT_UNSUPPORTED_EXPRESSION,l.ASSISTANT_UNSUPPORTED_EXPRESSION(e.name,t));if("relation"===s.kind){let[{target:e,cardinality:t}]=s.params;return{type:{kind:"edge",target:e,cardinality:t},localized:!1,description:void 0}}return U(n.declaredSchema,i.resolveRef)})(e,t,r,s):((e,t,{resolveRef:i},r)=>{let n=Object.entries(e.shape.properties).find(([e])=>e===t)?.[1];return n?U(n,i):void r.error(a.ASSISTANT_UNKNOWN_FIELD,l.ASSISTANT_UNKNOWN_FIELD("entities",e.name,t))})(e,t,r,s);if(!o)return;let S=i?.as??(t.startsWith("$")?`_${t.slice(1)}`:t);return u.has(S)?void s.error(a.ASSISTANT_RESERVED_FIELD_NAME,l.ASSISTANT_RESERVED_FIELD_NAME(e.name,t,S)):{name:S,source:t,type:o.type,localized:o.localized,description:n(i?.description??o.description,o.enumValues),describedByManifest:i?.description!==void 0,enumValues:o.enumValues,valueDescription:i?.description??o.description,...i?.lenses?.length?{lenses:[...new Set(i.lenses)]}:{}}},$=(e,t,i,r,n)=>s(t,i,r,(t,i,r)=>{n.error(a.ASSISTANT_FIELD_NAME_CONFLICT,l.ASSISTANT_FIELD_NAME_CONFLICT(e,t,[i,r]))});export const resolveAssistantManifest=(e,{model:t,states:i,phxDefinitions:r,expressions:n,components:s,actions:p},N)=>{let d=N.errors.length,f=N.at("entities"),A=S(t.commonTypes,r);for(let e of t.resolvedShapes.keys())O.has(e)&&N.error(a.ASSISTANT_RESERVED_TYPE_NAME,l.ASSISTANT_RESERVED_TYPE_NAME(e,[...O]));let E=((e,t,i)=>{let r=new Map;for(let n of e){let e=[];for(let[r,s]of Object.entries(n.entries)){let o=w(n,r,s,t,i.at(n.name,r));o&&e.push(o)}r.set(n.name,e)}return r})(((e,t,i)=>{let r=[];for(let[i,n]of t.resolvedShapes){let t=e[i];t&&r.push({name:i,shape:n,entries:t})}let n=new Set(t.resolvedShapes.keys());for(let t of Object.keys(e))n.has(t)||i.at(t).error(a.ASSISTANT_UNKNOWN_TYPE,l.ASSISTANT_UNKNOWN_TYPE("entities",t));return r})(e.entities??{},t,f),{resolveRef:A,expressions:n,entityTypes:t.entityTypes},f),u=$(c,[],E.get(c)??[],()=>void 0,N),C=new Map([[c,u]]);for(let e of t.aliases.keys())e!==c&&C.set(e,$(e,[u],E.get(e)??[],()=>void 0,N));for(let e of t.entityTypes){let i=[c,...[...t.aliases].filter(([t,i])=>t!==c&&i.includes(e)).map(([e])=>e)],r=[...t.resolvedShapes].find(([t])=>t===e)?.[1];C.set(e,$(e,i.map(e=>C.get(e)??[]),E.get(e)??[],e=>{let t=Object.entries(r?.properties??{}).find(([t])=>t===e)?.[1];return t&&T(t,A).enumValues},N))}let U=Object.fromEntries(t.entityTypes.map(e=>[e,Object.fromEntries((C.get(e)??[]).map(({name:e,source:t,localized:i})=>[e,i?{source:t,localized:!0}:{source:t}]))])),F=j(e.childRecipes??{},m,{model:t,components:s},N.at("childRecipes")),W=j(e.replaceRecipes??{},_,{model:t,components:s},N.at("replaceRecipes")),k=((e,t,i)=>{let r=new Set;for(let n of Object.keys(e)){let e=t.entityTypes.find(e=>e===n),s=e?[e]:[...t.aliases].find(([e])=>e===n)?.[1];s?s.forEach(e=>r.add(e)):i.at(n).error(a.ASSISTANT_UNKNOWN_TYPE,l.ASSISTANT_UNKNOWN_TYPE("deletable",n))}return t.entityTypes.filter(e=>r.has(e))})(e.deletable??{},t,N.at("deletable")),K=((e,t,i)=>{let{model:r}=t,n=((e,t,i)=>{let r=new Map;for(let[n,s]of Object.entries(e)){let e=t.entityTypes.find(e=>e===n),o=e?[e]:[...t.aliases].find(([e])=>e===n)?.[1],S=[...t.resolvedShapes].find(([e])=>e===n)?.[1];if(!o||!S){i.at(n).error(a.ASSISTANT_UNKNOWN_TYPE,l.ASSISTANT_UNKNOWN_TYPE("writable",n));continue}for(let e of Object.keys(s)){let t=i.at(n,e);e.startsWith("$")||y.has(e)?t.error(a.ASSISTANT_NOT_WRITABLE,l.ASSISTANT_NOT_WRITABLE(n,e)):h(S.properties,e)?o.forEach(t=>{let i=r.get(t)??new Map;r.set(t,i.has(e)?i:i.set(e,n))}):t.error(a.ASSISTANT_UNKNOWN_FIELD,l.ASSISTANT_UNKNOWN_FIELD("writable",n,e))}}return r})(e,r,i);return r.entityTypes.flatMap(e=>{let s=[...r.resolvedShapes].find(([t])=>t===e)?.[1],o=[...n.get(e)??[]].flatMap(([r,n])=>{let o=s&&h(s.properties,r);return(o&&g(e,{property:r,writableKey:n},o,t,i))??[]});return o.length>0?[{entityType:e,fields:o}]:[]})})(e.writable??{},{model:t,resolveRef:A,exposed:E,sources:U},N.at("writable")),V=((e,t,i,r)=>{let{model:n}=i,s=new Map;for(let[o,S]of Object.entries(e)){let e=n.entityTypes.find(e=>e===o),p=e?[e]:[...n.aliases].find(([e])=>e===o)?.[1]??[];for(let[e,n]of Object.entries(S)){let S=n?.options;if(!S)continue;let N=r.at(o,e,"options"),T=`${o}.${e}`;for(let r of p){let n=t.find(e=>e.entityType===r)?.fields.find(t=>t.property===e);if(!n)continue;let o=S.as??`${n.name}Options`,p=v(r,S.componentType,i),c=s.get(r)??[],d=c.find(e=>e.options.edgeName===o||e.options.field===n.name);if(p.includes(S.instanceName))if(o===m.section||o===_.section)N.error(a.ASSISTANT_RESERVED_OPTIONS_EDGE_NAME,l.ASSISTANT_RESERVED_OPTIONS_EDGE_NAME(T,o,n.name));else if(d)N.error(a.ASSISTANT_DUPLICATE_OPTIONS_EDGE,l.ASSISTANT_DUPLICATE_OPTIONS_EDGE(r,d.options.edgeName===o?{edgeName:o}:{field:n.name},[d.entry,T]));else{let e={field:n.name,edgeName:o,componentType:S.componentType,instanceName:S.instanceName,validate:S.validate??!0,dryRuns:S.allowDryRuns??!0};s.set(r,[...c,{options:e,hints:R(S.hints),entry:T,entryCtx:N}])}else N.error(a.ASSISTANT_UNKNOWN_OPTIONS_COMPONENT,l.ASSISTANT_UNKNOWN_OPTIONS_COMPONENT(T,S,r,p))}}}return s})(e.writable??{},K,{model:t,components:s},N.at("writable"));var Y=[F.metaEntries,W.metaEntries,new Map([...V].flatMap(([e,[t]])=>t?[[e,t.entryCtx]]:[]))];for(let e of t.entityTypes){let i=Y.flatMap(t=>t.get(e)??[])[0],r=M(e);i&&[...t.resolvedShapes.keys()].some(e=>e===r)&&i.error(a.ASSISTANT_META_TYPE_NAME_CONFLICT,l.ASSISTANT_META_TYPE_NAME_CONFLICT(e,r))}let x=t.entityTypes.filter(e=>(F.groups[e]?.length??0)>0||(W.groups[e]?.length??0)>0||(V.get(e)?.length??0)>0).map(M),G=((e,{actions:t,resolveRef:i,builtInMutations:r,typeNames:n},s)=>{let o=[],S=[...t.values()].map(e=>String(e.name));for(let[p,N]of Object.entries(e)){let e=s.at(p),T=[...t.values()].find(e=>e.name===p);if(!T){e.error(a.ASSISTANT_UNKNOWN_ACTION,l.ASSISTANT_UNKNOWN_ACTION(p,S));continue}let c=N?.as??p,d=o.find(e=>e.fieldName===c),f=D(c);if(r.has(c)||d){e.error(a.ASSISTANT_MUTATION_NAME_CONFLICT,l.ASSISTANT_MUTATION_NAME_CONFLICT(p,c,d?`"assistant.actions.${d.actionName}" already exposes`:"the built-in mutation of that name already takes"));continue}if(n.has(f)||o.some(e=>D(e.fieldName)===f)){e.error(a.ASSISTANT_ACTION_RESULT_TYPE_CONFLICT,l.ASSISTANT_ACTION_RESULT_TYPE_CONFLICT(p,c,f));continue}let A=N?.description??T.description;o.push({fieldName:c,actionName:p,...void 0===A?{}:{description:A},args:Object.fromEntries(Object.entries(T.params).map(([e,{schema:t}])=>[e,P(t,i)])),result:Object.fromEntries(Object.entries(T.result).map(([e,t])=>[e,L(t,i)]))})}return o})(e.actions??{},{actions:p,resolveRef:A,builtInMutations:new Set(["deleteEntity",m.mutation,_.mutation,...K.map(({entityType:e})=>`modify${e}`)]),typeNames:new Set([...O,...t.resolvedShapes.keys(),...x])},N.at("actions")),z=((e,t)=>{let i=e.lenses??{},r=new Set;for(let[n,s]of Object.entries(e.entities??{}))for(let[e,o]of Object.entries(s))for(let s of o?.lenses??[])Object.hasOwn(i,s)?r.add(s):t.at("entities",n,e,"lenses").error(a.ASSISTANT_UNKNOWN_LENS,l.ASSISTANT_UNKNOWN_LENS(`${n}.${e}`,s,Object.keys(i)));for(let e of Object.keys(i))I.has(e)?t.at("lenses",e).error(a.ASSISTANT_RESERVED_LENS_NAME,l.ASSISTANT_RESERVED_LENS_NAME(e,[...I])):r.has(e)||t.at("lenses",e).error(a.ASSISTANT_EMPTY_LENS,l.ASSISTANT_EMPTY_LENS(e));return Object.fromEntries(Object.entries(i).map(([e,{description:t,maxDepth:i}])=>[e,{description:t,maxDepth:i??1}]))})(e,N);if(N.errors.length>d)return;let B=e=>Object.fromEntries(Object.entries(e).map(([e,t])=>[e,{hints:R(t.hints),groups:Object.entries(t.groups).map(([e,t])=>({instanceName:e,groupId:t?.as??e,...t?.description===void 0?{}:{description:t.description},dryRuns:t?.allowDryRuns??!0}))}]));return{declarations:{entities:Object.fromEntries(Object.keys(e.entities??{}).map(e=>[e,(E.get(e)??[]).map(e=>({name:e.name,source:e.source,type:e.type,localized:e.localized,describedByManifest:e.describedByManifest,...void 0===e.description?{}:{description:e.description},...void 0===e.valueDescription?{}:{valueDescription:e.valueDescription},...void 0===e.enumValues?{}:{enumValues:e.enumValues},...void 0===e.lenses?{}:{lenses:e.lenses}}))])),enumValues:Object.fromEntries(t.entityTypes.map(e=>[e,Object.fromEntries(Object.entries(t.resolvedShapes.get(e)?.properties??{}).flatMap(([e,t])=>{let i=T(t,A).enumValues;return i?[[e,i]]:[]}))])),deletable:Object.keys(e.deletable??{}),writable:Object.fromEntries(Object.entries(e.writable??{}).map(([e,i])=>[e,Object.fromEntries(Object.entries(i).map(([i,r])=>[i,{defaultOptionsName:r?.options?.as===void 0,members:Object.fromEntries(t.entityTypes.filter(i=>i===e||[...t.aliases].find(([t])=>t===e)?.[1].includes(i)).flatMap(r=>{let n=h(t.resolvedShapes.get(r)?.properties??{},i),s=n&&g(r,{property:i,writableKey:e},n,{model:t,resolveRef:A,exposed:new Map,sources:{}},N.at("writable")),o=V.get(r)?.find(t=>t.entry===`${e}.${i}`);return s?[[r,{field:s,...T(n,A).localized?{exposedDescription:b}:{},...o?{options:{options:o.options,hints:o.hints}}:{}}]]:[]}))}]))])),childRecipes:B(e.childRecipes??{}),replaceRecipes:B(e.replaceRecipes??{})},entityTypes:t.entityTypes,aliases:Object.fromEntries(t.aliases),exampleType:t.rootEntityTypes[0]??t.entityTypes[0],states:{navigationStates:i.navigationStates,viewStates:i.viewStates,toolStates:i.toolStates},deletableTypes:k,writableTypes:K,actionFields:G,childRecipes:Object.fromEntries(Object.entries(F.groups).map(([e,t])=>[e,{groups:t,hints:F.hints.get(e)??[]}])),replaceRecipes:Object.fromEntries(Object.entries(W.groups).map(([e,t])=>[e,{groups:t,hints:W.hints.get(e)??[]}])),optionsEdges:Object.fromEntries([...V].map(([e,t])=>[e,t.map(({options:e,hints:t})=>({options:e,hints:t}))])),fields:Object.fromEntries([...C].map(([e,t])=>[e,t.map(t=>o(t,E.get(e)??[]))])),lenses:z}};export const createAssistantSchema=(e,t,n)=>{let s=resolveAssistantManifest(e,t,n);if(s)try{return{...r(s),resolved:s}}catch(e){if(e instanceof i)return void n.at("entities").error(a.ASSISTANT_FIELD_SHAPE_CONFLICT,l.ASSISTANT_FIELD_SHAPE_CONFLICT(e.fieldName,e.exposures));n.error(a.ASSISTANT_INVALID_SCHEMA,l.ASSISTANT_INVALID_SCHEMA([e instanceof Error?e.message:String(e)]));return}};
1
+ import{isArray as e}from"@salsita-ai/common/utils";import{ASSISTANT_HINTS as t,AssistantFieldClashError as r,buildAssistantManifest as i,describeAssistantValues as n,mergeAssistantFields as s,resolveAssistantField as o}from"#package/domain";import{DiagnosticCode as a,diagnosticMessages as l}from"../../../../errors/index.js";import{createSchemaRefResolver as S,isNullableSchema as p,phxDefinitionRefName as N}from"../../common/index.js";import{resolveAssistantFieldType as T}from"./assistant-field-type.js";import{resolveFormElements as c}from"./assistant-form-elements.js";let d="AnyDomainEntity",E="RecipeGroup",f="Recipe",A="DryRun",_="FieldOption",m="agentScreenshot",u=/^(?!(true|false|null)$)[_A-Za-z][_0-9A-Za-z]*$/,O={section:"childRecipes",component:"childEntityRecipes",mutation:"addChild"},I={section:"replaceRecipes",component:"replaceEntityRecipes",mutation:"replaceEntity"},y=new Set(["id","type","meta"]),h=new Set(["Query","Mutation","Navigation",E,f,A,"DryRunError",_,"Screenshot","JSON","String","Int","Float","Boolean","ID"]),v=new Set([E,f,_,A]),b=new Set(["id","type"]),g="A string, a message descriptor or a multilingual object; passed without translation.",R=(e,t)=>Object.entries(e).find(([e])=>e===t)?.[1],M=(e,{property:t,writableKey:r},i,{resolveRef:s,exposed:o,sources:S},p)=>{let N=Object.entries(R(S,e)??{}),c=o.get(e)?.find(({source:e})=>e===t)?.name??N.find(([,{source:e}])=>e===t)?.[0],d=void 0===c?N.find(([e])=>e===t)?.[1]:void 0;if(d)return void p.at(r,t).error(a.ASSISTANT_FIELD_NAME_CONFLICT,l.ASSISTANT_FIELD_NAME_CONFLICT(e,t,[d.source,t],`${r}.${t}`));let{type:E,enumValues:f,localized:A,description:_}=T(i,s),m={...E,nonNull:!1},u=c??t,O=void 0===c?n(_,f):void 0;return A?{name:u,property:t,type:{kind:"scalar",name:"JSON",nonNull:!1},description:O?`${O} ${g}`:g}:{name:u,property:t,type:m,...void 0===O?{}:{description:O}}},C=(e,t,{model:r,components:i})=>{let n=new Set([e,...[...r.aliases].filter(([,t])=>t.includes(e)).map(([e])=>e)]);return i.filter(({componentName:e,typeOrAlias:r})=>e===t&&n.has(r)).map(({instanceName:e})=>String(e))},j=(...e)=>t.filter(t=>e.some(e=>e?.includes(t))),w=(e,t,r,i)=>{let{model:n}=r,s=new Map,o=new Map;for(let[S,{groups:p,hints:N}]of Object.entries(e)){let e=n.entityTypes.find(e=>e===S),T=e?[e]:[...n.aliases].find(([e])=>e===S)?.[1];if(!T){i.at(S).error(a.ASSISTANT_UNKNOWN_TYPE,l.ASSISTANT_UNKNOWN_TYPE(t.section,S));continue}for(let[e,n]of(T.forEach(e=>o.set(e,j(o.get(e),N))),Object.entries(p))){let o=i.at(S,"groups",e),p=`${S}.groups.${e}`,N={groupId:n?.as??e,instanceName:e,...n?.description===void 0?{}:{description:n.description},dryRuns:n?.allowDryRuns??!0};for(let i of T){let n=C(i,t.component,r),T=s.get(i)??[],c=T.find(e=>e.group.groupId===N.groupId);n.includes(e)?c?o.error(a.ASSISTANT_DUPLICATE_RECIPE_GROUP,l.ASSISTANT_DUPLICATE_RECIPE_GROUP(t,i,N.groupId,[c.entry,p])):s.set(i,[...T,{group:N,entry:p,entryCtx:o}]):o.error(a.ASSISTANT_UNKNOWN_RECIPE_GROUP,l.ASSISTANT_UNKNOWN_RECIPE_GROUP(t,S,e,i,n))}}}let S=n.entityTypes.flatMap(e=>{let t=s.get(e);return t?[{entityType:e,groups:t}]:[]});return{groups:Object.fromEntries(S.map(({entityType:e,groups:t})=>[e,t.map(({group:e})=>e)])),hints:o,metaEntries:new Map(S.flatMap(({entityType:e,groups:[t]})=>t?[[e,t.entryCtx]]:[]))}},P=(e,t,{model:r,components:i})=>{let n=new Set([e,...[...r.aliases].filter(([,t])=>t.includes(e)).map(([e])=>e)]),s=i.find(e=>e.componentName===m&&String(e.instanceName)===t&&n.has(e.typeOrAlias));if(s?.implementation.kind!=="userBound"||"composite"!==s.implementation.implementation.kind)return;let o=s.implementation.implementation.resolvedProps.exportState;return"string"==typeof o?o:void 0},D=e=>`${e}Meta`,L=e=>`${e}ScreenshotView`,U=e=>`${e.charAt(0).toUpperCase()+e.slice(1)}Result`,W=e=>{let t=(({anyOf:e,oneOf:t})=>[...e??[],...t??[]].filter(e=>"null"!==e.type))(e);return e.$ref||1!==t.length?e:t[0]},$=(e,t)=>{let{type:r,enumValues:i,localized:s,description:o}=T(e,t),a={...r,nonNull:!1},l=s?`${n(o,i)??""} ${g}`.trim():n(o,i);return{type:{...s?{kind:"scalar",name:"JSON",nonNull:!1}:a,nonNull:r.nonNull},...void 0===l?{}:{description:l}}},F=(t,r)=>{let i=(t=>{let r=W(t),i=r?.type==="array"&&r.items&&!e(r.items)?W(r.items):void 0,n=i??r;if(n?.$ref!==void 0&&"$DomainEntity"===N(n.$ref))return{target:n.references??d,list:void 0!==i}})(t);if(i)return{kind:"edge",...i,nullable:p(t,r),...void 0===t.description?{}:{description:t.description}};let{type:s,enumValues:o,localized:a,description:l}=T(t,r),S=n(l,o);return a?{kind:"value",type:{kind:"scalar",name:"JSON",nonNull:s.nonNull},description:`${l??""} ${g}`.trim()}:{kind:"value",type:s,...void 0===S?{}:{description:S}}},V=(e,t)=>{let{type:r,enumValues:i,localized:n,description:s}=T(e,t);return{type:{kind:"value",type:r},localized:!0===n,description:s,enumValues:i}},K=(e,t,r,i,s)=>{let o=t.startsWith("$")?((e,t,r,i)=>{let n=((e,t,{expressions:r,entityTypes:i})=>{let n=i.find(t=>t===e);for(let i of void 0===n?[e]:r.entityExpressions.get(n)?.shapeGroups??[]){let e=[...r.expressions.get(i)?.values()??[]].find(e=>e.name===t);if(e?.declaredSchema)return{implementation:e.implementation,declaredSchema:e.declaredSchema}}})(e.name,t,r);if(!n)return void i.error(a.ASSISTANT_UNKNOWN_FIELD,l.ASSISTANT_UNKNOWN_FIELD("entities",e.name,t));let{implementation:s}=n;if("violations"===s.kind)return void i.error(a.ASSISTANT_UNSUPPORTED_EXPRESSION,l.ASSISTANT_UNSUPPORTED_EXPRESSION(e.name,t));if("relation"===s.kind){let[{target:e,cardinality:t}]=s.params;return{type:{kind:"edge",target:e,cardinality:t},localized:!1,description:void 0}}return V(n.declaredSchema,r.resolveRef)})(e,t,i,s):((e,t,{resolveRef:r},i)=>{let n=Object.entries(e.shape.properties).find(([e])=>e===t)?.[1];return n?V(n,r):void i.error(a.ASSISTANT_UNKNOWN_FIELD,l.ASSISTANT_UNKNOWN_FIELD("entities",e.name,t))})(e,t,i,s);if(!o)return;let S=r?.as??(t.startsWith("$")?`_${t.slice(1)}`:t);return y.has(S)?void s.error(a.ASSISTANT_RESERVED_FIELD_NAME,l.ASSISTANT_RESERVED_FIELD_NAME(e.name,t,S)):{name:S,source:t,type:o.type,localized:o.localized,description:n(r?.description??o.description,o.enumValues),describedByManifest:r?.description!==void 0,enumValues:o.enumValues,valueDescription:r?.description??o.description,...r?.lenses?.length?{lenses:[...new Set(r.lenses)]}:{}}},k=(e,t,r,i,n)=>s(t,r,i,(t,r,i)=>{n.error(a.ASSISTANT_FIELD_NAME_CONFLICT,l.ASSISTANT_FIELD_NAME_CONFLICT(e,t,[r,i]))});export const resolveAssistantManifest=(e,{model:t,states:r,phxDefinitions:i,expressions:n,components:s,actions:p},N)=>{let E=N.errors.length,f=N.at("entities"),A=S(t.commonTypes,i);for(let e of t.resolvedShapes.keys())h.has(e)&&N.error(a.ASSISTANT_RESERVED_TYPE_NAME,l.ASSISTANT_RESERVED_TYPE_NAME(e,[...h]));let _=((e,t,r)=>{let i=new Map;for(let n of e){let e=[];for(let[i,s]of Object.entries(n.entries)){let o=K(n,i,s,t,r.at(n.name,i));o&&e.push(o)}i.set(n.name,e)}return i})(((e,t,r)=>{let i=[];for(let[r,n]of t.resolvedShapes){let t=e[r];t&&i.push({name:r,shape:n,entries:t})}let n=new Set(t.resolvedShapes.keys());for(let t of Object.keys(e))n.has(t)||r.at(t).error(a.ASSISTANT_UNKNOWN_TYPE,l.ASSISTANT_UNKNOWN_TYPE("entities",t));return i})(e.entities??{},t,f),{resolveRef:A,expressions:n,entityTypes:t.entityTypes},f),y=k(d,[],_.get(d)??[],()=>void 0,N),W=new Map([[d,y]]);for(let e of t.aliases.keys())e!==d&&W.set(e,k(e,[y],_.get(e)??[],()=>void 0,N));for(let e of t.entityTypes){let r=[d,...[...t.aliases].filter(([t,r])=>t!==d&&r.includes(e)).map(([e])=>e)],i=[...t.resolvedShapes].find(([t])=>t===e)?.[1];W.set(e,k(e,r.map(e=>W.get(e)??[]),_.get(e)??[],e=>{let t=Object.entries(i?.properties??{}).find(([t])=>t===e)?.[1];return t&&T(t,A).enumValues},N))}let V=Object.fromEntries(t.entityTypes.map(e=>[e,Object.fromEntries((W.get(e)??[]).map(({name:e,source:t,localized:r})=>[e,r?{source:t,localized:!0}:{source:t}]))])),Y=w(e.childRecipes??{},O,{model:t,components:s},N.at("childRecipes")),x=w(e.replaceRecipes??{},I,{model:t,components:s},N.at("replaceRecipes")),H=((e,t,r)=>{let i=new Set;for(let n of Object.keys(e)){let e=t.entityTypes.find(e=>e===n),s=e?[e]:[...t.aliases].find(([e])=>e===n)?.[1];s?s.forEach(e=>i.add(e)):r.at(n).error(a.ASSISTANT_UNKNOWN_TYPE,l.ASSISTANT_UNKNOWN_TYPE("deletable",n))}return t.entityTypes.filter(e=>i.has(e))})(e.deletable??{},t,N.at("deletable")),z=((e,t,r)=>{let{model:i}=t,n=((e,t,r)=>{let i=new Map;for(let[n,s]of Object.entries(e)){let e=t.entityTypes.find(e=>e===n),o=e?[e]:[...t.aliases].find(([e])=>e===n)?.[1],S=[...t.resolvedShapes].find(([e])=>e===n)?.[1];if(!o||!S){r.at(n).error(a.ASSISTANT_UNKNOWN_TYPE,l.ASSISTANT_UNKNOWN_TYPE("writable",n));continue}for(let e of Object.keys(s)){let t=r.at(n,e);e.startsWith("$")||b.has(e)?t.error(a.ASSISTANT_NOT_WRITABLE,l.ASSISTANT_NOT_WRITABLE(n,e)):R(S.properties,e)?o.forEach(t=>{let r=i.get(t)??new Map;i.set(t,r.has(e)?r:r.set(e,n))}):t.error(a.ASSISTANT_UNKNOWN_FIELD,l.ASSISTANT_UNKNOWN_FIELD("writable",n,e))}}return i})(e,i,r);return i.entityTypes.flatMap(e=>{let s=[...i.resolvedShapes].find(([t])=>t===e)?.[1],o=[...n.get(e)??[]].flatMap(([i,n])=>{let o=s&&R(s.properties,i);return(o&&M(e,{property:i,writableKey:n},o,t,r))??[]});return o.length>0?[{entityType:e,fields:o}]:[]})})(e.writable??{},{model:t,resolveRef:A,exposed:_,sources:V},N.at("writable")),G=((e,t,r,i)=>{let{model:n}=r,s=new Map;for(let[o,S]of Object.entries(e)){let e=n.entityTypes.find(e=>e===o),p=e?[e]:[...n.aliases].find(([e])=>e===o)?.[1]??[];for(let[e,n]of Object.entries(S)){let S=n?.options;if(!S)continue;let N=i.at(o,e,"options"),T=`${o}.${e}`;for(let i of p){let n=t.find(e=>e.entityType===i)?.fields.find(t=>t.property===e);if(!n)continue;let o=S.as??`${n.name}Options`,p=C(i,S.componentType,r),c=s.get(i)??[],d=c.find(e=>e.options.edgeName===o||e.options.field===n.name);if(p.includes(S.instanceName))if(o===O.section||o===I.section||"screenshot"===o)N.error(a.ASSISTANT_RESERVED_OPTIONS_EDGE_NAME,l.ASSISTANT_RESERVED_OPTIONS_EDGE_NAME(T,o,n.name));else if(d)N.error(a.ASSISTANT_DUPLICATE_OPTIONS_EDGE,l.ASSISTANT_DUPLICATE_OPTIONS_EDGE(i,d.options.edgeName===o?{edgeName:o}:{field:n.name},[d.entry,T]));else{let e={field:n.name,edgeName:o,componentType:S.componentType,instanceName:S.instanceName,validate:S.validate??!0,dryRuns:S.allowDryRuns??!0};s.set(i,[...c,{options:e,hints:j(S.hints),entry:T,entryCtx:N}])}else N.error(a.ASSISTANT_UNKNOWN_OPTIONS_COMPONENT,l.ASSISTANT_UNKNOWN_OPTIONS_COMPONENT(T,S,i,p))}}}return s})(e.writable??{},z,{model:t,components:s},N.at("writable")),B=((e,t,r)=>{let{model:i,states:n}=t,s=new Map;for(let[o,S]of Object.entries(e)){let e=i.entityTypes.find(e=>e===o),p=e?[e]:[...i.aliases].find(([e])=>e===o)?.[1];if(!p){r.at(o).error(a.ASSISTANT_UNKNOWN_TYPE,l.ASSISTANT_UNKNOWN_TYPE("screenshots",o));continue}for(let[e,{description:i,as:N}]of Object.entries(S)){let S=r.at(o,e),T=`${o}.${e}`,c={view:N??e,instanceName:e,description:i};if(!u.test(c.view)){S.error(a.ASSISTANT_INVALID_SCREENSHOT_VIEW_NAME,l.ASSISTANT_INVALID_SCREENSHOT_VIEW_NAME(o,e,c.view));continue}for(let r of p){let i=C(r,m,t),p=P(r,e,t),N=s.get(r)??[],d=N.find(e=>e.view.view===c.view);i.includes(e)?void 0===p||n.exportStates.includes(p)?d?S.error(a.ASSISTANT_DUPLICATE_SCREENSHOT_VIEW,l.ASSISTANT_DUPLICATE_SCREENSHOT_VIEW(r,c.view,[d.entry,T])):s.set(r,[...N,{view:c,entry:T,entryCtx:S}]):S.error(a.ASSISTANT_UNKNOWN_SCREENSHOT_EXPORT_STATE,l.ASSISTANT_UNKNOWN_SCREENSHOT_EXPORT_STATE(o,e,p,n.exportStates)):S.error(a.ASSISTANT_UNKNOWN_SCREENSHOT_VIEW,l.ASSISTANT_UNKNOWN_SCREENSHOT_VIEW(o,e,r,i))}}}let o=i.entityTypes.flatMap(e=>{let t=s.get(e);return t?[{entityType:e,views:t}]:[]});return{views:new Map(o.map(({entityType:e,views:t})=>[e,t.map(({view:e})=>e)])),metaEntries:new Map(o.flatMap(({entityType:e,views:[t]})=>t?[[e,t.entryCtx]]:[]))}})(e.screenshots??{},{model:t,components:s,states:r},N.at("screenshots"));var J=[Y.metaEntries,x.metaEntries,new Map([...G].flatMap(([e,[t]])=>t?[[e,t.entryCtx]]:[])),B.metaEntries];for(let e of t.entityTypes){let r=J.flatMap(t=>t.get(e)??[])[0],i=D(e);r&&[...t.resolvedShapes.keys()].some(e=>e===i)&&r.error(a.ASSISTANT_META_TYPE_NAME_CONFLICT,l.ASSISTANT_META_TYPE_NAME_CONFLICT(e,i))}for(let[e,r]of B.metaEntries){let i=L(e);[...t.resolvedShapes.keys()].some(e=>e===i)&&r.error(a.ASSISTANT_SCREENSHOT_VIEW_TYPE_NAME_CONFLICT,l.ASSISTANT_SCREENSHOT_VIEW_TYPE_NAME_CONFLICT(e,i))}let X=t.entityTypes.filter(e=>(Y.groups[e]?.length??0)>0||(x.groups[e]?.length??0)>0||(G.get(e)?.length??0)>0||B.views.has(e)).flatMap(e=>[D(e),...B.views.has(e)?[L(e)]:[]]),Z=((e,{actions:t,resolveRef:r,builtInMutations:i,typeNames:n},s)=>{let o=[],S=[...t.values()].map(e=>String(e.name));for(let[p,N]of Object.entries(e)){let e=s.at(p),T=[...t.values()].find(e=>e.name===p);if(!T){e.error(a.ASSISTANT_UNKNOWN_ACTION,l.ASSISTANT_UNKNOWN_ACTION(p,S));continue}let c=N?.as??p,d=o.find(e=>e.fieldName===c),E=U(c);if(i.has(c)||d){e.error(a.ASSISTANT_MUTATION_NAME_CONFLICT,l.ASSISTANT_MUTATION_NAME_CONFLICT(p,c,d?`"assistant.actions.${d.actionName}" already exposes`:"the built-in mutation of that name already takes"));continue}if(n.has(E)||o.some(e=>U(e.fieldName)===E)){e.error(a.ASSISTANT_ACTION_RESULT_TYPE_CONFLICT,l.ASSISTANT_ACTION_RESULT_TYPE_CONFLICT(p,c,E));continue}let f=N?.description??T.description;o.push({fieldName:c,actionName:p,...void 0===f?{}:{description:f},args:Object.fromEntries(Object.entries(T.params).map(([e,{schema:t}])=>[e,$(t,r)])),result:Object.fromEntries(Object.entries(T.result).map(([e,t])=>[e,F(t,r)]))})}return o})(e.actions??{},{actions:p,resolveRef:A,builtInMutations:new Set(["deleteEntity",O.mutation,I.mutation,...z.map(({entityType:e})=>`modify${e}`)]),typeNames:new Set([...h,...t.resolvedShapes.keys(),...X])},N.at("actions")),Q=((e,t)=>{let r=e.lenses??{},i=new Set;for(let[n,s]of Object.entries(e.entities??{}))for(let[e,o]of Object.entries(s))for(let s of o?.lenses??[])Object.hasOwn(r,s)?i.add(s):t.at("entities",n,e,"lenses").error(a.ASSISTANT_UNKNOWN_LENS,l.ASSISTANT_UNKNOWN_LENS(`${n}.${e}`,s,Object.keys(r)));for(let e of Object.keys(r))v.has(e)?t.at("lenses",e).error(a.ASSISTANT_RESERVED_LENS_NAME,l.ASSISTANT_RESERVED_LENS_NAME(e,[...v])):i.has(e)||t.at("lenses",e).error(a.ASSISTANT_EMPTY_LENS,l.ASSISTANT_EMPTY_LENS(e));return Object.fromEntries(Object.entries(r).map(([e,{description:t,maxDepth:r}])=>[e,{description:t,maxDepth:r??1}]))})(e,N),q=c(e.formElements??{},N.at("formElements"));if(N.errors.length>E)return;let ee=e=>Object.fromEntries(Object.entries(e).map(([e,t])=>[e,{hints:j(t.hints),groups:Object.entries(t.groups).map(([e,t])=>({instanceName:e,groupId:t?.as??e,...t?.description===void 0?{}:{description:t.description},dryRuns:t?.allowDryRuns??!0}))}]));return{declarations:{entities:Object.fromEntries(Object.keys(e.entities??{}).map(e=>[e,(_.get(e)??[]).map(e=>({name:e.name,source:e.source,type:e.type,localized:e.localized,describedByManifest:e.describedByManifest,...void 0===e.description?{}:{description:e.description},...void 0===e.valueDescription?{}:{valueDescription:e.valueDescription},...void 0===e.enumValues?{}:{enumValues:e.enumValues},...void 0===e.lenses?{}:{lenses:e.lenses}}))])),enumValues:Object.fromEntries(t.entityTypes.map(e=>[e,Object.fromEntries(Object.entries(t.resolvedShapes.get(e)?.properties??{}).flatMap(([e,t])=>{let r=T(t,A).enumValues;return r?[[e,r]]:[]}))])),deletable:Object.keys(e.deletable??{}),writable:Object.fromEntries(Object.entries(e.writable??{}).map(([e,r])=>[e,Object.fromEntries(Object.entries(r).map(([r,i])=>[r,{defaultOptionsName:i?.options?.as===void 0,members:Object.fromEntries(t.entityTypes.filter(r=>r===e||[...t.aliases].find(([t])=>t===e)?.[1].includes(r)).flatMap(i=>{let n=R(t.resolvedShapes.get(i)?.properties??{},r),s=n&&M(i,{property:r,writableKey:e},n,{model:t,resolveRef:A,exposed:new Map,sources:{}},N.at("writable")),o=G.get(i)?.find(t=>t.entry===`${e}.${r}`);return s?[[i,{field:s,...T(n,A).localized?{exposedDescription:g}:{},...o?{options:{options:o.options,hints:o.hints}}:{}}]]:[]}))}]))])),childRecipes:ee(e.childRecipes??{}),replaceRecipes:ee(e.replaceRecipes??{}),screenshots:Object.fromEntries(Object.entries(e.screenshots??{}).map(([e,t])=>[e,Object.entries(t).map(([e,{description:t,as:r}])=>({view:r??e,instanceName:e,description:t}))]))},entityTypes:t.entityTypes,aliases:Object.fromEntries(t.aliases),exampleType:t.rootEntityTypes[0]??t.entityTypes[0],states:{navigationStates:r.navigationStates,viewStates:r.viewStates,toolStates:r.toolStates},deletableTypes:H,writableTypes:z,actionFields:Z,childRecipes:Object.fromEntries(Object.entries(Y.groups).map(([e,t])=>[e,{groups:t,hints:Y.hints.get(e)??[]}])),replaceRecipes:Object.fromEntries(Object.entries(x.groups).map(([e,t])=>[e,{groups:t,hints:x.hints.get(e)??[]}])),optionsEdges:Object.fromEntries([...G].map(([e,t])=>[e,t.map(({options:e,hints:t})=>({options:e,hints:t}))])),screenshots:Object.fromEntries(B.views),fields:Object.fromEntries([...W].map(([e,t])=>[e,t.map(t=>o(t,_.get(e)??[]))])),lenses:Q,formElements:q}};export const createAssistantSchema=(e,t,n)=>{let s=resolveAssistantManifest(e,t,n);if(s)try{return{...i(s),resolved:s}}catch(e){if(e instanceof r)return void n.at("entities").error(a.ASSISTANT_FIELD_SHAPE_CONFLICT,l.ASSISTANT_FIELD_SHAPE_CONFLICT(e.fieldName,e.exposures));n.error(a.ASSISTANT_INVALID_SCHEMA,l.ASSISTANT_INVALID_SCHEMA([e instanceof Error?e.message:String(e)]));return}};
@@ -1 +1 @@
1
- import{createSelector as e}from"@salsita-ai/common/utils";import{createSessionAssistant as t,isAssistantSetupHandler as n}from"#package/domain";import{AnimationSystem as s,AssistantSystem as o,CameraControlSystem as r,CameraSystem as a,CanvasSystem as i,DragAndDropSystem as g,ExportSystem as m,GraphQLSystem as c,ImageRenderingSystem as l,LayoutSystem as S,SketchingSystem as u}from"./systems/index.js";export const createConfiguratorExtensionSelectors=y=>{let{getState:C,domainContextSelectors:d,specSelectors:f,domainServicesSelectors:p,accessSelectors:w,commonServicesSelectors:h}=y,x=e.withTypes()([f.selectConfiguratorSpec,d.selectInitializationContext],(e,s)=>t({resolved:e.assistant,handler:e.handlers.find(n),inputs:s&&s.access?{role:s.role,sessionData:s.sessionData,access:s.access}:null})),A={getConfigurationId:()=>w.getConfiguratorConfigurationId(),getAuthorizedClient:()=>h.getConfiguratorAuthorizedClient(),getAssistantSetup:()=>x(C()),domainContextSelectors:d,domainServicesSelectors:p,specSelectors:f},D={getSystems:()=>q,getCanvasSystem:()=>I,getCameraControlSystem:()=>z,getAnimationSystem:()=>v,getDragAndDropSystem:()=>k,getImageRenderingSystem:()=>b,getLayoutSystem:()=>Q,getSketchingSystem:()=>R,getExportSystem:()=>L,getCameraSystem:()=>T,getGraphQLSystem:()=>j,getAssistantSystem:()=>G},E={...A,configuratorExtensionSelectors:D},I=new i(A),z=new r({configuratorExtensionSelectors:D}),k=new g(E),v=new s(A),L=new m(E),j=new c(A),G=new o(E),Q=new S(A),R=new u(E),T=new a(E),b=new l(E),q={cameraControlSystem:z,layoutSystem:Q,sketchingSystem:R,dragAndDropSystem:k,canvasSystem:I,cameraSystem:T,exportSystem:L,imageRenderingSystem:b,animationSystem:v,graphQLSystem:j,assistantSystem:G};return D};
1
+ import{createSelector as e}from"@salsita-ai/common/utils";import{createSessionAssistant as t,isAssistantSetupHandler as n}from"#package/domain";import{AnimationSystem as s,AssistantSystem as o,CameraControlSystem as r,CameraSystem as a,CanvasSystem as i,DragAndDropSystem as g,ExportSystem as m,GraphQLSystem as c,ImageRenderingSystem as l,LayoutSystem as S,SketchingSystem as d}from"./systems/index.js";export const createConfiguratorExtensionSelectors=u=>{let{getState:y,domainContextSelectors:w,specSelectors:C,domainServicesSelectors:f,accessSelectors:p,commonServicesSelectors:h}=u,x=e.withTypes()([C.selectConfiguratorSpec,w.selectInitializationContext],(e,s)=>t({resolved:e.assistant,handler:e.handlers.find(n),inputs:s&&s.access?{role:s.role,sessionData:s.sessionData,access:s.access}:null})),A={getConfigurationId:()=>p.getConfiguratorConfigurationId(),getAuthorizedClient:()=>h.getConfiguratorAuthorizedClient(),getAssistantSetup:()=>x(y()),domainContextSelectors:w,domainServicesSelectors:f,specSelectors:C},I={getSystems:()=>Q,getCanvasSystem:()=>D,getCameraControlSystem:()=>v,getAnimationSystem:()=>b,getDragAndDropSystem:()=>z,getImageRenderingSystem:()=>G,getLayoutSystem:()=>T,getSketchingSystem:()=>j,getExportSystem:()=>k,getCameraSystem:()=>F,getGraphQLSystem:()=>L,getAssistantSystem:()=>R},E={...A,configuratorExtensionSelectors:I},D=new i(A),v=new r({configuratorExtensionSelectors:I}),z=new g(E),b=new s(A),k=new m(E),L=new c({...A,renderScreenshot:async(e,t)=>{let[n]=await I.getImageRenderingSystem().generateImagesFromComponents([e],t,{},{awaitSettledState:!0});if(!n)throw Error("The canvas rendered no image.");return n.blob}}),R=new o(E),T=new S(A),j=new d(E),F=new a(E),G=new l(E),Q={cameraControlSystem:v,layoutSystem:T,sketchingSystem:j,dragAndDropSystem:z,canvasSystem:D,cameraSystem:F,exportSystem:k,imageRenderingSystem:G,animationSystem:b,graphQLSystem:L,assistantSystem:R};return I};
@@ -1 +1 @@
1
- import{ChatClient as e,fetchServerSentEvents as t}from"@tanstack/ai-client";import{BehaviorSubject as i}from"rxjs";import{skip as s}from"rxjs/operators";import{isShallowEqual as a}from"@salsita-ai/common/utils";import{AssistantRequestError as r}from"#package/client";import{createBrowserDetailsClientTool as n}from"./browser-details.js";import{ChangeRecorder as o}from"./change-recorder.js";import{createApplyClientTool as l,createQueryClientTool as h}from"./graphql-tools.js";let d=Object.freeze({messages:Object.freeze([]),isLoading:!1,error:void 0,failedStatus:void 0,isContinuable:!0,isLoaded:!1,isResuming:!1,earlierThreads:Object.freeze([]),hasEarlierThread:!1,isLoadingEarlierThread:!1}),c=Object.freeze({open:!1,hasUnreadReply:!1,isInputFocused:!1}),u=Object.freeze({}),_=Object.freeze({results:Object.freeze([]),committed:!1,sideEffects:Object.freeze([]),message:"Nothing was run, because the user closed the configurator while this reply was being written. Do not call any more tools in this reply and end it now. The tools work again as soon as the user writes the next message."}),g=e=>{if(e.getError())return!0;let t=e.getMessages().at(-1)?.parts.at(-1);return t?.type!=="tool-call"&&t?.type!=="tool-result"};export class AssistantSystem{_context;_tools;_chat=new i(d);_panel=new i(c);_activation=null;_current=null;_finishing=new Set;_olderThreadId=null;_resuming=null;_refused=null;_firstAttempt=null;_chain={};_loadingThreadId=null;_recorder;_recording=null;_applyToolCallIds=new WeakMap;_undoTipToolCallId=new i(void 0);constructor(e){this._context=e;let{configuratorExtensionSelectors:t,domainContextSelectors:i,domainServicesSelectors:s,getAssistantSetup:a}=e;this._recorder=new o({maxEntities:30},{readers:{selectedEntityId:e=>i.getNavigationSystem().getActiveSelectionState(e),navigationState:e=>i.getNavigationSystem().getActiveNavigationState(e),viewState:e=>i.getNavigationSystem().getActiveViewState(e),toolState:e=>i.getNavigationSystem().getActiveToolState(e)},getExposedFields:()=>a().manifest?.fields??u,resolveLocalizable:e=>s.getResolveLocalizable()(e)});let r={getGraphQLSystem:()=>t.getGraphQLSystem(),getStoreSystem:()=>i.getStoreSystem(),getActiveMode:()=>i.getActiveMode(),clearChanges:()=>this._recorder.clear(this._committedState()),rememberCommit:(e,t)=>{this._applyToolCallIds.set(t,e),this._refreshUndoTip()}};this._tools=[n(),h(r),l(r)],i.getUndoSystem().tip$.subscribe(this._refreshUndoTip)}chat$=this._chat;panel$=this._panel;undoTipToolCallId$=this._undoTipToolCallId;getSessionManifest=()=>this._context.getAssistantSetup().manifest;initialize=()=>{let{committed:e}=this._context.domainContextSelectors.getStoreSystem().stateSlices;this._recording??=e.pipe(s(1)).subscribe(this._recorder.record),this._recorder.reset(e.value.state),this._endChat(),this._patchPanel(c),this._activation&&this._startClient()};activate=()=>{this._endChat();let e={};return this._activation=e,this._startClient(),()=>{this._activation===e&&(this._activation=null,this._endChat())}};send=async e=>{if(!this._chat.value.isContinuable&&!await this.resume())return;let t=this._current;if(!t)return;let i={changes:this._recorder.take(this._committedState())};this._firstAttempt=t;try{await this._sendOn(t,e,i);let s=this._findRefusedMessageIndex(t);if(-1===s)return;this._refused={chat:t,index:s}}finally{this._firstAttempt===t&&(this._firstAttempt=null)}if(!await this.resume())return;let s=this._current;s&&await this._sendOn(s,e,i)};resume=async()=>{let e=this._current;if(!e)return!1;if(this._chat.value.isContinuable)return!0;if(this._resuming)return this._resuming;let t=this._resume(e).finally(()=>{this._resuming===t&&(this._resuming=null)});return this._resuming=t,t};loadEarlierThread=async()=>{let e=this._current,t=this._olderThreadId;if(!e||null===t||this._loadingThreadId===t)return;let i=this._chain;this._loadingThreadId=t,this._chat.next({...this._chat.value,isLoadingEarlierThread:!0});try{let s=await e.authorizedClient.getAssistantEarlierThread(e.configurationId,t,50);if(this._chain!==i||this._olderThreadId!==t)return;this._olderThreadId=s.previousThreadId,this._chat.next({...this._chat.value,earlierThreads:[{threadId:t,messages:s.messages},...this._chat.value.earlierThreads],hasEarlierThread:null!==s.previousThreadId})}catch(e){this._context.domainServicesSelectors.getLogger().warn("Could not load the earlier assistant conversation",{error:e})}finally{this._chain===i&&this._loadingThreadId===t&&(this._loadingThreadId=null,this._chat.next({...this._chat.value,isLoadingEarlierThread:!1}))}};stop=async()=>{let e=this._current;if(!e)return;let t=e.client.getCurrentRunId();if(e.client.stop(),null!=t)try{let i=e.getConnection();await fetch(`${i.url}/runs/${encodeURIComponent(t)}/cancel`,{method:"POST",headers:await i.headers()})}catch(e){this._context.domainServicesSelectors.getLogger().warn("Could not cancel the assistant run on the server",{error:e})}};reloadThread=()=>{if(!this._activation)return;let e=this._retireChat();e?.client.stop(),e?.dispose(),this._recorder.reset(this._committedState()),this._startClient()};undoToolCalls=e=>{let t=this._context.domainContextSelectors.getUndoSystem(),i=()=>{let t=this._findTipToolCallId();return void 0!==t&&e.has(t)};for(;i();)t.undo()};setOpen=e=>{this._patchPanel(e?{open:e,hasUnreadReply:!1}:{open:e,isInputFocused:!1})};setInputFocused=e=>{this._patchPanel({isInputFocused:e})};_findRefusedMessageIndex=e=>{var t;let i,{failedStatus:s,isContinuable:a}=this._chat.value;return this._firstAttempt===e&&e.generation.active&&409===s&&!a?(i=(t=e.client.getMessages()).map(e=>e.role).lastIndexOf("user"),t.slice(i+1).every(e=>0===e.parts.length)?i:-1):-1};_sendOn=async(e,t,i)=>{let{client:s}=e;s.getIsLoading()||null===s.getResumeState()||s.stop();try{await s.sendMessage({content:t,metadata:i})}catch(i){let t=i instanceof Error?i:Error(String(i));e.generation.active&&this._chat.next({...this._chat.value,error:t}),this._context.domainServicesSelectors.getLogger().error("The assistant message could not be sent",{},t)}};_resume=async e=>{this._chat.next({...this._chat.value,isResuming:!0,error:void 0,failedStatus:void 0});try{let{previousThreadId:t}=await e.authorizedClient.compactAssistantThread(e.configurationId);if(!e.generation.active)return!1;let{messages:i,earlierThreads:s,hasEarlierThread:a}=this._chat.value,r=this._refused?.chat===e?i.slice(0,this._refused.index):i;return e.generation.active=!1,this._current=null,e.client.stop(),e.dispose(),this._chat.next({...d,earlierThreads:[...s,{threadId:t,messages:r}],hasEarlierThread:a,isLoadingEarlierThread:null!==this._loadingThreadId}),this._startClient(),!0}catch(i){let t=i instanceof Error?i:Error(String(i));if(t instanceof r&&409===t.status)return e.generation.active&&this.reloadThread(),!1;return e.generation.active&&this._chat.next({...this._chat.value,isResuming:!1,error:t,failedStatus:t instanceof r?t.status:void 0}),this._context.domainServicesSelectors.getLogger().error("The assistant thread could not be resumed",{},t),!1}};_findTipToolCallId=()=>{let e=this._context.domainContextSelectors.getUndoSystem(),t=e.tip$.value?.committedState;return void 0!==t&&e.canUndo()?this._applyToolCallIds.get(t):void 0};_refreshUndoTip=()=>{let e=this._findTipToolCallId();e!==this._undoTipToolCallId.value&&this._undoTipToolCallId.next(e)};_committedState=()=>this._context.domainContextSelectors.getStoreSystem().stateSlices.committed.value.state;_patchPanel=e=>{let t={...this._panel.value,...e};a(t,this._panel.value)||this._panel.next(t)};_takeOverThread=e=>{for(let t of this._finishing)t.configurationId===e&&(t.client.stop(),t.dispose())};_startClient=()=>{let i={active:!0},s=this._context.getConfigurationId();this._takeOverThread(s);let r=this._context.getAuthorizedClient(),n=()=>{if(i.active){let{manifest:e,error:t}=this._context.getAssistantSetup();if(t)throw t;if(!e)throw Error("The assistant is disabled for this session.")}return r.getAssistantChatConnection(s)},o=e=>{if(!i.active)return;let t={...this._chat.value,...e};a(t,this._chat.value)||this._chat.next(t)},l=async(e,t)=>{let s=await fetch(e,t).finally(()=>o({isLoaded:!0})),a=s.headers.get("x-assistant-thread-continuable");return null!=a&&s.ok&&i.active&&0===this._chat.value.earlierThreads.length&&(this._olderThreadId=s.headers.get("x-assistant-previous-thread")),o({failedStatus:s.ok?void 0:s.status,...null!=a&&{isContinuable:"false"!==a,hasEarlierThread:null!==this._olderThreadId}}),s},h=async()=>({headers:await n().headers(),fetchClient:l}),d=t(()=>n().url,h),c=t(()=>`${n().url}/runs`,h),u=this._tools.map(e=>({...e,execute:(t,s)=>{if(!i.active)return _;let{manifest:a,error:r}=this._context.getAssistantSetup();return r||!a?{error:r?.message??"The assistant is disabled for this session."}:e.execute?.(t,s)}})),f=new e({threadId:`assistant:${s}`,connection:{...d,joinRun:c.joinRun},persistence:!0,history:{pageSize:50},tools:u,onMessagesChange:e=>o({messages:e}),onLoadingChange:e=>{let t=i.active&&this._chat.value.isLoading;o({isLoading:e}),!e&&g(f)&&(i.active?t&&!this._panel.value.open&&this._patchPanel({hasUnreadReply:!0}):p())},onErrorChange:e=>{i.active&&e!==this._chat.value.error&&(!e||-1===this._findRefusedMessageIndex(v))&&(o({error:e}),e&&this._context.domainServicesSelectors.getLogger().error("The assistant request failed",{status:this._chat.value.failedStatus},e))}}),m=!1,p=()=>{m||(m=!0,this._finishing.delete(v),f.dispose())},v={client:f,configurationId:s,generation:i,authorizedClient:r,getConnection:n,dispose:p};this._current=v,f.attach()};_retireChat=()=>{let e=this._current;return this._current=null,e&&(e.generation.active=!1),this._chain={},this._olderThreadId=null,this._resuming=null,this._refused=null,this._loadingThreadId=null,this._chat.value!==d&&this._chat.next(d),e};_endChat=()=>{let e=this._retireChat();if(e){if(!e.client.getIsLoading()&&g(e.client))return void e.dispose();this._finishing.add(e)}}}
1
+ import{ChatClient as t,fetchServerSentEvents as e}from"@tanstack/ai-client";import{BehaviorSubject as i}from"rxjs";import{skip as s}from"rxjs/operators";import{isShallowEqual as a}from"@salsita-ai/common/utils";import{AssistantRequestError as r}from"#package/client";import{createBrowserDetailsClientTool as n}from"./browser-details.js";import{ChangeRecorder as o}from"./change-recorder.js";import{createApplyClientTool as l,createQueryClientTool as h,toToolResult as d}from"./graphql-tools.js";let c=Object.freeze({messages:Object.freeze([]),isLoading:!1,error:void 0,failedStatus:void 0,isContinuable:!0,isLoaded:!1,isResuming:!1,earlierThreads:Object.freeze([]),hasEarlierThread:!1,isLoadingEarlierThread:!1}),u=Object.freeze({open:!1,hasUnreadReply:!1,isInputFocused:!1}),_=Object.freeze({}),g=d({results:[],committed:!1,sideEffects:[],screenshots:new Map,message:"Nothing was run, because the user closed the configurator while this reply was being written. Do not call any more tools in this reply and end it now. The tools work again as soon as the user writes the next message."}),m=t=>{if(t.getError())return!0;let e=t.getMessages().at(-1)?.parts.at(-1);return e?.type!=="tool-call"&&e?.type!=="tool-result"};export class AssistantSystem{_context;_tools;_chat=new i(c);_panel=new i(u);_activation=null;_current=null;_finishing=new Set;_olderThreadId=null;_resuming=null;_refused=null;_firstAttempt=null;_chain={};_loadingThreadId=null;_recorder;_recording=null;_applyToolCallIds=new WeakMap;_undoTipToolCallId=new i(void 0);constructor(t){this._context=t;let{configuratorExtensionSelectors:e,domainContextSelectors:i,domainServicesSelectors:s,getAssistantSetup:a}=t;this._recorder=new o({maxEntities:30},{readers:{selectedEntityId:t=>i.getNavigationSystem().getActiveSelectionState(t),navigationState:t=>i.getNavigationSystem().getActiveNavigationState(t),viewState:t=>i.getNavigationSystem().getActiveViewState(t),toolState:t=>i.getNavigationSystem().getActiveToolState(t)},getExposedFields:()=>a().manifest?.fields??_,resolveLocalizable:t=>s.getResolveLocalizable()(t)});let r={getGraphQLSystem:()=>e.getGraphQLSystem(),getStoreSystem:()=>i.getStoreSystem(),getActiveMode:()=>i.getActiveMode(),clearChanges:()=>this._recorder.clear(this._committedState()),rememberCommit:(t,e)=>{this._applyToolCallIds.set(e,t),this._refreshUndoTip()}};this._tools=[n(),h(r),l(r)],i.getUndoSystem().tip$.subscribe(this._refreshUndoTip)}chat$=this._chat;panel$=this._panel;undoTipToolCallId$=this._undoTipToolCallId;getSessionManifest=()=>this._context.getAssistantSetup().manifest;initialize=()=>{let{committed:t}=this._context.domainContextSelectors.getStoreSystem().stateSlices;this._recording??=t.pipe(s(1)).subscribe(this._recorder.record),this._recorder.reset(t.value.state),this._endChat(),this._patchPanel(u),this._activation&&this._startClient()};activate=()=>{this._endChat();let t={};return this._activation=t,this._startClient(),()=>{this._activation===t&&(this._activation=null,this._endChat())}};send=async t=>{if(!this._chat.value.isContinuable&&!await this.resume())return;let e=this._current;if(!e)return;let i={changes:this._recorder.take(this._committedState())};this._firstAttempt=e;try{await this._sendOn(e,t,i);let s=this._findRefusedMessageIndex(e);if(-1===s)return;this._refused={chat:e,index:s}}finally{this._firstAttempt===e&&(this._firstAttempt=null)}if(!await this.resume())return;let s=this._current;s&&await this._sendOn(s,t,i)};resume=async()=>{let t=this._current;if(!t)return!1;if(this._chat.value.isContinuable)return!0;if(this._resuming)return this._resuming;let e=this._resume(t).finally(()=>{this._resuming===e&&(this._resuming=null)});return this._resuming=e,e};loadEarlierThread=async()=>{let t=this._current,e=this._olderThreadId;if(!t||null===e||this._loadingThreadId===e)return;let i=this._chain;this._loadingThreadId=e,this._chat.next({...this._chat.value,isLoadingEarlierThread:!0});try{let s=await t.authorizedClient.getAssistantEarlierThread(t.configurationId,e,50);if(this._chain!==i||this._olderThreadId!==e)return;this._olderThreadId=s.previousThreadId,this._chat.next({...this._chat.value,earlierThreads:[{threadId:e,messages:s.messages},...this._chat.value.earlierThreads],hasEarlierThread:null!==s.previousThreadId})}catch(t){this._context.domainServicesSelectors.getLogger().warn("Could not load the earlier assistant conversation",{error:t})}finally{this._chain===i&&this._loadingThreadId===e&&(this._loadingThreadId=null,this._chat.next({...this._chat.value,isLoadingEarlierThread:!1}))}};stop=async()=>{let t=this._current;if(!t)return;let e=t.client.getCurrentRunId();if(t.client.stop(),null!=e)try{let i=t.getConnection();await fetch(`${i.url}/runs/${encodeURIComponent(e)}/cancel`,{method:"POST",headers:await i.headers()})}catch(t){this._context.domainServicesSelectors.getLogger().warn("Could not cancel the assistant run on the server",{error:t})}};reloadThread=()=>{if(!this._activation)return;let t=this._retireChat();t?.client.stop(),t?.dispose(),this._recorder.reset(this._committedState()),this._startClient()};undoToolCalls=t=>{let e=this._context.domainContextSelectors.getUndoSystem(),i=()=>{let e=this._findTipToolCallId();return void 0!==e&&t.has(e)};for(;i();)e.undo()};setOpen=t=>{this._patchPanel(t?{open:t,hasUnreadReply:!1}:{open:t,isInputFocused:!1})};setInputFocused=t=>{this._patchPanel({isInputFocused:t})};_findRefusedMessageIndex=t=>{var e;let i,{failedStatus:s,isContinuable:a}=this._chat.value;return this._firstAttempt===t&&t.generation.active&&409===s&&!a?(i=(e=t.client.getMessages()).map(t=>t.role).lastIndexOf("user"),e.slice(i+1).every(t=>0===t.parts.length)?i:-1):-1};_sendOn=async(t,e,i)=>{let{client:s}=t;s.getIsLoading()||null===s.getResumeState()||s.stop();try{await s.sendMessage({content:e,metadata:i})}catch(i){let e=i instanceof Error?i:Error(String(i));t.generation.active&&this._chat.next({...this._chat.value,error:e}),this._context.domainServicesSelectors.getLogger().error("The assistant message could not be sent",{},e)}};_resume=async t=>{this._chat.next({...this._chat.value,isResuming:!0,error:void 0,failedStatus:void 0});try{let{previousThreadId:e}=await t.authorizedClient.compactAssistantThread(t.configurationId);if(!t.generation.active)return!1;let{messages:i,earlierThreads:s,hasEarlierThread:a}=this._chat.value,r=this._refused?.chat===t?i.slice(0,this._refused.index):i;return t.generation.active=!1,this._current=null,t.client.stop(),t.dispose(),this._chat.next({...c,earlierThreads:[...s,{threadId:e,messages:r}],hasEarlierThread:a,isLoadingEarlierThread:null!==this._loadingThreadId}),this._startClient(),!0}catch(i){let e=i instanceof Error?i:Error(String(i));if(e instanceof r&&409===e.status)return t.generation.active&&this.reloadThread(),!1;return t.generation.active&&this._chat.next({...this._chat.value,isResuming:!1,error:e,failedStatus:e instanceof r?e.status:void 0}),this._context.domainServicesSelectors.getLogger().error("The assistant thread could not be resumed",{},e),!1}};_findTipToolCallId=()=>{let t=this._context.domainContextSelectors.getUndoSystem(),e=t.tip$.value?.committedState;return void 0!==e&&t.canUndo()?this._applyToolCallIds.get(e):void 0};_refreshUndoTip=()=>{let t=this._findTipToolCallId();t!==this._undoTipToolCallId.value&&this._undoTipToolCallId.next(t)};_committedState=()=>this._context.domainContextSelectors.getStoreSystem().stateSlices.committed.value.state;_patchPanel=t=>{let e={...this._panel.value,...t};a(e,this._panel.value)||this._panel.next(e)};_takeOverThread=t=>{for(let e of this._finishing)e.configurationId===t&&(e.client.stop(),e.dispose())};_startClient=()=>{let i={active:!0},s=this._context.getConfigurationId();this._takeOverThread(s);let r=this._context.getAuthorizedClient(),n=()=>{if(i.active){let{manifest:t,error:e}=this._context.getAssistantSetup();if(e)throw e;if(!t)throw Error("The assistant is disabled for this session.")}return r.getAssistantChatConnection(s)},o=t=>{if(!i.active)return;let e={...this._chat.value,...t};a(e,this._chat.value)||this._chat.next(e)},l=async(t,e)=>{let s=await fetch(t,e).finally(()=>o({isLoaded:!0})),a=s.headers.get("x-assistant-thread-continuable");return null!=a&&s.ok&&i.active&&0===this._chat.value.earlierThreads.length&&(this._olderThreadId=s.headers.get("x-assistant-previous-thread")),o({failedStatus:s.ok?void 0:s.status,...null!=a&&{isContinuable:"false"!==a,hasEarlierThread:null!==this._olderThreadId}}),s},h=async()=>({headers:await n().headers(),fetchClient:l}),d=e(()=>n().url,h),c=e(()=>`${n().url}/runs`,h),u=this._tools.map(t=>({...t,execute:(e,s)=>{if(!i.active)return g;let{manifest:a,error:r}=this._context.getAssistantSetup();return r||!a?{error:r?.message??"The assistant is disabled for this session."}:t.execute?.(e,s)}})),_=new t({threadId:`assistant:${s}`,connection:{...d,joinRun:c.joinRun},persistence:!0,history:{pageSize:50},tools:u,onMessagesChange:t=>o({messages:t}),onLoadingChange:t=>{let e=i.active&&this._chat.value.isLoading;o({isLoading:t}),!t&&m(_)&&(i.active?e&&!this._panel.value.open&&this._patchPanel({hasUnreadReply:!0}):p())},onErrorChange:t=>{i.active&&t!==this._chat.value.error&&(!t||-1===this._findRefusedMessageIndex(v))&&(o({error:t}),t&&this._context.domainServicesSelectors.getLogger().error("The assistant request failed",{status:this._chat.value.failedStatus},t))}}),f=!1,p=()=>{f||(f=!0,this._finishing.delete(v),_.dispose())},v={client:_,configurationId:s,generation:i,authorizedClient:r,getConnection:n,dispose:p};this._current=v,_.attach()};_retireChat=()=>{let t=this._current;return this._current=null,t&&(t.generation.active=!1),this._chain={},this._olderThreadId=null,this._resuming=null,this._refused=null,this._loadingThreadId=null,this._chat.value!==c&&this._chat.next(c),t};_endChat=()=>{let t=this._retireChat();if(t){if(!t.client.getIsLoading()&&m(t.client))return void t.dispose();this._finishing.add(t)}}}
@@ -1,12 +1,13 @@
1
+ import type { ContentPart } from '@tanstack/ai';
1
2
  import type { DomainEntity, DomainMode, DomainState } from '#package/domain';
2
- import type { GraphQLSystem } from '../graphql-system/graphql-system.js';
3
+ import type { GraphQLExecution, GraphQLSystem } from '../graphql-system/graphql-system.js';
3
4
  /**
4
5
  * What the assistant's client tools need from the running configurator, as accessors they call when
5
6
  * the model calls them: a tool call always runs against the systems and the session mode current at
6
7
  * that moment, never against ones captured when its reply began streaming.
7
8
  */
8
9
  export interface AssistantToolDependencies {
9
- readonly getGraphQLSystem: () => Pick<GraphQLSystem, 'prepare' | 'execute' | 'readSideEffects'>;
10
+ readonly getGraphQLSystem: () => Pick<GraphQLSystem, 'prepare' | 'execute' | 'readSideEffects' | 'readScreenshots'>;
10
11
  readonly getStoreSystem: () => {
11
12
  readonly isInitialized: boolean;
12
13
  readonly stateSlices: {
@@ -26,6 +27,15 @@ export interface AssistantToolDependencies {
26
27
  */
27
28
  readonly rememberCommit: (toolCallId: string, state: DomainState<DomainEntity<string>>) => void;
28
29
  }
30
+ /**
31
+ * A tool result as the chat client accepts it: content parts, the execution's JSON as a text part
32
+ * followed, per screenshot, by its label as a text part and its picture as an image part, so the
33
+ * model reads each picture under the label its `image` field returned. The client deep-clones a
34
+ * result and rejects anything that is not plain JSON, so the `GraphQLError` instances of a result
35
+ * are formatted through their `toJSON` and `undefined` members are dropped. The prepared state stays
36
+ * in the browser.
37
+ */
38
+ export declare const toToolResult: ({ preparedState, screenshots, ...execution }: GraphQLExecution) => ContentPart[];
29
39
  /**
30
40
  * Browser implementation of the assistant's `query` tool: prepares the operations from the
31
41
  * committed state and drops their emissions, so mutations in the list show a hypothetical state and
@@ -1 +1 @@
1
- import{AssistantGraphQLToolInput as e,applyTool as t,queryTool as o}from"@salsita-ai/contracts/assistant";let a=e=>({results:[],committed:!1,message:e,sideEffects:[]}),i=({preparedState:e,...t})=>JSON.parse(JSON.stringify(t)),s=t=>{let o=e.safeParse(t);if(!o.success){let e=o.error.issues.map(e=>`${e.path.join(".")}: ${e.message}`).join("; ");return{ok:!1,refusal:a(`Nothing was run, because the tool input is not a list of operations (${e}). Send { "operations": [{ "query": "<one GraphQL operation>" }] } with at least one item.`)}}return{ok:!0,operations:o.data.operations.map(({query:e,variables:t,operationName:o})=>({document:e,variables:t??void 0,operationName:o??void 0}))}};export const createQueryClientTool=e=>o.client(async t=>{let o=s(t);if(!o.ok)return o.refusal;let r=e.getStoreSystem();if(!r.isInitialized)return a("Nothing was run, because the configuration is not loaded yet, so there is no state to query. Call the tool again once the configurator has finished loading.");let n=e.getGraphQLSystem(),l=await n.prepare(r.stateSlices.committed.value.state,o.operations);return i({results:l.map(({result:e})=>e),committed:!1,sideEffects:n.readSideEffects(l)})});export const createApplyClientTool=e=>t.client(async(t,o)=>{if("owner"!==e.getActiveMode())return a("Nothing was run, because changes cannot be applied in this session: the user may view this configuration but not change it. The query tool still works here, including mutations, to show what a change would do without keeping it.");let r=s(t);if(!r.ok)return r.refusal;let n=await e.getGraphQLSystem().execute(r.operations),l=o?.toolCallId;return void 0!==l&&void 0!==n.preparedState&&e.rememberCommit(l,n.preparedState),n.committed&&e.clearChanges(),i(n)});
1
+ import{AssistantGraphQLToolInput as e,applyTool as t,queryTool as o}from"@salsita-ai/contracts/assistant";let s=e=>({results:[],committed:!1,message:e,sideEffects:[],screenshots:new Map});export const toToolResult=({preparedState:e,screenshots:t,...o})=>[{type:"text",content:JSON.stringify(o)},...[...t].flatMap(([e,{data:t,mimeType:o}])=>[{type:"text",content:e},{type:"image",source:{type:"data",value:t,mimeType:o}}])];let a=t=>{let o=e.safeParse(t);if(!o.success){let e=o.error.issues.map(e=>`${e.path.join(".")}: ${e.message}`).join("; ");return{ok:!1,refusal:s(`Nothing was run, because the tool input is not a list of operations (${e}). Send { "operations": [{ "query": "<one GraphQL operation>" }] } with at least one item.`)}}return{ok:!0,operations:o.data.operations.map(({query:e,variables:t,operationName:o})=>({document:e,variables:t??void 0,operationName:o??void 0}))}};export const createQueryClientTool=e=>o.client(async t=>{let o=a(t);if(!o.ok)return toToolResult(o.refusal);let i=e.getStoreSystem();if(!i.isInitialized)return toToolResult(s("Nothing was run, because the configuration is not loaded yet, so there is no state to query. Call the tool again once the configurator has finished loading."));let r=e.getGraphQLSystem(),n=await r.prepare(i.stateSlices.committed.value.state,o.operations);return toToolResult({results:n.map(({result:e})=>e),committed:!1,sideEffects:r.readSideEffects(n),screenshots:r.readScreenshots(n)})});export const createApplyClientTool=e=>t.client(async(t,o)=>{if("owner"!==e.getActiveMode())return toToolResult(s("Nothing was run, because changes cannot be applied in this session: the user may view this configuration but not change it. The query tool still works here, including mutations, to show what a change would do without keeping it."));let i=a(t);if(!i.ok)return toToolResult(i.refusal);let r=await e.getGraphQLSystem().execute(i.operations),n=o?.toolCallId;return void 0!==n&&void 0!==r.preparedState&&e.rememberCommit(n,r.preparedState),r.committed&&e.clearChanges(),toToolResult(r)});
@@ -94,6 +94,14 @@ export declare class CanvasSystem<ENTITIES extends DomainEntity<string>> {
94
94
  readonly getCanvasEnvironment: (state: DomainState<ENTITIES>) => import("#package/domain").ComponentSnapshot<import("#package/domain").SystemComponentType<"canvasCustomEnvironment", import("#package/domain").CanvasCustomEnvironmentComponentState, never> | import("#package/domain").SystemComponentType<"canvasPresetEnvironment", import("#package/domain").CanvasPresetEnvironmentComponentState, never>> | undefined;
95
95
  readonly getCanvasConfig: (state: DomainState<ENTITIES>) => import("#package/domain").CanvasConfigComponentState | undefined;
96
96
  readonly register: (scene: Scene, gl: WebGLRenderer, r3fHandle: R3FHandle, loadingProgress$: Observable<LoadingProgress>) => () => void;
97
+ /**
98
+ * Resolves with the handle of the mounted canvas, waiting for a canvas to register when none is
99
+ * mounted yet. Work that needs the canvas and can be started by the user before the first mount
100
+ * (an image capture started from the summary) awaits this instead of failing on an empty handle.
101
+ *
102
+ * @internal
103
+ */
104
+ readonly waitForCanvas: (timeoutMs?: number) => Promise<R3FHandle>;
97
105
  readonly reportFlush: (token: FlushToken) => void;
98
106
  readonly registerForceFlush: (trigger: (token: FlushToken) => void) => () => void;
99
107
  private _forceFlush;
@@ -1 +1 @@
1
- import{BehaviorSubject as e,Subject as t,debounceTime as i,delay as n,filter as s,firstValueFrom as r,map as o,merge as a,of as h,switchMap as l,tap as c}from"rxjs";import{applicationConfigComponent as d,canvasConfigComponent as u,canvasCustomEnvironmentComponent as g,canvasPresetEnvironmentComponent as _}from"#package/domain";export const resolveInteractiveViewport=(e,t)=>!Number.isFinite(t.width)||!Number.isFinite(t.height)||t.width<=0||t.height<=0||e?.width===t.width&&e.height===t.height?e:t;export class CanvasInitializationCancellationError extends Error{constructor(){super("Canvas generation ended while waiting for a flush!"),this.name="CanvasInitializationCancellationError"}}export class CanvasSystem{_context;_dimensionIndicatorShown;_meshChanged$=new e(!0);_firstLoadFinished=new e(!1);dimensionIndicatorShown$;meshChanged$=this._meshChanged$;firstLoadFinished$=this._firstLoadFinished;onCanvasClick$=new t;onCanvasPointerMove$=new t;_ready$=new e(null);ready$=this._ready$;_r3fHandle$=new e(null);r3fHandle$=this._r3fHandle$;_interactiveViewport$=new e(null);interactiveViewport$=this._interactiveViewport$;_flushed$=new t;_flushSequence=0;_lastReportedFlushSequence=0;_triggerForceFlush=null;_registration=null;_progress$=new e({type:"finished"});_generation=null;constructor(t){this._context=t,this._dimensionIndicatorShown=new e(!1),this.dimensionIndicatorShown$=this._dimensionIndicatorShown}getCanvasEnvironment=e=>{let t=this._context.domainContextSelectors.getComponentReader().readManyComponents({componentTypes:[g,_]},e);return t.length>1&&this._context.domainServicesSelectors.getLogger().warn("Multiple canvas environments found, using the first one!",{environments:t}),t.at(0)};getCanvasConfig=e=>{let t=this._context.domainContextSelectors.getComponentReader().readManyComponents({componentTypes:[u]},e);return t.length>1&&this._context.domainServicesSelectors.getLogger().warn("Multiple canvas configs found, using the first one!",{configs:t}),t.at(0)?.state};register=(e,t,i,n)=>{let s={scene:e,gl:t,r3fHandle:i};this._registration=s,this._ready$.next({scene:e,gl:t}),this._r3fHandle$.next(i),this._interactiveViewport$.next(null);let r=n.subscribe(e=>{this._progress$.next(e)}),o=e=>{if(this._registration!==s)return;let t=resolveInteractiveViewport(this._interactiveViewport$.value,e);t!==this._interactiveViewport$.value&&(this._interactiveViewport$.next(t),i.get().invalidate())},a=t.domElement,h=a.getBoundingClientRect();o({width:h.width,height:h.height});let l=new ResizeObserver(([e])=>{e&&o({width:e.contentRect.width,height:e.contentRect.height})});return l.observe(a),()=>{r.unsubscribe(),l.disconnect(),this._registration===s&&(this._registration=null,this._ready$.next(null),this._r3fHandle$.next(null),this._interactiveViewport$.next(null),this._progress$.next({type:"finished"}),this._firstLoadFinished.next(!1))}};reportFlush=e=>{this._lastReportedFlushSequence=Math.max(this._lastReportedFlushSequence,e.sequence),this._flushed$.next(e)};registerForceFlush=e=>(this._triggerForceFlush=e,()=>{this._triggerForceFlush===e&&(this._triggerForceFlush=null)});_forceFlush(){if(!this._triggerForceFlush)throw this._context.domainServicesSelectors.getLogger().debug("Force flush skipped: canvas not ready"),new CanvasInitializationCancellationError;let e={sequence:++this._flushSequence},t=r(a(this._flushed$.pipe(s(t=>t.sequence>=e.sequence),o(()=>void 0)),h(void 0).pipe(n(1e4),c(()=>{this._context.domainServicesSelectors.getLogger().error("Force flush timeout",{requestedSequence:e.sequence,lastReportedSequence:this._lastReportedFlushSequence,timeoutMs:1e4,detectorRegistered:null!==this._triggerForceFlush})}))));return this._triggerForceFlush(e),t}toggleDimensionIndicator=()=>{this._dimensionIndicatorShown.next(!this._dimensionIndicatorShown.value)};setDimensionIndicatorShown=e=>{this._dimensionIndicatorShown.value!==e&&this._dimensionIndicatorShown.next(e)};reportMeshChanged=()=>{this._meshChanged$.next(!0)};reportFirstLoadFinished=()=>{this._firstLoadFinished.next(!0)};_findEntity3d(e,t){let i=[];if(e.entityIds?.length===0)return i;if(t.traverse(t=>{let n=t.userData;"entityRoot"===n.type&&(void 0===e.entityIds||e.entityIds.includes(n.entityId))&&i.push(t)}),e.descendantEntities){let e=[...i];for(;e.length>0;){let t=e.shift();t&&t.children.forEach(t=>{"entityRoot"===t.userData.type&&(i.push(t),e.push(t))})}}let n=e.entityRoots?[...i]:[];if(e.entityObjects){let t=i=>{i.children.forEach(i=>{let s=i.userData;"entityObjectGroup"===s.type?t(i):"entityObject"!==s.type||(void 0===e.objectInstanceNames||e.objectInstanceNames.includes(s.objectInstanceName))&&(e.recursiveObjects?i.traverse(e=>{n.push(e)}):n.push(i))})};i.forEach(e=>t(e))}return n}findEntity3d=e=>this.ready$.value?.scene?this._findEntity3d(e,this.ready$.value.scene):[];findEntity3d$=e=>this.ready$.pipe(s(e=>null!=e),l(({scene:t})=>this.meshChanged$.pipe(i(0),o(()=>this._findEntity3d(e,t)))));initialize=()=>{let e=this._context.domainContextSelectors.getComponentReader().readFirstRootComponent({componentTypes:[d]},this._context.domainContextSelectors.getStoreSystem().stateSlices.committed.value.state);null!==this._generation&&(this._generation.active=!1);let t=e?.state.initialIndicatorShown??!1;this._generation={active:!0},this._dimensionIndicatorShown.next(t),this._meshChanged$.next(!0)};waitForFlush=async()=>{let e=this._generation,t=()=>{if(!e?.active)throw new CanvasInitializationCancellationError};for(t(),await this._forceFlush(),t(),await this._forceFlush(),t();"finished"!==this._progress$.value.type;)await r(this._progress$.pipe(s(e=>"finished"===e.type))),t(),await this._forceFlush(),t(),await this._forceFlush(),t()}}
1
+ import{BehaviorSubject as e,Subject as t,debounceTime as i,delay as n,filter as s,firstValueFrom as r,map as o,merge as a,of as h,switchMap as l,tap as c,throwError as d,timeout as u}from"rxjs";import{isNonNullish as g}from"@salsita-ai/common/utils";import{applicationConfigComponent as _,canvasConfigComponent as p,canvasCustomEnvironmentComponent as m,canvasPresetEnvironmentComponent as w}from"#package/domain";export const resolveInteractiveViewport=(e,t)=>!Number.isFinite(t.width)||!Number.isFinite(t.height)||t.width<=0||t.height<=0||e?.width===t.width&&e.height===t.height?e:t;export class CanvasInitializationCancellationError extends Error{constructor(){super("Canvas generation ended while waiting for a flush!"),this.name="CanvasInitializationCancellationError"}}let v=1e4;export class CanvasSystem{_context;_dimensionIndicatorShown;_meshChanged$=new e(!0);_firstLoadFinished=new e(!1);dimensionIndicatorShown$;meshChanged$=this._meshChanged$;firstLoadFinished$=this._firstLoadFinished;onCanvasClick$=new t;onCanvasPointerMove$=new t;_ready$=new e(null);ready$=this._ready$;_r3fHandle$=new e(null);r3fHandle$=this._r3fHandle$;_interactiveViewport$=new e(null);interactiveViewport$=this._interactiveViewport$;_flushed$=new t;_flushSequence=0;_lastReportedFlushSequence=0;_triggerForceFlush=null;_registration=null;_progress$=new e({type:"finished"});_generation=null;constructor(t){this._context=t,this._dimensionIndicatorShown=new e(!1),this.dimensionIndicatorShown$=this._dimensionIndicatorShown}getCanvasEnvironment=e=>{let t=this._context.domainContextSelectors.getComponentReader().readManyComponents({componentTypes:[m,w]},e);return t.length>1&&this._context.domainServicesSelectors.getLogger().warn("Multiple canvas environments found, using the first one!",{environments:t}),t.at(0)};getCanvasConfig=e=>{let t=this._context.domainContextSelectors.getComponentReader().readManyComponents({componentTypes:[p]},e);return t.length>1&&this._context.domainServicesSelectors.getLogger().warn("Multiple canvas configs found, using the first one!",{configs:t}),t.at(0)?.state};register=(e,t,i,n)=>{let s={scene:e,gl:t,r3fHandle:i};this._registration=s,this._ready$.next({scene:e,gl:t}),this._r3fHandle$.next(i),this._interactiveViewport$.next(null);let r=n.subscribe(e=>{this._progress$.next(e)}),o=e=>{if(this._registration!==s)return;let t=resolveInteractiveViewport(this._interactiveViewport$.value,e);t!==this._interactiveViewport$.value&&(this._interactiveViewport$.next(t),i.get().invalidate())},a=t.domElement,h=a.getBoundingClientRect();o({width:h.width,height:h.height});let l=new ResizeObserver(([e])=>{e&&o({width:e.contentRect.width,height:e.contentRect.height})});return l.observe(a),()=>{r.unsubscribe(),l.disconnect(),this._registration===s&&(this._registration=null,this._ready$.next(null),this._r3fHandle$.next(null),this._interactiveViewport$.next(null),this._progress$.next({type:"finished"}),this._firstLoadFinished.next(!1))}};waitForCanvas=(e=v)=>r(this._r3fHandle$.pipe(s(g),u({first:e,with:()=>d(()=>Error(`The 3D canvas did not mount within ${e} ms, so the canvas work that waited for it (an image capture or an export) cannot run. The canvas mounts when the configurator layout renders it; make sure the current layout shows the canvas, then retry once the scene is visible.`))})));reportFlush=e=>{this._lastReportedFlushSequence=Math.max(this._lastReportedFlushSequence,e.sequence),this._flushed$.next(e)};registerForceFlush=e=>(this._triggerForceFlush=e,()=>{this._triggerForceFlush===e&&(this._triggerForceFlush=null)});_forceFlush(){if(!this._triggerForceFlush)throw this._context.domainServicesSelectors.getLogger().debug("Force flush skipped: canvas not ready"),new CanvasInitializationCancellationError;let e={sequence:++this._flushSequence},t=r(a(this._flushed$.pipe(s(t=>t.sequence>=e.sequence),o(()=>void 0)),h(void 0).pipe(n(1e4),c(()=>{this._context.domainServicesSelectors.getLogger().error("Force flush timeout",{requestedSequence:e.sequence,lastReportedSequence:this._lastReportedFlushSequence,timeoutMs:1e4,detectorRegistered:null!==this._triggerForceFlush})}))));return this._triggerForceFlush(e),t}toggleDimensionIndicator=()=>{this._dimensionIndicatorShown.next(!this._dimensionIndicatorShown.value)};setDimensionIndicatorShown=e=>{this._dimensionIndicatorShown.value!==e&&this._dimensionIndicatorShown.next(e)};reportMeshChanged=()=>{this._meshChanged$.next(!0)};reportFirstLoadFinished=()=>{this._firstLoadFinished.next(!0)};_findEntity3d(e,t){let i=[];if(e.entityIds?.length===0)return i;if(t.traverse(t=>{let n=t.userData;"entityRoot"===n.type&&(void 0===e.entityIds||e.entityIds.includes(n.entityId))&&i.push(t)}),e.descendantEntities){let e=[...i];for(;e.length>0;){let t=e.shift();t&&t.children.forEach(t=>{"entityRoot"===t.userData.type&&(i.push(t),e.push(t))})}}let n=e.entityRoots?[...i]:[];if(e.entityObjects){let t=i=>{i.children.forEach(i=>{let s=i.userData;"entityObjectGroup"===s.type?t(i):"entityObject"!==s.type||(void 0===e.objectInstanceNames||e.objectInstanceNames.includes(s.objectInstanceName))&&(e.recursiveObjects?i.traverse(e=>{n.push(e)}):n.push(i))})};i.forEach(e=>t(e))}return n}findEntity3d=e=>this.ready$.value?.scene?this._findEntity3d(e,this.ready$.value.scene):[];findEntity3d$=e=>this.ready$.pipe(s(e=>null!=e),l(({scene:t})=>this.meshChanged$.pipe(i(0),o(()=>this._findEntity3d(e,t)))));initialize=()=>{let e=this._context.domainContextSelectors.getComponentReader().readFirstRootComponent({componentTypes:[_]},this._context.domainContextSelectors.getStoreSystem().stateSlices.committed.value.state);null!==this._generation&&(this._generation.active=!1);let t=e?.state.initialIndicatorShown??!1;this._generation={active:!0},this._dimensionIndicatorShown.next(t),this._meshChanged$.next(!0)};waitForFlush=async()=>{let e=this._generation,t=()=>{if(!e?.active)throw new CanvasInitializationCancellationError};for(t(),await this._forceFlush(),t(),await this._forceFlush(),t();"finished"!==this._progress$.value.type;)await r(this._progress$.pipe(s(e=>"finished"===e.type))),t(),await this._forceFlush(),t(),await this._forceFlush(),t()}}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * An export run that waited for the configuration to settle gave up: the user kept changing what
3
+ * the configurator shows (a drag in progress, for instance), and exporting over it would have thrown
4
+ * the change away.
5
+ */
6
+ export declare class ExportBusyError extends Error {
7
+ constructor();
8
+ }
@@ -0,0 +1 @@
1
+ export class ExportBusyError extends Error{constructor(){super("The export did not start, because the user kept changing the configuration while it waited, and exporting would have discarded the change. Run the export again once the user has finished."),this.name="ExportBusyError"}}
@@ -1,4 +1,5 @@
1
1
  import type { Scene } from 'three';
2
+ import { type ObservableWithLastValue } from '@salsita-ai/common/utils';
2
3
  import type { $DomainEntity, DomainContextSelectors, DomainEntity, DomainEntityId, DomainServicesSelectors, DomainState } from '#package/domain';
3
4
  import type { ConfiguratorExtensionSelectors } from '../../selectors.js';
4
5
  import type { R3FHandle } from '../canvas-system.js';
@@ -29,12 +30,26 @@ export interface ExportJob<EXPORT_STATES extends string, ENTITIES extends Domain
29
30
  selectedEntityId?: ENTITIES['id'];
30
31
  execute: (props: ExportJobArguments<ENTITIES, any, any>) => Promise<R>;
31
32
  }
33
+ /** How one export run starts. */
34
+ export interface ExportRunOptions {
35
+ /**
36
+ * Waits for the user to finish a change they are making, such as a drag, before the first job, for
37
+ * up to {@link SETTLE_TIMEOUT_MS}, and rejects with an {@link ExportBusyError} when they do not. A run
38
+ * the user did not start themselves, such as the assistant's screenshot, must not export over it. A
39
+ * hover is no such change: the run's restore drops it, as it always did.
40
+ */
41
+ readonly awaitSettledState?: boolean;
42
+ }
32
43
  /** ExportSystem with its entity types erased — how the runtime selectors expose the instance. */
33
44
  export type AnyExportSystem = ExportSystem<any, any, any, any, any, any, any>;
34
45
  export declare class ExportSystem<ENTITIES extends DomainEntity<string>, ALLOWED_ROOT_ENTITIES extends ENTITIES, $MAP extends Record<ENTITIES['type'], $DomainEntity<ENTITIES>>, NAVIGATION_STATES extends string, VIEW_STATES extends string, TOOL_STATES extends string, EXPORT_STATES extends string> {
35
46
  private readonly _context;
36
47
  private _generation;
37
48
  private _frameloopPause;
49
+ private _queue;
50
+ private readonly _exporting;
51
+ /** True while an export run is queued or running; the configurator UI is inert meanwhile. */
52
+ readonly exporting$: ObservableWithLastValue<boolean>;
38
53
  constructor(_context: ExportSystemContext);
39
54
  /**
40
55
  * Pauses the frameloop of the given canvas mount until the returned release has been called as
@@ -55,6 +70,13 @@ export declare class ExportSystem<ENTITIES extends DomainEntity<string>, ALLOWED
55
70
  navigationState?: NAVIGATION_STATES;
56
71
  selectedEntityId?: DomainEntityId<ENTITY_TYPE>;
57
72
  }, job: (props: ExportJobArguments<ENTITIES, $MAP[ENTITY_TYPE], $MAP[ALLOWED_ROOT_ENTITIES['type']]>) => Promise<R>) => ExportJob<EXPORT_STATES, ENTITIES, TOOL_STATES, VIEW_STATES, NAVIGATION_STATES, R>;
58
- runExportJobs: <R>(jobs: ReadonlyArray<ExportJob<EXPORT_STATES, ENTITIES, TOOL_STATES, VIEW_STATES, NAVIGATION_STATES, R>>) => Promise<R[]>;
73
+ /**
74
+ * Queues the jobs as one export run behind every run already queued and resolves with their
75
+ * results in order. While the run is queued or running, `exporting$` is true. Afterwards the store
76
+ * shows the committed state current at that moment, not the one the run started from.
77
+ */
78
+ runExportJobs: <R>(jobs: ReadonlyArray<ExportJob<EXPORT_STATES, ENTITIES, TOOL_STATES, VIEW_STATES, NAVIGATION_STATES, R>>, options?: ExportRunOptions) => Promise<R[]>;
79
+ private readonly _awaitSettledState;
80
+ private readonly _runExportJobs;
59
81
  initialize: () => void;
60
82
  }