@salsita-ai/sdk 0.50.0-develop.9feae4851f → 0.50.0-develop.a4dde5f252

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 (252) hide show
  1. package/README.md +2 -2
  2. package/dist/admin/create-admin-pages-from-spec.d.ts +1 -1
  3. package/dist/application-spec/generate-application/code/project-scaffolding.d.ts +1 -1
  4. package/dist/application-spec/generate-application/code/project-scaffolding.js +2 -2
  5. package/dist/application-spec/generate-application/code/templates/domain-spec/generate-domain-block.js +36 -36
  6. package/dist/application-spec/generate-application/code/templates/domain-spec/generate-invariants.d.ts +1 -1
  7. package/dist/application-spec/generate-application/code/templates/domain-spec/generate-invariants.js +1 -1
  8. package/dist/application-spec/generate-application/code/templates/domain.js +1 -1
  9. package/dist/application-spec/generate-application/code/templates/index.d.ts +0 -6
  10. package/dist/application-spec/generate-application/code/templates/index.js +1 -1
  11. package/dist/application-spec/generate-application/code/templates/package-json.js +1 -1
  12. package/dist/application-spec/generate-application/code/templates/skills/phoenix-create-configurator-skill.js +66 -42
  13. package/dist/application-spec/generate-application/code/templates/skills/sdk-content.d.ts +10 -12
  14. package/dist/application-spec/generate-application/code/templates/skills/sdk-content.js +6 -8
  15. package/dist/application-spec/parse-application-files/raw-spec/raw-assistant.js +1 -1
  16. package/dist/client/phoenix-api.d.ts +5 -3
  17. package/dist/configurator/application-runtime/extensions/systems/assistant-system/assistant-system.d.ts +34 -1
  18. package/dist/configurator/application-runtime/extensions/systems/assistant-system/assistant-system.js +1 -1
  19. package/dist/configurator/pages/DefaultConfiguratorLayout.d.ts +6 -0
  20. package/dist/configurator/pages/DefaultConfiguratorLayout.js +1 -0
  21. package/dist/configurator/pages/LoadedConfiguratorDetailPage.js +1 -1
  22. package/dist/docs/index.json +1 -1
  23. package/dist/docs/integrations/woocommerce/local-development.mdx +1 -1
  24. package/dist/docs/phoenix/configurator-interface.mdx +33 -6
  25. package/dist/docs/phoenix/expression-context.mdx +11 -3
  26. package/dist/docs/phoenix/getting-started.mdx +1 -1
  27. package/dist/docs/phoenix/phoenix-cli.mdx +17 -26
  28. package/dist/docs/phoenix/project-structure.mdx +5 -3
  29. package/dist/docs/phoenix/quickstart.mdx +2 -2
  30. package/dist/docs/phoenix/sdk-builder-workflow.mdx +2 -2
  31. package/dist/docs/phoenix/webhooks.mdx +1 -1
  32. package/dist/docs/ux-guidelines/choosing-a-control.mdx +11 -0
  33. package/dist/domain/admin-domain/implementation/common-admin-expressions.d.ts +10 -10
  34. package/dist/domain/admin-domain/index.d.ts +7 -2
  35. package/dist/domain/application/GetApplicationWrapperHandler.d.ts +1 -1
  36. package/dist/domain/application/GetThemeHandler.d.ts +1 -1
  37. package/dist/domain/application/GetWebComponentOptions.d.ts +1 -1
  38. package/dist/domain/application/admin-page-factories.d.ts +1 -2
  39. package/dist/domain/application/application-config.d.ts +3 -24
  40. package/dist/domain/application/assistant-mutations.d.ts +2 -1
  41. package/dist/domain/application/assistant-recipe-groups.d.ts +1 -1
  42. package/dist/domain/application/configurator-builder.d.ts +1 -1
  43. package/dist/domain/application/create-initial-published-data.d.ts +1 -2
  44. package/dist/domain/application/index.d.ts +1 -4
  45. package/dist/domain/application/index.js +1 -1
  46. package/dist/domain/application/published-data/DataRowValidatorHandler.d.ts +1 -1
  47. package/dist/domain/application/published-data/FilterPublishedChunksHandler.d.ts +1 -1
  48. package/dist/domain/application/published-data/MapPublishedChunkedDataHandler.d.ts +1 -1
  49. package/dist/domain/application/published-data/MapPublishedRevisionDataHandler.d.ts +1 -1
  50. package/dist/domain/application/published-data/SheetValidatorHandler.d.ts +1 -1
  51. package/dist/domain/application/published-data/build-legend-data.d.ts +1 -1
  52. package/dist/domain/application/published-data/format-field-type.d.ts +1 -1
  53. package/dist/domain/application/published-data/map-data-to-sheet.d.ts +1 -1
  54. package/dist/domain/application/published-data/map-published-chunked-data.d.ts +2 -2
  55. package/dist/domain/application/published-data/validate-sheet-data.d.ts +1 -1
  56. package/dist/domain/core/action/action-factory.d.ts +2 -2
  57. package/dist/domain/core/action/action.d.ts +19 -14
  58. package/dist/domain/core/component/domain-component-optic.d.ts +30 -40
  59. package/dist/domain/core/component/domain-component-optic.js +2 -2
  60. package/dist/domain/core/component/legacy-filter.d.ts +2 -17
  61. package/dist/domain/core/component/system-component-filter.d.ts +23 -18
  62. package/dist/domain/core/data-schema/data-cell-value.d.ts +4 -0
  63. package/dist/domain/core/data-schema/data-cell-value.js +1 -0
  64. package/dist/domain/core/data-schema/data-schema-array-field.d.ts +5 -0
  65. package/dist/domain/core/data-schema/data-schema-array-field.js +1 -0
  66. package/dist/domain/core/data-schema/data-schema-field-object.d.ts +4 -0
  67. package/dist/domain/core/data-schema/data-schema-field-object.js +1 -0
  68. package/dist/domain/core/data-schema/data-schema-field.d.ts +2 -0
  69. package/dist/domain/core/data-schema/data-schema-field.js +1 -0
  70. package/dist/domain/core/data-schema/data-schema-localized-field.d.ts +4 -0
  71. package/dist/domain/core/data-schema/data-schema-localized-field.js +1 -0
  72. package/dist/domain/core/data-schema/data-schema-primitive-field.d.ts +5 -0
  73. package/dist/domain/core/data-schema/data-schema-primitive-field.js +1 -0
  74. package/dist/domain/core/data-schema/data-schema.d.ts +2 -0
  75. package/dist/domain/core/data-schema/data-schema.js +1 -0
  76. package/dist/domain/core/data-schema/index.d.ts +7 -0
  77. package/dist/domain/core/data-schema/index.js +1 -0
  78. package/dist/domain/core/domain.d.ts +2 -2
  79. package/dist/domain/core/emission/index.d.ts +8 -0
  80. package/dist/domain/core/emission/index.js +1 -0
  81. package/dist/domain/core/emission/reset-state-emission.d.ts +12 -0
  82. package/dist/domain/core/emission/reset-state-emission.js +1 -0
  83. package/dist/domain/core/emission/state-emission.d.ts +4 -0
  84. package/dist/domain/core/emission/state-emission.js +1 -0
  85. package/dist/domain/core/emission/state-update-emission.d.ts +14 -0
  86. package/dist/domain/core/emission/state-update-emission.js +1 -0
  87. package/dist/domain/core/emission/state-validation-result.d.ts +18 -0
  88. package/dist/domain/core/emission/state-validation-result.js +1 -0
  89. package/dist/domain/core/emission/transformation-log.d.ts +2 -0
  90. package/dist/domain/core/emission/transformation-log.js +1 -0
  91. package/dist/domain/core/emission/transformer-log-entry.d.ts +6 -0
  92. package/dist/domain/core/emission/transformer-log-entry.js +1 -0
  93. package/dist/domain/core/emission/transformer-log.d.ts +11 -0
  94. package/dist/domain/core/emission/transformer-log.js +1 -0
  95. package/dist/domain/core/emission/transformer-phase.d.ts +1 -0
  96. package/dist/domain/core/emission/transformer-phase.js +1 -0
  97. package/dist/domain/core/expression/aliased-expressions.d.ts +2 -2
  98. package/dist/domain/core/expression/constant-expressions.d.ts +2 -2
  99. package/dist/domain/core/expression/constant-expressions.js +1 -1
  100. package/dist/domain/core/expression/custom-expression.d.ts +6 -6
  101. package/dist/domain/core/expression/custom-expression.js +1 -1
  102. package/dist/domain/core/expression/expression.d.ts +15 -17
  103. package/dist/domain/core/index.d.ts +2 -0
  104. package/dist/domain/core/invariant/custom-invariants.d.ts +5 -5
  105. package/dist/domain/core/invariant/index.d.ts +1 -1
  106. package/dist/domain/core/invariant/index.js +1 -1
  107. package/dist/domain/core/invariant/invariants.d.ts +4 -67
  108. package/dist/domain/core/invariant/invariants.js +1 -1
  109. package/dist/domain/core/invariant/violation.d.ts +1 -1
  110. package/dist/domain/core/relation/reference-component.d.ts +2 -2
  111. package/dist/domain/core/state/migrations.d.ts +14 -45
  112. package/dist/domain/core/state/migrations.js +1 -1
  113. package/dist/domain/domain-builder.d.ts +1 -1
  114. package/dist/domain/domain-builder.js +1 -1
  115. package/dist/domain/domain-context/action-system/action-system.d.ts +2 -2
  116. package/dist/domain/domain-context/action-system/action-system.js +1 -1
  117. package/dist/domain/domain-context/action-system/index.d.ts +1 -0
  118. package/dist/domain/domain-context/action-system/index.js +1 -0
  119. package/dist/domain/domain-context/analytics-system/index.d.ts +2 -0
  120. package/dist/domain/domain-context/analytics-system/index.js +1 -0
  121. package/dist/domain/domain-context/create-transformer.d.ts +1 -1
  122. package/dist/domain/domain-context/dimension-system/dimension-system.d.ts +2 -2
  123. package/dist/domain/domain-context/dimension-system/dimension-system.js +1 -1
  124. package/dist/domain/domain-context/dimension-system/index.d.ts +1 -0
  125. package/dist/domain/domain-context/dimension-system/index.js +1 -0
  126. package/dist/domain/domain-context/domain-context-systems.d.ts +7 -7
  127. package/dist/domain/domain-context/index.d.ts +8 -17
  128. package/dist/domain/domain-context/index.js +1 -1
  129. package/dist/domain/domain-context/navigation-system/index.d.ts +3 -0
  130. package/dist/domain/domain-context/navigation-system/index.js +1 -0
  131. package/dist/domain/domain-context/navigation-system/navigation-system.d.ts +2 -3
  132. package/dist/domain/domain-context/navigation-system/navigation-system.js +1 -1
  133. package/dist/domain/domain-context/navigation-system/selection-transformers.d.ts +1 -1
  134. package/dist/domain/domain-context/recipe-system/index.d.ts +1 -0
  135. package/dist/domain/domain-context/recipe-system/index.js +1 -0
  136. package/dist/domain/domain-context/recipe-system/recipe-cooker-system.d.ts +1 -1
  137. package/dist/domain/domain-context/recipe-system/recipe-cooker-system.js +1 -1
  138. package/dist/domain/domain-context/relation-transformers/cascade-deletes.d.ts +2 -2
  139. package/dist/domain/domain-context/relation-transformers/cascade-ref-pruning.d.ts +2 -2
  140. package/dist/domain/domain-context/relation-transformers/create-entity-refs.d.ts +2 -2
  141. package/dist/domain/domain-context/relation-transformers/index.d.ts +4 -0
  142. package/dist/domain/domain-context/relation-transformers/index.js +1 -0
  143. package/dist/domain/domain-context/relation-transformers/replace-refs.d.ts +2 -2
  144. package/dist/domain/domain-context/store-system/index.d.ts +4 -0
  145. package/dist/domain/domain-context/store-system/index.js +1 -0
  146. package/dist/domain/domain-context/store-system/reconcile-projected-entities.js +1 -1
  147. package/dist/domain/domain-context/store-system/store-system.d.ts +2 -35
  148. package/dist/domain/domain-context/store-system/store-system.js +2 -2
  149. package/dist/domain/domain-context/store-system/system-state.d.ts +1 -1
  150. package/dist/domain/domain-context/transformers/actions-builder.js +1 -1
  151. package/dist/domain/domain-context/transformers/auto-snap-transformer.d.ts +5 -7
  152. package/dist/domain/domain-context/transformers/auto-snap-transformer.js +1 -1
  153. package/dist/domain/domain-context/transformers/chain-builder.d.ts +1 -1
  154. package/dist/domain/domain-context/transformers/context.d.ts +2 -3
  155. package/dist/domain/domain-context/transformers/context.js +1 -1
  156. package/dist/domain/domain-context/transformers/entity-builder.js +1 -1
  157. package/dist/domain/domain-context/transformers/index.d.ts +1 -1
  158. package/dist/domain/domain-context/transformers/index.js +1 -1
  159. package/dist/domain/domain-context/transformers/when-builder.d.ts +2 -3
  160. package/dist/domain/domain-context/transformers/when-builder.js +1 -1
  161. package/dist/domain/domain-context/undo-system.d.ts +2 -3
  162. package/dist/domain/domain-runtime/domain-context/action-context.d.ts +11 -0
  163. package/dist/domain/domain-runtime/domain-context/action-context.js +1 -0
  164. package/dist/domain/domain-runtime/domain-context/domain-context.js +1 -1
  165. package/dist/domain/domain-runtime/domain-context/expression-context.d.ts +8 -0
  166. package/dist/domain/domain-runtime/domain-context/expression-context.js +1 -0
  167. package/dist/domain/domain-runtime/domain-context/index.d.ts +3 -0
  168. package/dist/domain/domain-runtime/domain-context/runtime-expression-context.d.ts +18 -0
  169. package/dist/domain/domain-runtime/domain-context/runtime-expression-context.js +1 -0
  170. package/dist/domain/domain-runtime/domain-context/selectors.d.ts +5 -10
  171. package/dist/domain/domain-runtime/domain-context/selectors.js +1 -1
  172. package/dist/domain/domain-runtime/domain-runtime.d.ts +52 -52
  173. package/dist/domain/domain-runtime/spec/index.d.ts +2 -0
  174. package/dist/domain/domain-runtime/spec/index.js +1 -1
  175. package/dist/domain/domain-runtime/spec/initialize.js +1 -1
  176. package/dist/domain/domain-runtime/spec/invariant-builder.d.ts +26 -0
  177. package/dist/domain/domain-runtime/spec/invariant-builder.js +1 -0
  178. package/dist/domain/{application → domain-runtime/spec/module-checks}/element-registration.d.ts +2 -2
  179. package/dist/domain/domain-runtime/spec/module-checks/element-registration.js +1 -0
  180. package/dist/domain/{application → domain-runtime/spec/module-checks}/handler-registration.d.ts +1 -1
  181. package/dist/domain/domain-runtime/spec/module-checks/handler-registration.js +1 -0
  182. package/dist/domain/domain-runtime/spec/module-checks/index.d.ts +8 -0
  183. package/dist/domain/domain-runtime/spec/module-checks/index.js +1 -0
  184. package/dist/domain/{project-modules → domain-runtime/spec/module-checks}/project-modules.d.ts +0 -16
  185. package/dist/domain/domain-runtime/spec/module-checks/project-modules.js +1 -0
  186. package/dist/domain/domain-runtime/spec/module-checks/resolve-action-modules.d.ts +4 -0
  187. package/dist/domain/domain-runtime/spec/module-checks/resolve-action-modules.js +1 -0
  188. package/dist/domain/domain-runtime/spec/module-checks/resolve-component-optic-modules.d.ts +4 -0
  189. package/dist/domain/domain-runtime/spec/module-checks/resolve-component-optic-modules.js +1 -0
  190. package/dist/domain/domain-runtime/spec/module-checks/resolve-expression-modules.d.ts +15 -0
  191. package/dist/domain/domain-runtime/spec/module-checks/resolve-expression-modules.js +1 -0
  192. package/dist/domain/{application → domain-runtime/spec/module-checks}/resolve-imported-transformers.d.ts +4 -5
  193. package/dist/domain/domain-runtime/spec/module-checks/resolve-imported-transformers.js +1 -0
  194. package/dist/domain/domain-runtime/spec/module-checks/resolve-invariant-modules.d.ts +6 -0
  195. package/dist/domain/domain-runtime/spec/module-checks/resolve-invariant-modules.js +1 -0
  196. package/dist/domain/domain-runtime/spec/selectors.d.ts +5 -6
  197. package/dist/domain/domain-runtime/spec/selectors.js +1 -1
  198. package/dist/domain/domain-runtime/spec/types.d.ts +3 -2
  199. package/dist/domain/index.d.ts +0 -1
  200. package/dist/domain/index.js +1 -1
  201. package/dist/domain/mocks/create-mock-domain-runtime-services.d.ts +1 -1
  202. package/dist/domain/mocks/create-mock-domain-runtime.d.ts +10 -10
  203. package/dist/domain/projections/blender-projections/blender-projections.d.ts +1 -2
  204. package/dist/domain/projections/blender-projections/camera.d.ts +1 -1
  205. package/dist/domain/projections/collision-projections.d.ts +1 -1
  206. package/dist/domain/projections/composition-projections.d.ts +1 -1
  207. package/dist/domain/projections/constraint-projections.d.ts +1 -1
  208. package/dist/domain/projections/index.d.ts +1 -1
  209. package/dist/domain/projections/position-projections.d.ts +1 -1
  210. package/dist/domain/projections/projected-entities-projections.d.ts +1 -1
  211. package/dist/domain/projections/snapping-projections.d.ts +1 -1
  212. package/dist/domain/projections/validation-projections.d.ts +3 -19
  213. package/dist/domain/sketching/sketching-projections.d.ts +1 -1
  214. package/dist/domain/system-components/phoenix-entity-elements/admin-data-table-sidebar.d.ts +1 -1
  215. package/dist/domain/system-components/phoenix-layout-elements/admin-connected-resource-page.d.ts +1 -1
  216. package/dist/domain/system-components/phoenix-layout-elements/admin-data-table-actions.d.ts +1 -1
  217. package/dist/domain/system-components/phoenix-layout-elements/admin-data-table-page.d.ts +1 -1
  218. package/dist/domain/utils/memo-eval.d.ts +2 -2
  219. package/dist/legacy/domain-cli/gen/constant-expressions.js +1 -3
  220. package/dist/legacy/domain-cli/gen/implementation.js +7 -11
  221. package/dist/legacy/domain-cli/gen/invariants.js +5 -3
  222. package/dist/legacy/domain-cli/utils/cache.d.ts +1 -1
  223. package/dist/legacy/domain-cli/utils/cache.js +1 -1
  224. package/dist/legacy/phoenix-cli/project-gen/generators/file-templates/agents/claude-md.d.ts +1 -1
  225. package/dist/legacy/phoenix-cli/project-gen/generators/file-templates/agents/cursor-rules.d.ts +1 -1
  226. package/dist/legacy/phoenix-cli/project-gen/generators/file-templates/agents/shared-sdk-content.d.ts +1 -1
  227. package/dist/legacy/phoenix-cli/project-gen/generators/file-templates/agents/shared-sdk-content.js +1 -0
  228. package/dist/legacy/phoenix-cli/project-gen/generators/file-templates/domain-index.js +4 -4
  229. package/dist/phoenix/commands/init.d.ts +4 -4
  230. package/dist/phoenix/commands/init.js +1 -1
  231. package/dist/phoenix/commands/prettier.js +1 -1
  232. package/dist/react-sdk/application-runtime/runtime/create-application-runtime.d.ts +20 -20
  233. package/dist/react-sdk/domain-context/common-hooks/use-domain-context.d.ts +5 -5
  234. package/package.json +3 -3
  235. package/dist/application-spec/generate-application/code/templates/env-ci.d.ts +0 -1
  236. package/dist/application-spec/generate-application/code/templates/env-ci.js +0 -25
  237. package/dist/application-spec/generate-application/code/templates/env-example.d.ts +0 -1
  238. package/dist/application-spec/generate-application/code/templates/env-example.js +0 -94
  239. package/dist/application-spec/generate-application/code/templates/private-assets-readme.d.ts +0 -1
  240. package/dist/application-spec/generate-application/code/templates/private-assets-readme.js +0 -24
  241. package/dist/application-spec/generate-application/code/templates/public-assets-readme.d.ts +0 -1
  242. package/dist/application-spec/generate-application/code/templates/public-assets-readme.js +0 -24
  243. package/dist/application-spec/generate-application/code/templates/readme.d.ts +0 -1
  244. package/dist/application-spec/generate-application/code/templates/readme.js +0 -23
  245. package/dist/application-spec/generate-application/code/templates/skills/agents-md.d.ts +0 -1
  246. package/dist/application-spec/generate-application/code/templates/skills/agents-md.js +0 -2
  247. package/dist/domain/application/element-registration.js +0 -1
  248. package/dist/domain/application/handler-registration.js +0 -1
  249. package/dist/domain/application/resolve-imported-transformers.js +0 -1
  250. package/dist/domain/project-modules/index.d.ts +0 -1
  251. package/dist/domain/project-modules/index.js +0 -1
  252. package/dist/domain/project-modules/project-modules.js +0 -1
@@ -1,15 +1,11 @@
1
1
  /**
2
2
  * Documented Phoenix SDK guidance, one Markdown section per entry.
3
3
  *
4
- * The agent guidance shipped into generated projects — the starter `AGENTS.md` and the Phoenix-owned
5
- * agent skills — is composed from these sections, so each documented fact has exactly one home and
6
- * the generated files differ only in which sections they carry.
4
+ * The agent guidance shipped into generated projects — the Phoenix-owned agent skills — is composed
5
+ * from these sections, so each documented fact has exactly one home and the generated files differ
6
+ * only in which sections they carry.
7
7
  */
8
8
  export declare const sdkGuidanceSections: {
9
- /** Opening description of what a Phoenix configurator project is. */
10
- readonly projectIntro: `## Phoenix Configurator Project
11
-
12
- Declarative 3D product configurator built with Phoenix SDK. Domain model defined in YAML, custom logic in TypeScript.`;
13
9
  /** The three everyday `phoenix` package scripts. */
14
10
  readonly buildCommands: `## Build Commands
15
11
  - \`pnpm phoenix build\` — Full production build
@@ -30,6 +26,7 @@ Declarative 3D product configurator built with Phoenix SDK. Domain model defined
30
26
  - \`src/transformers/\` — Automatic state fixers (\`phx.frontend.transformerBuilder\`)
31
27
  - \`src/components/\` — ECS system components (\`phx.domain.attachComponent.withEntity()\`)
32
28
  - \`src/elements/\` — Page-level React components (\`phx.frontend.createCustomPhoenixLayoutElement()\`)
29
+ - A layout element named \`ConfiguratorLayout\` replaces everything the built-in layout renders for a loaded configuration, the finalize page included, so prefer arranging the layout in the spec; when you register it, mount \`LayoutSlots.SharedCanvas\` (from \`@salsita-ai/sdk/configurator\`) in it, or the app loader stays on screen
33
30
  - \`src/handlers/\` — Event & lifecycle handlers
34
31
  - \`src/webhooks/\` — Server-side webhook handlers (\`phx.webhooks.*Builder\`)
35
32
  - \`src/migrations/\` — Schema version migrations (v*.ts)
@@ -78,7 +75,8 @@ The default export must be what the folder's phx factory returns (for example \`
78
75
  readonly doNotEdit: `## Do Not Edit
79
76
  - \`.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
80
77
  - \`tsconfig.json\`, \`eslint.config.js\`, \`prettier.config.js\`, \`.prettierignore\`, \`.linguirc\`, \`.nvmrc\`
81
- - \`src/locales/\` .ts files — compiled from .po files`;
78
+ - \`src/locales/\` .ts files — compiled from .po files
79
+ - \`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\``;
82
80
  /** When and how a deployed domain model gets a new version and a migration, with the recipe for replacing the starter. */
83
81
  readonly modelVersioning: `## Version the Domain Model Once It Is Deployed
84
82
  A 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.
@@ -284,10 +282,10 @@ Rendered appearance is a deliverable, not a side effect. Before presenting any p
284
282
  Inspect and report:
285
283
 
286
284
  1. Geometry. Every part rests on its support. Name where the product meets the ground and confirm its shadow and any selection indicator sit there too. Nothing floats, intersects, or protrudes. Scale and orientation are consistent between parts and match the stated dimensions. Each part is complete and present.
287
- 2. Fidelity. The result matches the client's own photographs, renders, and drawings where supplied. Name any divergence.
288
- 3. Response. Every configurable surface of every part reacts to its control, not only the parts you just changed.
289
- 4. Density. Every decision the user must make is reachable without hunting, and no single property's options dominate the surface holding them.
290
- 5. Loading. How long until a meaningful first frame, and whether anything indicates progress meanwhile.
285
+ 2. Response. Every configurable surface of every part reacts to its control, not only the parts you just changed.
286
+ 3. Loading. How long until a meaningful first frame, and whether anything indicates progress meanwhile.
287
+
288
+ This covers what follows from the change you just made, in the state you are already in. Fidelity against the client's own material, the density of the controls, and every state you did not open belong to an independent reviewer, which the \`phoenix-create-configurator\` skill's Appearance review covers.
291
289
 
292
290
  Report defects you are deferring as defects. Do not call work done, verified, or complete while any of the above is unchecked or failing: say which, and recommend fixing now or later.
293
291
 
@@ -1,12 +1,10 @@
1
- export const sdkGuidanceSections={projectIntro:`## Phoenix Configurator Project
2
-
3
- Declarative 3D product configurator built with Phoenix SDK. Domain model defined in YAML, custom logic in TypeScript.`,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",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- `src/handlers/` — Event & lifecycle handlers\n- `src/webhooks/` — Server-side webhook handlers (`phx.webhooks.*Builder`)\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={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",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 - A layout element named `ConfiguratorLayout` replaces everything the built-in layout renders for a loaded configuration, the finalize page included, so prefer arranging the layout in the spec; when you register it, mount `LayoutSlots.SharedCanvas` (from `@salsita-ai/sdk/configurator`) in it, or the app loader stays on screen\n- `src/handlers/` — Event & lifecycle handlers\n- `src/webhooks/` — Server-side webhook handlers (`phx.webhooks.*Builder`)\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
4
2
  - Components: PascalCase (.tsx)
5
3
  - Utilities: kebab-case (.ts)
6
4
  - Expressions: entity-name.ts
7
5
  - Migrations: v*.ts (sequential)
8
6
  - Specs: kebab-case.yaml
9
- - 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 `createMapPublishedRevisionDataHandler`, `createMapPublishedChunkedDataHandler`, `createDataRowValidatorHandler`, or `createSheetValidatorHandler` is included in the webhook automatically.\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\n- `tsconfig.json`, `eslint.config.js`, `prettier.config.js`, `.prettierignore`, `.linguirc`, `.nvmrc`\n- `src/locales/` .ts files — compiled from .po files",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 `createMapPublishedRevisionDataHandler`, `createMapPublishedChunkedDataHandler`, `createDataRowValidatorHandler`, or `createSheetValidatorHandler` is included in the webhook automatically.\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\n- `tsconfig.json`, `eslint.config.js`, `prettier.config.js`, `.prettierignore`, `.linguirc`, `.nvmrc`\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
10
8
 
11
9
  Get the current editor context data as JSON via \`pnpm phoenix inspect:data\`.
12
10
 
@@ -71,10 +69,10 @@ Rendered appearance is a deliverable, not a side effect. Before presenting any p
71
69
  Inspect and report:
72
70
 
73
71
  1. Geometry. Every part rests on its support. Name where the product meets the ground and confirm its shadow and any selection indicator sit there too. Nothing floats, intersects, or protrudes. Scale and orientation are consistent between parts and match the stated dimensions. Each part is complete and present.
74
- 2. Fidelity. The result matches the client's own photographs, renders, and drawings where supplied. Name any divergence.
75
- 3. Response. Every configurable surface of every part reacts to its control, not only the parts you just changed.
76
- 4. Density. Every decision the user must make is reachable without hunting, and no single property's options dominate the surface holding them.
77
- 5. Loading. How long until a meaningful first frame, and whether anything indicates progress meanwhile.
72
+ 2. Response. Every configurable surface of every part reacts to its control, not only the parts you just changed.
73
+ 3. Loading. How long until a meaningful first frame, and whether anything indicates progress meanwhile.
74
+
75
+ This covers what follows from the change you just made, in the state you are already in. Fidelity against the client's own material, the density of the controls, and every state you did not open belong to an independent reviewer, which the \`phoenix-create-configurator\` skill's Appearance review covers.
78
76
 
79
77
  Report defects you are deferring as defects. Do not call work done, verified, or complete while any of the above is unchecked or failing: say which, and recommend fixing now or later.
80
78
 
@@ -1 +1 @@
1
- import{z as t}from"zod";import{ASSISTANT_AI_PROVIDERS as e,ASSISTANT_MODEL_TIERS as o,ASSISTANT_REASONING_LEVELS as i}from"@salsita-ai/contracts/assistant";export const ASSISTANT_OPTION_COMPONENT_TYPES=["materialSwatch","attributeSelection","distanceDimensionSelect"];export const ASSISTANT_HINTS=["sameForAllInstances","constantDuringSession"];let n=t.string().regex(/^(?!__)[_A-Za-z][_0-9A-Za-z]*$/,'must be a GraphQL name: letters, digits and "_", not starting with a digit or "__"'),r=e=>t.enum(e,{errorMap:()=>({message:`must be one of: ${e.join(", ")}`})}),s=t.object({description:t.string().optional(),as:n.optional()}).strict(),a=t.array(r(ASSISTANT_HINTS)),c=t.object({options:t.object({componentType:r(ASSISTANT_OPTION_COMPONENT_TYPES),instanceName:t.string().min(1,"must not be empty; name the component instance that holds the choices"),as:n.optional(),validate:t.boolean().optional(),allowDryRuns:t.boolean().optional(),hints:a.optional()}).strict().optional()}).strict(),l=t.object({as:t.string().trim().min(1,"must not be empty; remove the key to use the instance name").optional(),description:t.string().optional(),allowDryRuns:t.boolean().optional()}).strict(),p=e=>t.object({hints:a.optional(),groups:t.record(t.string(),l.nullable(),{required_error:`is required: the recipe groups of the type go under "groups", keyed by the instance name of a "${e}" component`})}).strict(),d=t.object({description:t.string().optional(),as:n.optional()}).strict(),m=`is required: declare provider (${e.join(", ")}), model (${o.join(", ")}) and reasoning (${i.join(", ")})`,g=t.object({provider:r(e),model:r(o),reasoning:r(i)},{required_error:m,invalid_type_error:m}).strict();export const rawAssistantSchema=t.object({entities:t.record(t.string(),t.record(t.string(),s.nullable())).optional(),deletable:t.record(t.string(),t.object({}).strict().nullable()).optional(),writable:t.record(t.string(),t.record(t.string(),c.nullable())).optional(),childRecipes:t.record(t.string(),p("childEntityRecipes")).optional(),replaceRecipes:t.record(t.string(),p("replaceEntityRecipes")).optional(),actions:t.record(t.string(),d.nullable()).optional(),instructions:t.string().trim().min(1,"must not be empty; remove the key when there are no instructions").optional(),ai:g}).strict();
1
+ import{z as t}from"zod";import{ASSISTANT_AI_PROVIDERS as e,ASSISTANT_MODEL_TIERS as o,ASSISTANT_REASONING_LEVELS as i}from"@salsita-ai/contracts/assistant";export const ASSISTANT_OPTION_COMPONENT_TYPES=["materialSwatch","attributeSelection","distanceDimensionSelect"];export const ASSISTANT_HINTS=["sameForAllInstances","constantDuringSession"];let n=t.string().regex(/^(?!__)[_A-Za-z][_0-9A-Za-z]*$/,'must be a GraphQL name: letters, digits and "_", not starting with a digit or "__"'),r=e=>t.enum(e,{errorMap:()=>({message:`must be one of: ${e.join(", ")}`})}),s=t.object({description:t.string().optional(),as:n.optional()}).strict(),a=t.array(r(ASSISTANT_HINTS)),l=t.object({options:t.object({componentType:r(ASSISTANT_OPTION_COMPONENT_TYPES),instanceName:t.string().min(1,"must not be empty; name the component instance that holds the choices"),as:n.optional(),validate:t.boolean().optional(),allowDryRuns:t.boolean().optional(),hints:a.optional()}).strict().optional()}).strict(),c=t.object({as:t.string().trim().min(1,"must not be empty; remove the key to use the instance name").optional(),description:t.string().optional(),allowDryRuns:t.boolean().optional()}).strict(),p=e=>t.object({hints:a.optional(),groups:t.record(t.string(),c.nullable(),{required_error:`is required: the recipe groups of the type go under "groups", keyed by the instance name of a "${e}" component`})}).strict(),m=t.object({description:t.string().optional(),as:n.optional()}).strict(),d=`is required: declare provider (${e.join(", ")}), model (${o.join(", ")}) and reasoning (${i.join(", ")})`,u="must be a positive whole number of input tokens; remove the key to use the default of the server",b=t.object({provider:r(e),model:r(o),reasoning:r(i),maxThreadSize:t.number({invalid_type_error:u}).int(u).positive(u).optional()},{required_error:d,invalid_type_error:d}).strict();export const rawAssistantSchema=t.object({entities:t.record(t.string(),t.record(t.string(),s.nullable())).optional(),deletable:t.record(t.string(),t.object({}).strict().nullable()).optional(),writable:t.record(t.string(),t.record(t.string(),l.nullable())).optional(),childRecipes:t.record(t.string(),p("childEntityRecipes")).optional(),replaceRecipes:t.record(t.string(),p("replaceEntityRecipes")).optional(),actions:t.record(t.string(),m.nullable()).optional(),instructions:t.string().trim().min(1,"must not be empty; remove the key when there are no instructions").optional(),ai:b}).strict();
@@ -201,10 +201,10 @@ export interface paths {
201
201
  readonly path?: never
202
202
  readonly cookie?: never
203
203
  }
204
- /** @description Load the caller's latest assistant thread on the configuration with its active run, newest window first. The `x-assistant-thread-continuable` response header is `false` when the thread cannot be continued with this setup or is closed; a chat request then answers 409, and the thread continues once it is compacted. The `x-assistant-previous-thread` response header carries the id of the closed thread this one continues, when there is one. */
204
+ /** @description Load the caller's latest assistant thread on the configuration with its active run, newest window first. The `x-assistant-thread-continuable` response header is `false` when the thread cannot be continued with this setup, has reached its size limit or is closed; a chat request then answers 409, and the thread continues once it is compacted. The `x-assistant-previous-thread` response header carries the id of the closed thread this one continues, when there is one. */
205
205
  readonly get: operations['getAssistantThread']
206
206
  readonly put?: never
207
- /** @description Run the experimental assistant on the caller's thread of the configuration, or replay the run when the request carries `Last-Event-ID`. Answers 409 when the thread cannot be continued with this setup or another run is still answering, and 503 while the server shuts down; sent again, the message reaches another instance. Answers 403 unless the customer has the AI Assistant feature flag on. */
207
+ /** @description Run the experimental assistant on the caller's thread of the configuration, or replay the run when the request carries `Last-Event-ID`. Answers 409 when the thread cannot be continued or another run is still answering, and 503 while the server shuts down; sent again, the message reaches another instance. A 409 for a thread that cannot be continued, because it is closed, was started with another setup or has reached its size limit, carries the `x-assistant-thread-continuable: false` response header; the thread continues once it is compacted. Answers 403 unless the customer has the AI Assistant feature flag on. */
208
208
  readonly post: operations['assistantChat']
209
209
  readonly delete?: never
210
210
  readonly options?: never
@@ -221,7 +221,7 @@ export interface paths {
221
221
  }
222
222
  readonly get?: never
223
223
  readonly put?: never
224
- /** @description Continue the caller's latest assistant thread on the configuration, which cannot be continued with this setup, in a new one: the thread is summarized and closed, and its successor starts from the summary. Answers 409 when there is no thread, the latest one can be continued as it is, or one of its runs is still running, and 502 when the summary could not be made; nothing is closed then and the request can be sent again. */
224
+ /** @description Compact the caller's latest assistant thread on the configuration, which cannot be continued as it is: the thread is summarized and closed, and a new, empty thread is opened that links to it and keeps the summary. Nothing is run; the next chat request runs on the new thread, with the summary in its system prompt. Answers 409 when there is no thread, the latest one can be continued as it is, or one of its runs is still running, and 502 when the summary could not be made; nothing is closed then and the request can be sent again. */
225
225
  readonly post: operations['compactAssistantThread']
226
226
  readonly delete?: never
227
227
  readonly options?: never
@@ -5610,6 +5610,7 @@ export interface operations {
5610
5610
  | boolean
5611
5611
  | {
5612
5612
  readonly ai: {
5613
+ readonly maxThreadSize?: number
5613
5614
  /** @enum {string} */
5614
5615
  readonly model: 'frontier' | 'advanced' | 'balanced' | 'fast'
5615
5616
  /** @enum {string} */
@@ -5832,6 +5833,7 @@ export interface operations {
5832
5833
  | boolean
5833
5834
  | {
5834
5835
  readonly ai: {
5836
+ readonly maxThreadSize?: number
5835
5837
  /** @enum {string} */
5836
5838
  readonly model: 'frontier' | 'advanced' | 'balanced' | 'fast'
5837
5839
  /** @enum {string} */
@@ -103,6 +103,18 @@ export declare class AssistantSystem implements InitializableSystem {
103
103
  private _olderThreadId;
104
104
  /** The resume that is running, so two sends that find the thread locked share one. */
105
105
  private _resuming;
106
+ /**
107
+ * The message the server refused on a chat because its thread cannot be continued, by where it
108
+ * starts in the chat's messages: it is painted, but it is not part of the thread, so a resume
109
+ * leaves it out of the closed thread it paints above the new one.
110
+ */
111
+ private _refused;
112
+ /**
113
+ * The chat a message is being sent on for the first time. When the server refuses that message
114
+ * because the thread cannot be continued, the refusal is not a failure of the chat: the thread is
115
+ * resumed and the message is sent again; see `send`.
116
+ */
117
+ private _firstAttempt;
106
118
  /**
107
119
  * Identity of the chain of threads that is painted. A resumed chat keeps it, because its new
108
120
  * thread continues the same chain; any other new chat starts another one.
@@ -168,12 +180,26 @@ export declare class AssistantSystem implements InitializableSystem {
168
180
  * A thread this session may not continue is resumed first, in a new thread; see `resume`. When
169
181
  * that fails the message is not sent.
170
182
  *
183
+ * The chat can be out of date about that: the thread reached its size limit with the reply before
184
+ * this message, or the assistant's setup changed while the configurator stayed open. The server
185
+ * then refuses the message and says that the thread cannot be continued. The message stays
186
+ * painted while the thread is resumed, and is then sent again on the new thread, with the change
187
+ * record it was first sent with. This happens once: a refusal of the second attempt is the
188
+ * failure it is. When resuming fails, the message is left as any message that failed to send:
189
+ * painted, with the failure as the error of the chat.
190
+ *
191
+ * Accepted: when the server refuses the compaction itself with a conflict, because another tab
192
+ * compacted the thread or a run is still running, the thread is read again (see `resume`) and
193
+ * the refused message is gone without a notice. It takes two tabs or a run racing the send, and
194
+ * the user writes the message again, as after any reload of a thread that failed.
195
+ *
171
196
  * It never rejects: a message the client could not send is reported as the error of the chat.
172
197
  */
173
198
  send: (content: string) => Promise<void>;
174
199
  /**
175
200
  * Makes the chat ready for the next message. A thread the server no longer lets this session
176
- * continue, because the assistant's setup changed since it was started, is compacted there: the
201
+ * continue, because the assistant's setup changed since it was started or the thread reached
202
+ * its size limit, is compacted there: the
177
203
  * server closes it and opens a thread that starts from its summary. The messages painted so far
178
204
  * move to `earlierThreads`, and a new client takes the new thread over.
179
205
  *
@@ -201,6 +227,13 @@ export declare class AssistantSystem implements InitializableSystem {
201
227
  reloadThread: () => void;
202
228
  setOpen: (open: boolean) => void;
203
229
  setInputFocused: (isInputFocused: boolean) => void;
230
+ /**
231
+ * Where the message starts that is being sent on the chat for the first time and that the server
232
+ * refused because its thread cannot be continued, so it never reached the thread; `-1` when there
233
+ * is no such message. A refused continuation of a reply is not one: its message is stored.
234
+ */
235
+ private _findRefusedMessageIndex;
236
+ private _sendOn;
204
237
  private _resume;
205
238
  private _committedState;
206
239
  private _patchPanel;
@@ -1 +1 @@
1
- import{ChatClient as e,fetchServerSentEvents as t}from"@tanstack/ai-client";import{BehaviorSubject as i}from"rxjs";import{skip as a}from"rxjs/operators";import{isShallowEqual as s}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 h,createQueryClientTool as l}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}),d=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(c);_panel=new i(d);_activation=null;_current=null;_finishing=new Set;_olderThreadId=null;_resuming=null;_chain={};_loadingThreadId=null;_recorder;_recording=null;constructor(e){this._context=e;let{configuratorExtensionSelectors:t,domainContextSelectors:i,domainServicesSelectors:a,specSelectors:s}=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:()=>s.getAssistant()?.fields??u,resolveLocalizable:e=>a.getResolveLocalizable()(e)});let r={getGraphQLSystem:()=>t.getGraphQLSystem(),getStoreSystem:()=>i.getStoreSystem(),getActiveMode:()=>i.getActiveMode(),clearChanges:()=>this._recorder.clear(this._committedState())};this._tools=[n(),l(r),h(r)]}chat$=this._chat;panel$=this._panel;initialize=()=>{let{committed:e}=this._context.domainContextSelectors.getStoreSystem().stateSlices;this._recording??=e.pipe(a(1)).subscribe(this._recorder.record),this._recorder.reset(e.value.state),this._endChat(),this._patchPanel(d),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{client:i}=t;i.getIsLoading()||null===i.getResumeState()||i.stop();let a={changes:this._recorder.take(this._committedState())};try{await i.sendMessage({content:e,metadata:a})}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()=>{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 a=await e.authorizedClient.getAssistantEarlierThread(e.configurationId,t,50);if(this._chain!==i||this._olderThreadId!==t)return;this._olderThreadId=a.previousThreadId,this._chat.next({...this._chat.value,earlierThreads:[{threadId:t,messages:a.messages},...this._chat.value.earlierThreads],hasEarlierThread:null!==a.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()};setOpen=e=>{this._patchPanel(e?{open:e,hasUnreadReply:!1}:{open:e,isInputFocused:!1})};setInputFocused=e=>{this._patchPanel({isInputFocused:e})};_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:a,hasEarlierThread:s}=this._chat.value;return e.generation.active=!1,this._current=null,e.client.stop(),e.dispose(),this._chat.next({...c,earlierThreads:[...a,{threadId:t,messages:i}],hasEarlierThread:s,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}};_committedState=()=>this._context.domainContextSelectors.getStoreSystem().stateSlices.committed.value.state;_patchPanel=e=>{let t={...this._panel.value,...e};s(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},a=this._context.getConfigurationId();this._takeOverThread(a);let r=this._context.getAuthorizedClient(),n=()=>r.getAssistantChatConnection(a),o=e=>{if(!i.active)return;let t={...this._chat.value,...e};s(t,this._chat.value)||this._chat.next(t)},h=async(e,t)=>{let a=await fetch(e,t).finally(()=>o({isLoaded:!0})),s=a.headers.get("x-assistant-thread-continuable");return null!=s&&i.active&&0===this._chat.value.earlierThreads.length&&(this._olderThreadId=a.headers.get("x-assistant-previous-thread")),o({failedStatus:a.ok?void 0:a.status,...null!=s&&{isContinuable:"false"!==s,hasEarlierThread:null!==this._olderThreadId}}),a},l=async()=>({headers:await n().headers(),fetchClient:h}),c=t(()=>n().url,l),d=t(()=>`${n().url}/runs`,l),u=this._tools.map(e=>({...e,execute:(t,a)=>i.active?e.execute?.(t,a):_})),m=new e({threadId:`assistant:${a}`,connection:{...c,joinRun:d.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(m)&&(i.active?t&&!this._panel.value.open&&this._patchPanel({hasUnreadReply:!0}):f())},onErrorChange:e=>{i.active&&e!==this._chat.value.error&&(o({error:e}),e&&this._context.domainServicesSelectors.getLogger().error("The assistant request failed",{status:this._chat.value.failedStatus},e))}}),v=!1,f=()=>{v||(v=!0,this._finishing.delete(p),m.dispose())},p={client:m,configurationId:a,generation:i,authorizedClient:r,getConnection:n,dispose:f};this._current=p,m.attach()};_retireChat=()=>{let e=this._current;return this._current=null,e&&(e.generation.active=!1),this._chain={},this._olderThreadId=null,this._resuming=null,this._loadingThreadId=null,this._chat.value!==c&&this._chat.next(c),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 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 h}from"./change-recorder.js";import{createApplyClientTool as o,createQueryClientTool as l}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}),d=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(c);_panel=new i(d);_activation=null;_current=null;_finishing=new Set;_olderThreadId=null;_resuming=null;_refused=null;_firstAttempt=null;_chain={};_loadingThreadId=null;_recorder;_recording=null;constructor(e){this._context=e;let{configuratorExtensionSelectors:t,domainContextSelectors:i,domainServicesSelectors:s,specSelectors:a}=e;this._recorder=new h({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.getAssistant()?.fields??u,resolveLocalizable:e=>s.getResolveLocalizable()(e)});let r={getGraphQLSystem:()=>t.getGraphQLSystem(),getStoreSystem:()=>i.getStoreSystem(),getActiveMode:()=>i.getActiveMode(),clearChanges:()=>this._recorder.clear(this._committedState())};this._tools=[n(),l(r),o(r)]}chat$=this._chat;panel$=this._panel;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(d),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()};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({...c,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}};_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=()=>r.getAssistantChatConnection(s),h=e=>{if(!i.active)return;let t={...this._chat.value,...e};a(t,this._chat.value)||this._chat.next(t)},o=async(e,t)=>{let s=await fetch(e,t).finally(()=>h({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")),h({failedStatus:s.ok?void 0:s.status,...null!=a&&{isContinuable:"false"!==a,hasEarlierThread:null!==this._olderThreadId}}),s},l=async()=>({headers:await n().headers(),fetchClient:o}),c=t(()=>n().url,l),d=t(()=>`${n().url}/runs`,l),u=this._tools.map(e=>({...e,execute:(t,s)=>i.active?e.execute?.(t,s):_})),m=new e({threadId:`assistant:${s}`,connection:{...c,joinRun:d.joinRun},persistence:!0,history:{pageSize:50},tools:u,onMessagesChange:e=>h({messages:e}),onLoadingChange:e=>{let t=i.active&&this._chat.value.isLoading;h({isLoading:e}),!e&&g(m)&&(i.active?t&&!this._panel.value.open&&this._patchPanel({hasUnreadReply:!0}):v())},onErrorChange:e=>{i.active&&e!==this._chat.value.error&&(!e||-1===this._findRefusedMessageIndex(p))&&(h({error:e}),e&&this._context.domainServicesSelectors.getLogger().error("The assistant request failed",{status:this._chat.value.failedStatus},e))}}),f=!1,v=()=>{f||(f=!0,this._finishing.delete(p),m.dispose())},p={client:m,configurationId:s,generation:i,authorizedClient:r,getConnection:n,dispose:v};this._current=p,m.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!==c&&this._chat.next(c),e};_endChat=()=>{let e=this._retireChat();if(e){if(!e.client.getIsLoading()&&g(e.client))return void e.dispose();this._finishing.add(e)}}}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * The built-in UI of a loaded configuration: the layout the spec's `layout` selects, in owner or
3
+ * read mode. `LoadedConfiguratorDetailPage` renders it when the project registers no layout element
4
+ * named `ConfiguratorLayout`.
5
+ */
6
+ export declare const DefaultConfiguratorLayout: () => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1 @@
1
+ import{jsx as r,jsxs as e,Fragment as t}from"react/jsx-runtime";import{useMemo as a}from"react";import{match as o}from"ts-pattern";import{useAccessService as i}from"#package/react-sdk";import{useLayoutSystemConfig as m}from"../configurator-context/index.js";import{Layout as n,LayoutCustomOwnerUi as u,LayoutCustomReadUi as s,LayoutDialog as d,Legacy as w}from"../ui/index.js";export const DefaultConfiguratorLayout=()=>{let{accessInfo:p}=i(),y=m(),{ownerUi:l,readUi:c}=a(()=>o(y.layout).with("simple",()=>({ownerUi:r(w.SimpleOwnerLayout,{}),readUi:r(w.SummaryReadLayout,{})})).with("tabs",()=>({ownerUi:r(w.TabsOwnerLayout,{}),readUi:r(w.SummaryReadLayout,{})})).with("steps",()=>({ownerUi:r(w.StepsOwnerLayout,{}),readUi:r(w.SummaryReadLayout,{})})).with("universal",()=>({ownerUi:r(n,{}),readUi:r(n,{})})).with("custom",()=>({ownerUi:r(u,{}),readUi:r(s,{})})).exhaustive(),[y.layout]);return e(t,{children:[p.viewMode?c:l,"universal"===y.layout&&e(t,{children:[r(w.CommonFinalizePage,{}),r(d,{})]})]})};
@@ -1 +1 @@
1
- import{jsx as e,jsxs as o,Fragment as i}from"react/jsx-runtime";import r from"mui-modal-provider";import{memo as n,useCallback as t,useEffect as a,useMemo as d}from"react";import{match as s}from"ts-pattern";import{InvalidAccessKeyError as l}from"#package/client";import{BuiltInLoadingPhase as u,CommonAnalyticsFactory as m,DomainLifecycleBoundary as c,LoadedDataProvider as f,useAccessService as p,useAnalyticsService as g,useAuthorizedClient as v,useConfiguratorDomainFactory as w,useConfiguratorLoadStatus as y,useConfiguratorRawLoadedData as h,useExpectPhase as I,useLoadedDataActions as j,useLoadingError as x,useLoadingPhase as C,useLogger as b,useRoutingService as k,useSpecAssistant as L}from"#package/react-sdk";import{useConfigurator as U}from"../application-runtime/index.js";import{IntegrationSubscription as M,OnConfiguratorReadyProvider as S,ViewportStateSync as E,useLayoutSystemConfig as F}from"../configurator-context/index.js";import{Layout as R,Legacy as D}from"../ui/index.js";import{LayoutCustomOwnerUi as T,LayoutCustomReadUi as P,LayoutDialog as A}from"../ui/phoenix-layout-elements/index.js";import{ConfiguratorNotFoundPage as O}from"./ConfiguratorNotFoundPage.js";import{DevContextFallback as V}from"./error-boundaries/DevContextFallback.js";import{DevLoadingFallback as q}from"./error-boundaries/DevLoadingFallback.js";import{LoadingErrorBoundary as z}from"./error-boundaries/LoadingErrorBoundary.js";import{useLoad as B}from"./use-load.js";let G=n(n=>{let{viewMode:t}=n,l=F();(()=>{let{assistantSystem:e}=U(),o=null!=L();a(()=>{if(o)return e.activate()},[e,o])})();let{ownerUi:u,readUi:m}=d(()=>s(l.layout).with("simple",()=>({ownerUi:e(D.SimpleOwnerLayout,{}),readUi:e(D.SummaryReadLayout,{})})).with("tabs",()=>({ownerUi:e(D.TabsOwnerLayout,{}),readUi:e(D.SummaryReadLayout,{})})).with("steps",()=>({ownerUi:e(D.StepsOwnerLayout,{}),readUi:e(D.SummaryReadLayout,{})})).with("universal",()=>({ownerUi:e(R,{}),readUi:e(R,{})})).with("custom",()=>({ownerUi:e(T,{}),readUi:e(P,{})})).exhaustive(),[l.layout]);return e(r,{children:o(S,{children:[t?m:u,"universal"===l.layout&&o(i,{children:[e(D.CommonFinalizePage,{}),e(A,{})]})]})})});export const LoadedConfiguratorDetailPage=()=>{I(u.SceneAssets);let{configurationId:i,accessInfo:r}=p(),n=v(),a=k(),d=b(),s=g(),{startConfiguratorLoad:L,completeConfiguratorLoad:U,failConfiguratorLoad:S,resetConfiguratorLoad:F}=j(),{isLoading:R,isError:D}=y(),T=h(),P=w(),A=t(()=>r.viewMode&&void 0!==r.revisionId?n.getConfigurationRevision(i,r.revisionId):n.getConfiguration(i),[r.revisionId,r.viewMode,n,i]),N=t(()=>{L()},[L]),H=t(e=>{U({initialRevisionId:e.revisionId,initialEntities:e.data,modelVersion:e.modelVersion,rootEntityType:e.rootEntityType,schemaIdentity:P}),s.push(m.ConfigurationLoaded(e.configurationId,e.revisionId,r.viewMode?"read":"owner")),d.appendToGlobalContext({configurationId:e.configurationId,revisionId:e.revisionId,mode:r.viewMode}),d.info("Configuration loaded.")},[r.viewMode,s,U,P,d]),J=t(e=>{if(S(e),e instanceof l)return void a.rejectAccessToken(i,r.raw);let o=e instanceof Error?e:Error("Configuration loading failed with an unknown error.",{cause:e});d.error("Configuration loading failed because the API request did not succeed. Verify the configuration ID and access token.",{configurationId:i},o)},[r.raw,i,S,d,a]),K=t(()=>{F()},[F]);return(B({key:[n,i,r.revisionId,r.viewMode],load:A,onStart:N,onSuccess:H,onError:J,onCleanup:K}),C({id:u.DataFetch,label:"Loading configuration…"},R),x(D),D)?e(O,{}):null===T?null:e(f,{value:T,children:e(z,{fallbackRender:o=>e(q,{...o}),children:e(c,{type:"configurator",children:e(z,{FallbackComponent:V,children:o(M,{children:[e(E,{}),e(G,{viewMode:r.viewMode})]})})})})})};
1
+ import{jsx as o,jsxs as r}from"react/jsx-runtime";import e from"mui-modal-provider";import{memo as i,useCallback as n,useEffect as t}from"react";import{InvalidAccessKeyError as a}from"#package/client";import{BuiltInLoadingPhase as d,CommonAnalyticsFactory as s,DomainLifecycleBoundary as l,LoadedDataProvider as u,PhoenixLayoutElement as c,useAccessService as f,useAnalyticsService as m,useAuthorizedClient as g,useConfiguratorDomainFactory as p,useConfiguratorLoadStatus as v,useConfiguratorRawLoadedData as I,useExpectPhase as C,useLoadedDataActions as h,useLoadingError as j,useLoadingPhase as k,useLogger as w,useRoutingService as y,useSpecAssistant as b}from"#package/react-sdk";import{useConfigurator as x}from"../application-runtime/index.js";import{IntegrationSubscription as E,OnConfiguratorReadyProvider as L,ViewportStateSync as D}from"../configurator-context/index.js";import{ConfiguratorNotFoundPage as M}from"./ConfiguratorNotFoundPage.js";import{DefaultConfiguratorLayout as F}from"./DefaultConfiguratorLayout.js";import{DevContextFallback as S}from"./error-boundaries/DevContextFallback.js";import{DevLoadingFallback as T}from"./error-boundaries/DevLoadingFallback.js";import{LoadingErrorBoundary as A}from"./error-boundaries/LoadingErrorBoundary.js";import{useLoad as P}from"./use-load.js";let R=i(()=>((()=>{let{assistantSystem:o}=x(),r=null!=b();t(()=>{if(r)return o.activate()},[o,r])})(),o(e,{children:o(L,{children:o(c,{name:"ConfiguratorLayout",defaultSlot:F})})})));export const LoadedConfiguratorDetailPage=()=>{C(d.SceneAssets);let{configurationId:e,accessInfo:i}=f(),t=g(),c=y(),b=w(),x=m(),{startConfiguratorLoad:L,completeConfiguratorLoad:F,failConfiguratorLoad:V,resetConfiguratorLoad:q}=h(),{isLoading:B,isError:G}=v(),N=I(),z=p(),H=n(()=>i.viewMode&&void 0!==i.revisionId?t.getConfigurationRevision(e,i.revisionId):t.getConfiguration(e),[i.revisionId,i.viewMode,t,e]),J=n(()=>{L()},[L]),K=n(o=>{F({initialRevisionId:o.revisionId,initialEntities:o.data,modelVersion:o.modelVersion,rootEntityType:o.rootEntityType,schemaIdentity:z}),x.push(s.ConfigurationLoaded(o.configurationId,o.revisionId,i.viewMode?"read":"owner")),b.appendToGlobalContext({configurationId:o.configurationId,revisionId:o.revisionId,mode:i.viewMode}),b.info("Configuration loaded.")},[i.viewMode,x,F,z,b]),O=n(o=>{if(V(o),o instanceof a)return void c.rejectAccessToken(e,i.raw);let r=o instanceof Error?o:Error("Configuration loading failed with an unknown error.",{cause:o});b.error("Configuration loading failed because the API request did not succeed. Verify the configuration ID and access token.",{configurationId:e},r)},[i.raw,e,V,b,c]),Q=n(()=>{q()},[q]);return(P({key:[t,e,i.revisionId,i.viewMode],load:H,onStart:J,onSuccess:K,onError:O,onCleanup:Q}),k({id:d.DataFetch,label:"Loading configuration…"},B),j(G),G)?o(M,{}):null===N?null:o(u,{value:N,children:o(A,{fallbackRender:r=>o(T,{...r}),children:o(l,{type:"configurator",children:o(A,{FallbackComponent:S,children:r(E,{children:[o(D,{}),o(R,{})]})})})})})};
@@ -1,5 +1,5 @@
1
1
  {
2
- "generatedAt": "2026-10-13T13:17:45.923Z",
2
+ "generatedAt": "2026-10-13T19:13:10.084Z",
3
3
  "categories": [
4
4
  {
5
5
  "name": "Phoenix",
@@ -133,7 +133,7 @@ The value of `WOOCOMMERCE_API_KEY` must correspond to the value of the 'API key'
133
133
  settings page and `WOOCOMMERCE_PRODUCT_NAME` must correspond to the name of the product template you created.
134
134
 
135
135
  - run development server (from this repo): `pnpm dev:server`
136
- - prepare local deployment (from the implementation project): `pnpm deployment:prepare-local`
136
+ - prepare local deployment (from the implementation project): `pnpm phoenix deployment:prepare-local`
137
137
  - start the dev process (from the implementation project): `pnpm dev`
138
138
  - run bundle build (from the implementation project): `pnpm build`
139
139
 
@@ -354,19 +354,19 @@ const onGoToIndex = phx.frontend.createOnGoToIndexHandler(() => {'{'}
354
354
 
355
355
  ```typescript
356
356
  // Custom Phoenix layout element
357
- const MyLayoutElement = phx.frontend.createCustomPhoenixLayoutElement(() => {'{'}
357
+ const MyLayoutElement = phx.frontend.createCustomPhoenixLayoutElement('MyLayoutElement', () => {
358
358
  return <div>Custom Layout</div>
359
- {'}'})
359
+ })
360
360
 
361
361
  // Custom Phoenix entity element
362
- const MyEntityElement = phx.frontend.createCustomPhoenixEntityElement(() => {'{'}
362
+ const MyEntityElement = phx.frontend.createCustomPhoenixEntityElement('MyEntityElement', () => {
363
363
  return <mesh>...</mesh>
364
- {'}'})
364
+ })
365
365
 
366
366
  // Custom page when auth fails
367
- const NotAuthenticatedPage = phx.frontend.createNotAuthenticatedPage(() => {'{'}
367
+ const NotAuthenticatedPage = phx.frontend.createNotAuthenticatedPage(() => {
368
368
  return <div>Please log in</div>
369
- {'}'})
369
+ })
370
370
  ```
371
371
 
372
372
  ### Preset UI
@@ -383,6 +383,33 @@ const PresetAction = phx.frontend.createPresetPageAction(PresetHelpAction)
383
383
  const PresetOverlay = phx.frontend.createPresetDeviceNotSupported(SmallScreenNotice)
384
384
  ```
385
385
 
386
+ ### Loaded Configuration UI
387
+
388
+ A layout element registered under the name `ConfiguratorLayout` replaces the whole UI of a loaded configuration, in owner and in read mode, whatever `layout` the spec sets. Without it, Phoenix renders the layout the spec selects.
389
+
390
+ ```typescript
391
+ // src/elements/ConfiguratorLayout.tsx
392
+ import { LayoutSlots } from '@salsita-ai/sdk/configurator'
393
+
394
+ export default phx.frontend.createCustomPhoenixLayoutElement('ConfiguratorLayout', () => {
395
+ return (
396
+ <>
397
+ <LayoutSlots.Services />
398
+ <LayoutSlots.SharedCanvas />
399
+ <aside style={{ position: 'absolute', insetBlock: 0, insetInlineStart: 0, inlineSize: 320 }}>
400
+ Project sidebar
401
+ </aside>
402
+ </>
403
+ )
404
+ })
405
+ ```
406
+
407
+ - `LayoutSlots.SharedCanvas` renders the scene and fills the page. The layout positions its own UI above it and after it in the markup, as the `aside` does; unpositioned content is painted under the canvas.
408
+ - The layout has to mount `LayoutSlots.SharedCanvas`, or the project sets `appLoader: legacy`. Otherwise the app loader stays on screen.
409
+ - The layout composes the rest from what `@salsita-ai/sdk/configurator` exports: `LayoutSlots.Services`, the other members of `LayoutSlots`, and `useSceneSafeArea`.
410
+ - Nothing of the built-in layouts is rendered for it. The finalize page is `Legacy.CommonFinalizePage`, the dialog is `LayoutDialog`, and the preview start screen is `DesktopPreview` or `MobilePreview`, which the layout shows while `usePreview().isPreview` is set. The assistant panel is not available to a project layout.
411
+ - Any project layout element named `ConfiguratorLayout` takes over the page.
412
+
386
413
  ## TransformerBuilder
387
414
 
388
415
  Use `transformerBuilder` to create entity transformers:
@@ -35,15 +35,23 @@ interface ExpressionContext<
35
35
 
36
36
  // System services
37
37
  readonly domain: Domain // Access to domain model and other entities
38
- readonly componentReader: ComponentReader // Read component states across all entities
39
38
  readonly migrate: MigrateFunction // Data migration utilities
40
- readonly sketchingProjections: SketchingProjections // Sketching system projections
41
- readonly collisionProjections: CollisionProjections // Collision detection projections
39
+ readonly componentReader: ComponentReader // Read component states across all entities
40
+
41
+ // Projections
42
+ readonly positionProjections: PositionProjections // World matrices of scene entities
43
+ readonly sketchingProjections: SketchingProjections // Shapes and connections of a sketch
44
+ readonly collisionProjections: CollisionProjections // Collision meshes and collisions between entities
45
+ readonly constraintProjections: ConstraintProjections // Spatial constraints an entity violates
46
+ readonly snappingProjections: SnappingProjections // Snap points, edges, planes and matches
47
+ readonly projectedEntitiesProjections: ProjectedEntitiesProjections // Data of projected entities
42
48
  }
43
49
  ```
44
50
 
45
51
  The context includes data sources (session, published, frontend, backend), system services (domain access, component reader, projections), and application state (mode, role). Each part serves a different purpose in expression calculations.
46
52
 
53
+ The SDK declares the context in two parts. `CoreExpressionContext` holds the mode, the role, the data sources, `domain` and `migrate`. `RuntimeExpressionContext` holds the component reader and the projections. An editor hover or a type error therefore shows `ctx` as `OpenExpressionContext<..., RuntimeExpressionContext<AnyDomainEntity>>`, which has the members listed above. A helper of your own that takes the context declares its parameter as `ExpressionContext` with the five types of your project.
54
+
47
55
  ### Configurator Modes
48
56
 
49
57
  The `mode` property indicates the current operational context of the configurator. Different modes enable different functionality and access levels. Expressions can check the mode to change their behavior.
@@ -86,7 +86,7 @@ For inspiration and real-world implementation patterns, examine deployed Salsita
86
86
 
87
87
  When you encounter challenges or need to implement specific features, use these resources in order:
88
88
 
89
- **AI-Powered Assistance**: Every Phoenix project ships `AGENTS.md` and Phoenix agent skills, so coding agents in your project already know the SDK conventions and CLI. Start by using AI coding assistants in the `configurator-phoenix` project. While they may not solve complex problems directly, they can point you toward relevant examples or search terms that will help you find the right solution.
89
+ **AI-Powered Assistance**: Every Phoenix project ships Phoenix agent skills, so coding agents in your project already know the SDK conventions and CLI. Start by using AI coding assistants in the `configurator-phoenix` project. While they may not solve complex problems directly, they can point you toward relevant examples or search terms that will help you find the right solution.
90
90
 
91
91
  **Storybook Documentation**: Search the Phoenix Storybook for functionality similar to what you need. When you find relevant examples, examine the corresponding code in the configurator-phoenix stories to understand the implementation details.
92
92
 
@@ -29,10 +29,11 @@ pnpm phoenix dev
29
29
 
30
30
  1. Refuses to run when a `spec/` folder already exists, so it never touches an existing project.
31
31
  2. Writes a `package.json` that depends on the CLI's SDK version when the folder has none. An existing `package.json` is kept, as long as it depends on the SDK.
32
- 3. Writes a starter spec (`spec/application.yaml`, `spec/configurator.yaml`) with one resizable, recolorable box, and switches `package.json` to the application-spec pipeline.
33
- 4. Writes `pnpm-workspace.yaml` with the pnpm settings the first install needs: the build scripts of the SDK's dependencies are allowed or denied, and `@salsita-ai/*` packages are exempt from pnpm's minimum release age. A project inside a pnpm monorepo gets no file; the workspace root holds these settings.
32
+ 3. Adds the `dev`, `build`, `deploy`, `check` and `fix` scripts to `package.json` (see [Project Scripts](#project-scripts)). A script the project already has under one of those names keeps its command.
33
+ 4. Writes a starter spec (`spec/application.yaml`, `spec/configurator.yaml`) with one resizable, recolorable box, and switches `package.json` to the application-spec pipeline.
34
+ 5. Writes `pnpm-workspace.yaml` with the pnpm settings the first install needs: the build scripts of the SDK's dependencies are allowed or denied, and `@salsita-ai/*` packages are exempt from pnpm's minimum release age. A project inside a pnpm monorepo gets no file; the workspace root holds these settings.
34
35
 
35
- It asks nothing, writes no `.env` and contacts no server. `phoenix sync` then generates every other project file, including `AGENTS.md` and the agent skills described in [Project Structure](?path=/docs/phoenix-project-structure--docs).
36
+ It asks nothing, writes no `.env` and contacts no server. `phoenix sync` then generates every other project file, including the agent skills described in [Project Structure](?path=/docs/phoenix-project-structure--docs).
36
37
 
37
38
  ### Which project `phoenix dev` develops in
38
39
 
@@ -114,8 +115,6 @@ For quick local development without a server, use:
114
115
 
115
116
  ```bash
116
117
  phoenix dev --persistence local-storage
117
- # or use the shortcut script:
118
- pnpm dev:local
119
118
  ```
120
119
 
121
120
  **Important:** The server must be running before `deployment:prepare-local` when using `server` persistence mode.
@@ -158,27 +157,19 @@ spec/application.yaml → Server prepares environment → CLI writes .env.genera
158
157
 
159
158
  ### Project Scripts
160
159
 
161
- When you run `phoenix sync` or `phoenix clean`, the CLI auto-generates standardized scripts in your `package.json`. These scripts are enforced — they cannot be overridden by custom scripts with the same name.
162
-
163
- | Script | Command | Purpose |
164
- | -------------------------- | ------------------------------------------------------- | --------------------------------------------- |
165
- | `dev` | `phoenix dev` | Dev server with hot reload |
166
- | `dev:local` | `phoenix dev --persistence local-storage` | Dev without server dependency |
167
- | `build` | `phoenix build` | Production build |
168
- | `clean` | `phoenix clean` | Clean all artifacts and rebuild |
169
- | `sync` | `phoenix sync` | Sync generated code and dependencies |
170
- | `check` | `phoenix check` | Run all checks (TypeScript, ESLint, Prettier) |
171
- | `ts:check` | `phoenix check` | Alias for `check` |
172
- | `fix` | `phoenix fix` | Auto-fix lint and formatting issues |
173
- | `lint:fix` | `eslint src --fix` | ESLint fix only |
174
- | `prettify:fix` | `prettier --write .` | Prettier fix only |
175
- | `verify:fix` | `phoenix build && phoenix fix && phoenix check --types` | Build, fix, then type-check |
176
- | `test` | `vitest --passWithNoTests run` | Run tests |
177
- | `test:watch` | `vitest` | Tests in watch mode |
178
- | `prepare:ci` | `phoenix prepare:ci` | CI environment setup |
179
- | `deployment:prepare-local` | `phoenix deployment:prepare-local` | Prepare local environment |
180
-
181
- Custom scripts you add to `package.json` are preserved during sync. Deprecated scripts (like `environment:create`, `deployment:register`) are automatically removed.
160
+ `phoenix init` writes five scripts into `package.json`, each a shortcut for the CLI command of the same name:
161
+
162
+ | Script | Command | Purpose |
163
+ | -------- | ---------------- | --------------------------------------------- |
164
+ | `dev` | `phoenix dev` | Dev server with hot reload |
165
+ | `build` | `phoenix build` | Production build |
166
+ | `deploy` | `phoenix deploy` | Build and publish the project |
167
+ | `check` | `phoenix check` | Run all checks (TypeScript, ESLint, Prettier) |
168
+ | `fix` | `phoenix fix` | Auto-fix lint and formatting issues |
169
+
170
+ The scripts are yours: change, rename or delete them, and add your own. `phoenix sync` and `phoenix clean` leave the `scripts` section of `package.json` as it is, so they add no script, restore none you removed and remove none. Every CLI command also runs without a script, as `pnpm phoenix <command>`.
171
+
172
+ A project may have other scripts than these five, such as `sync`, `test` or `verify:fix`; they are its own and stay as they are. All of this holds for a project on the application-spec pipeline, which `phoenix init` selects. In a project on the legacy pipeline (see [Current and legacy project tooling](#current-and-legacy-project-tooling)), `phoenix sync` and `phoenix clean` write their own set of scripts into `package.json` on every run, over scripts of the same name.
182
173
 
183
174
  ### Server-Generated Variables
184
175
 
@@ -178,7 +178,7 @@ type CustomMaterial = Phx['AdminModel']['CustomMaterial']
178
178
 
179
179
  | File | Auto-Generated? | Notes |
180
180
  | ------------------------------------------------------ | --------------- | -------------------------------------- |
181
- | `package.json` | Partially | Dependencies merged, scripts preserved |
181
+ | `package.json` | Partially | Dependencies merged, scripts untouched |
182
182
  | `tsconfig.json` | Yes | Managed by Phoenix |
183
183
  | `eslint.config.js` | Yes | Managed by Phoenix |
184
184
  | `prettier.config.js` | Yes | Managed by Phoenix |
@@ -190,9 +190,11 @@ type CustomMaterial = Phx['AdminModel']['CustomMaterial']
190
190
  | `.env.example` | No | Template for env vars |
191
191
  | `.env.generated` | Yes | Auto-generated IDs, git-ignored |
192
192
  | `README.md` | No | Project documentation |
193
- | `AGENTS.md` | Once | Coding-agent instructions, yours after |
193
+ | `AGENTS.md` | No | Your own coding-agent instructions |
194
194
  | `.claude/skills/phoenix*/`, `.agents/skills/phoenix*/` | Yes | Phoenix agent skills |
195
195
 
196
+ Sync does not write `README.md`, `AGENTS.md`, `.env.example`, `.env.ci` or the `README.md` files in `assets/public/` and `assets/private/`. Earlier SDK versions wrote them once; a project that has them keeps them, and they are yours to edit.
197
+
196
198
  ### DO NOT EDIT (Fully Auto-Generated)
197
199
 
198
200
  - **Everything in `.phoenix/`** - Completely regenerated on each build. The one exception is `.phoenix/variants/`, where a coding agent writes the option files for `phoenix variants`; a rebuild keeps it
@@ -204,7 +206,7 @@ type CustomMaterial = Phx['AdminModel']['CustomMaterial']
204
206
 
205
207
  ### Agent Guidance
206
208
 
207
- Coding agents (Claude Code, Codex, Cursor) read `AGENTS.md` and the Phoenix agent skills. `AGENTS.md` is written once with the build commands, the do-not-edit list and a pointer to the skills; after that it is yours. The `phoenix`, `phoenix-cli`, `phoenix-feedback` and `phoenix-review` skills carry the SDK conventions, CLI tooling and review checklist, and the `phoenix-create-configurator` skill walks the agent from your product documents and assets to a reviewed configurator in five stages. The `phoenix-variants` skill has the agent show you several options over the dev preview when a decision is a matter of taste, and build the one you pick. Phoenix keeps them in step with the installed SDK, including the reference files inside a skill folder. During the internal beta, a project created from a canary build of the SDK gets one more section in the `phoenix` skill: once per conversation the agent checks for a newer canary and, after telling you, updates the project's SDK and runs `phoenix sync`. Other skill folders are never touched.
209
+ Coding agents (Claude Code, Codex, Cursor) read the Phoenix agent skills. An `AGENTS.md` is yours to add for your own instructions; sync does not write one. The `phoenix`, `phoenix-cli`, `phoenix-feedback` and `phoenix-review` skills carry the SDK conventions, CLI tooling and review checklist, and the `phoenix-create-configurator` skill walks the agent from your product documents and assets to a reviewed configurator in five stages. The `phoenix-variants` skill has the agent show you several options over the dev preview when a decision is a matter of taste, and build the one you pick. Phoenix keeps them in step with the installed SDK, including the reference files inside a skill folder. During the internal beta, a project created from a canary build of the SDK gets one more section in the `phoenix` skill: once per conversation the agent checks for a newer canary and, after telling you, updates the project's SDK and runs `phoenix sync`. Other skill folders are never touched.
208
210
 
209
211
  Projects created by earlier SDK versions had this guidance in `CLAUDE.md`, `.cursor/rules/phoenix.mdc` and `commands/review.md`. Sync removes the SDK block from the first two, keeps your own text around it, deletes a file that held nothing else, and deletes the review commands; use the `phoenix-review` skill instead of `/review`.
210
212
 
@@ -104,10 +104,10 @@ pnpm install
104
104
  pnpm phoenix sync
105
105
  ```
106
106
 
107
- `phoenix sync` keeps `spec/` and `src/` as your authored inputs and generates the application shell, scripts, types, and `.phoenix/` implementation. Start the generated development script without a server:
107
+ `phoenix sync` keeps `spec/` and `src/` as your authored inputs and generates the application shell, types, and `.phoenix/` implementation. Start development without a server:
108
108
 
109
109
  ```sh
110
- pnpm dev:local
110
+ pnpm phoenix dev --persistence local-storage
111
111
  ```
112
112
 
113
113
  Open the URL printed by the command. The application shell loads with the default `<salsita-configurator>` element and an empty 3D scene.