@salsita-ai/sdk 0.50.0-develop.ea458ea381 → 0.50.0-develop.f42416a4e6

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 (100) hide show
  1. package/README.md +19 -11
  2. package/dist/application-spec/generate-application/code/templates/skills/phoenix-create-configurator-skill.js +3 -1
  3. package/dist/application-spec/generate-application/code/templates/skills/phoenix-feedback-skill.js +39 -16
  4. package/dist/application-spec/generate-application/code/templates/skills/phoenix-skill.js +1 -1
  5. package/dist/configurator/application-runtime/extensions/systems/layout-system/layout-system.d.ts +2 -0
  6. package/dist/configurator/application-runtime/extensions/systems/layout-system/layout-system.js +1 -1
  7. package/dist/configurator/assistant/use-assistant-chat.d.ts +15 -5
  8. package/dist/configurator/assistant/use-assistant-chat.js +1 -1
  9. package/dist/configurator/ui/assistant/AssistantChips.d.ts +11 -0
  10. package/dist/configurator/ui/assistant/AssistantChips.js +1 -0
  11. package/dist/configurator/ui/assistant/AssistantComposer.d.ts +11 -0
  12. package/dist/configurator/ui/assistant/AssistantComposer.js +1 -0
  13. package/dist/configurator/ui/assistant/AssistantMarkdown.d.ts +7 -0
  14. package/dist/configurator/ui/assistant/AssistantMarkdown.js +1 -0
  15. package/dist/configurator/ui/assistant/AssistantPanel.d.ts +11 -0
  16. package/dist/configurator/ui/assistant/AssistantPanel.js +1 -0
  17. package/dist/configurator/ui/assistant/AssistantPanelProvider.d.ts +42 -0
  18. package/dist/configurator/ui/assistant/AssistantPanelProvider.js +1 -0
  19. package/dist/configurator/ui/assistant/AssistantThread.d.ts +11 -0
  20. package/dist/configurator/ui/assistant/AssistantThread.js +1 -0
  21. package/dist/configurator/ui/assistant/assistant-failure.d.ts +18 -0
  22. package/dist/configurator/ui/assistant/assistant-failure.js +1 -0
  23. package/dist/configurator/ui/assistant/index.d.ts +2 -0
  24. package/dist/configurator/ui/assistant/index.js +1 -0
  25. package/dist/configurator/ui/assistant/streamed-markdown.d.ts +6 -0
  26. package/dist/configurator/ui/assistant/streamed-markdown.js +2 -0
  27. package/dist/configurator/ui/assistant/theme.d.ts +2 -0
  28. package/dist/configurator/ui/assistant/theme.js +1 -0
  29. package/dist/configurator/ui/assistant/thread-items.d.ts +37 -0
  30. package/dist/configurator/ui/assistant/thread-items.js +1 -0
  31. package/dist/configurator/ui/assistant/tool-chip.d.ts +37 -0
  32. package/dist/configurator/ui/assistant/tool-chip.js +1 -0
  33. package/dist/configurator/ui/assistant/use-follow-stream.d.ts +17 -0
  34. package/dist/configurator/ui/assistant/use-follow-stream.js +1 -0
  35. package/dist/configurator/ui/configurator-layout/universal/DesktopLayout.js +5 -5
  36. package/dist/configurator/ui/configurator-layout/universal/Layout.js +1 -1
  37. package/dist/configurator/ui/configurator-layout/universal/MobileLayout.d.ts +2 -0
  38. package/dist/configurator/ui/configurator-layout/universal/MobileLayout.js +3 -3
  39. package/dist/configurator/ui/phoenix-layout-elements/AssistantToolbarButton.d.ts +10 -0
  40. package/dist/configurator/ui/phoenix-layout-elements/AssistantToolbarButton.js +1 -0
  41. package/dist/configurator/ui/phoenix-layout-elements/index.d.ts +1 -0
  42. package/dist/configurator/ui/phoenix-layout-elements/index.js +1 -1
  43. package/dist/docs/index.json +1 -1
  44. package/dist/docs/layouts/toolbar.mdx +4 -0
  45. package/dist/docs/phoenix/admin.mdx +2 -2
  46. package/dist/docs/phoenix/finalization.mdx +1 -1
  47. package/dist/docs/phoenix/integration/headless-mode-external.mdx +5 -5
  48. package/dist/docs/phoenix/integration/widget-mode-external.mdx +5 -5
  49. package/dist/docs/phoenix/phoenix-cli.mdx +56 -18
  50. package/dist/docs/phoenix/project-structure.mdx +1 -1
  51. package/dist/docs/phoenix/sdk-builder-workflow.mdx +3 -2
  52. package/dist/domain/system-components/phoenix-layout-elements/assistant-toolbar-button.d.ts +44 -0
  53. package/dist/domain/system-components/phoenix-layout-elements/assistant-toolbar-button.js +1 -0
  54. package/dist/domain/system-components/phoenix-layout-elements/index.d.ts +1 -0
  55. package/dist/domain/system-components/phoenix-layout-elements/index.js +1 -1
  56. package/dist/domain/system-components/phoenix-layout-elements/phoenix-layout-element.d.ts +2 -1
  57. package/dist/domain/system-components/phoenix-layout-elements/phoenix-layout-element.js +1 -1
  58. package/dist/i18n/locales/cs/messages.js +1 -1
  59. package/dist/i18n/locales/da/messages.js +1 -1
  60. package/dist/i18n/locales/de/messages.js +1 -1
  61. package/dist/i18n/locales/de-AT/messages.js +1 -1
  62. package/dist/i18n/locales/en/messages.js +1 -1
  63. package/dist/i18n/locales/en-AU/messages.js +1 -1
  64. package/dist/i18n/locales/en-GB/messages.js +1 -1
  65. package/dist/i18n/locales/es/messages.js +1 -1
  66. package/dist/i18n/locales/fr/messages.js +1 -1
  67. package/dist/i18n/locales/hu/messages.js +1 -1
  68. package/dist/i18n/locales/it/messages.js +1 -1
  69. package/dist/i18n/locales/nl/messages.js +1 -1
  70. package/dist/i18n/locales/pl/messages.js +1 -1
  71. package/dist/i18n/locales/pt/messages.js +1 -1
  72. package/dist/i18n/locales/ro/messages.js +1 -1
  73. package/dist/i18n/locales/sk/messages.js +1 -1
  74. package/dist/i18n/locales/tr/messages.js +1 -1
  75. package/dist/legacy/domain-spec/__generated/domain-schema.d.ts +6 -0
  76. package/dist/phoenix/cli.js +1 -1
  77. package/dist/phoenix/commands/build.d.ts +5 -0
  78. package/dist/phoenix/commands/build.js +1 -1
  79. package/dist/phoenix/commands/deploy.js +2 -2
  80. package/dist/phoenix/commands/feedback.d.ts +2 -1
  81. package/dist/phoenix/commands/feedback.js +1 -1
  82. package/dist/phoenix/services/index.d.ts +1 -0
  83. package/dist/phoenix/services/index.js +1 -1
  84. package/dist/phoenix/services/phx-banner-plugin.d.ts +32 -0
  85. package/dist/phoenix/services/phx-banner-plugin.js +1 -0
  86. package/dist/phoenix/services/phx-licenses-plugin.d.ts +55 -0
  87. package/dist/phoenix/services/phx-licenses-plugin.js +8 -0
  88. package/dist/phoenix/services/vite-config.d.ts +2 -0
  89. package/dist/phoenix/services/vite-config.js +1 -1
  90. package/dist/phoenix/utils/index.d.ts +1 -0
  91. package/dist/phoenix/utils/index.js +1 -1
  92. package/dist/phoenix/utils/read-git-commit.d.ts +14 -0
  93. package/dist/phoenix/utils/read-git-commit.js +1 -0
  94. package/dist/ui-sdk/theme/types.d.ts +4 -0
  95. package/package.json +4 -7
  96. package/schema/domain-spec.json +24 -0
  97. package/dist/extras/assistant/AssistantChat.d.ts +0 -6
  98. package/dist/extras/assistant/AssistantChat.js +0 -1
  99. package/dist/extras/assistant/index.d.ts +0 -1
  100. package/dist/extras/assistant/index.js +0 -1
package/README.md CHANGED
@@ -42,17 +42,25 @@ Generated code exposes the `phx` global in project files: `phx.domain` for the m
42
42
 
43
43
  Run the CLI through the project's package manager so its version matches the installed SDK.
44
44
 
45
- | Command | What it does |
46
- | ------------------------------ | ---------------------------------------------------------------------------- |
47
- | `phoenix init [folder]` | Starts a project: `package.json` when missing, and a starter spec. |
48
- | `phoenix sync` | Regenerates project files and aligns dependencies and scripts. |
49
- | `phoenix dev` | Development server with hot reload, webhook tunnel and the editor. |
50
- | `phoenix build` | Production build of the frontend and the webhooks. |
51
- | `phoenix check` / `fix` | Type check, lint and format; `fix` applies the automatic fixes. |
52
- | `phoenix login <workspace>` | Signs in through Studio; the token is kept in `~/.phoenix/credentials.json`. |
53
- | `phoenix deploy` | Uploads the project sources for a platform build. |
54
- | `phoenix docs` | Searches and reads the SDK documentation bundled with the package. |
55
- | `phoenix feedback "<message>"` | Sends feedback about the SDK, CLI, documentation or Studio to Salsita. |
45
+ | Command | What it does |
46
+ | ------------------------------ | ----------------------------------------------------------------------------------------------------------------- |
47
+ | `phoenix init [folder]` | Starts a project: `package.json` when missing, and a starter spec. |
48
+ | `phoenix sync` | Regenerates project files and aligns dependencies and scripts. |
49
+ | `phoenix dev` | Development server with hot reload, webhook tunnel and the editor. |
50
+ | `phoenix build` | Production build of the frontend and the webhooks. |
51
+ | `phoenix check` / `fix` | Type check, lint and format; `fix` applies the automatic fixes. |
52
+ | `phoenix login <workspace>` | Signs in through Studio; the token is kept in `~/.phoenix/credentials.json`. |
53
+ | `phoenix deploy` | Uploads the project sources for a platform build. |
54
+ | `phoenix docs` | Searches and reads the SDK documentation bundled with the package. |
55
+ | `phoenix feedback "<message>"` | Sends feedback about the SDK, CLI, documentation or Studio to Salsita; `--file <path>` sends a text file instead. |
56
+
57
+ Every frontend `.js` file `phoenix build` emits starts with a comment naming the application version from `spec/application.yaml`, the git commit when the build runs in a checkout, and the SDK version. `--banner <text>` replaces the application line with your own text. The line is public in every bundle:
58
+
59
+ ```sh
60
+ pnpm phoenix build --banner "Release: ${RELEASE_NAME}; Commit: ${COMMIT_SHA}"
61
+ ```
62
+
63
+ The same build writes `.phoenix/dist/frontend/THIRD-PARTY-LICENSES.txt`, the third-party packages bundled into the frontend with their licence texts. Deploy it together with the bundles.
56
64
 
57
65
  `phoenix dev` works against your own development environment once you are signed in. Alternatively paste the development-environment block of a Studio project (`SERVER_URL`, `PROJECT_SLUG`, `ENVIRONMENT_SLUG`, `DEVELOPMENT_KEY`) into `.env`. Never commit credentials.
58
66
 
@@ -71,7 +71,9 @@ Walk through the whole journey as the intended user: start, explore and change o
71
71
 
72
72
  Check representative valid and invalid combinations, boundaries, dependent changes, independent options on repeated parts, and hand-calculated prices where applicable. Include persistence/reload, undo/redo, exports, and integrations when implemented. Invalid configurations should produce the agreed handling, not an assumed universal blocking pattern.
73
73
 
74
- Fix gaps and regressions, update the specification, and present the complete experience for the client's final review. State what is implemented, what remains provisional or unverified, and whether it is a working draft or ready for its intended users. Publishing and connecting live systems remain separate actions governed by the client's authorization and the environment's rules; obtain any required approval against the concrete result.`,c=`## Verification evidence
74
+ Fix gaps and regressions, update the specification, and present the complete experience for the client's final review. State what is implemented, what remains provisional or unverified, and whether it is a working draft or ready for its intended users. Publishing and connecting live systems remain separate actions governed by the client's authorization and the environment's rules; obtain any required approval against the concrete result.
75
+
76
+ Once the client says they are done, or the configurator is live and they confirm the work is finished, follow the \`phoenix-feedback\` skill to interview them for an evaluation of the run. Ask for it once, and send it only with their approval.`,c=`## Verification evidence
75
77
 
76
78
  Use the project's documented verification commands and available tools throughout. Distinguish automated checks, your actual preview observations, and client feedback. A successful build does not prove preview access or interaction quality. Missing browser automation need not prevent the client from using a preview: offer a focused check and record their result as client feedback. Disclose anything you cannot test; a generic “OK” is not evidence that an untested interaction works.`,d=`# Phoenix UX guidance
77
79
 
@@ -4,7 +4,9 @@ Draft feedback for the Phoenix team when one of these happens while you work in
4
4
  - **Misleading guidance** — a doc page, a Phoenix skill or an error message sent you the wrong way, or did not say what you needed.
5
5
  - **Missing feature** — something the SDK does not support forced a workaround, or a routine task took far more steps than it should.
6
6
 
7
- Do not draft feedback for problems in this project's own specs or code, for a suspicion you have not checked, or twice for the same problem. Write one draft per distinct problem.`,o=`## Ask Before You Send
7
+ Do not draft feedback for problems in this project's own specs or code, for a suspicion you have not checked, or twice for the same problem. Write one draft per distinct problem.
8
+
9
+ The evaluation of a finished run is feedback too; the section "Evaluate the Run" says when to ask for it.`,o=`## Ask Before You Send
8
10
  Never send feedback on your own. Show the user a draft and send it only after they approve it.
9
11
 
10
12
  When to ask is your judgement:
@@ -12,20 +14,41 @@ When to ask is your judgement:
12
14
  - **At the end of the task** — everything else. Collect the details while you work and show all drafts together in your final message.
13
15
  - **When the user asks** you to send feedback — draft it and show it before sending.
14
16
 
15
- The draft is exactly what would be sent: the type, the message, the project if you name one with \`--projectSlug\`, and the contact if the user gave one. Send it unchanged after a yes; if the user edits it, send their version. If they decline, drop it and do not raise that problem again.`,a=`## Ask for a Rating After a Deploy
16
- When \`phoenix deploy\` reports that the configurator is live, tell the user the live URL first, then ask them to rate building with Phoenix so far:
17
- - Ask for a score from 0 to 10, where 10 is the best, and one optional sentence on what would have made it better. Say in the same question that the answer is sent to the Phoenix team as feedback.
18
- - Ask once per conversation, after the first deploy that goes live. Do not ask again after later deploys, and do not ask after a deploy that failed.
19
- - Never choose or guess the score yourself. If the user skips the question or declines, send nothing and do not ask again.
20
-
21
- A rating is feedback like any other, so the rules of this skill apply. Write the comment from the user's words, and take out anything the last section forbids: replace this project's product, customer and people names with generic words. Then show the user the three lines below exactly as they would be sent and send them only after a yes; if they edit the text, send their version. Use \`none\` when they gave no comment:
22
- \`\`\`bash
23
- pnpm phoenix feedback 'Rating: <score>/10
24
- Comment: <their comment, or none>
25
- Stage: after a deploy' --type other --agent '<your agent name>'
17
+ The draft is exactly what would be sent: the type, the full message, the project if you name one with \`--projectSlug\`, and the contact if the user gave one. Send it unchanged after a yes; if the user edits it, send their version. If they decline, drop it and do not raise that problem again.`,n=`## Evaluate the Run
18
+ When the work on the configurator is finished, interview the user for an evaluation of the run and send it as feedback. The run is finished when the user says they are done, or when \`phoenix deploy\` reported the configurator live and the user confirms they have nothing more to build; ask that once after a deploy goes live, and do not start the interview while they want to continue.
19
+
20
+ Say first that the answers are sent to the Phoenix team as feedback, and that the user can decline. Then ask one question at a time, in this order, and wait for each answer before asking the next. Give the short explanation with each question. Every score is a whole number from 0 to 10, where 0 is catastrophic, 5 is passable and 10 is amazing:
21
+ 1. **Minutes spent** — roughly how many minutes the user spent on the run, from the first prompt to now. This one is a number of minutes, not a score.
22
+ 2. **Agent guidance** — how clear it was what to expect and what to do, how clearly you explained yourself, and how well they understood the consequences of their choices.
23
+ 3. **Functionality** — how complete the configurator is: whether it configures the full product with all its options, keeps the user from creating or submitting a wrong configuration, and offers a way to finalize one.
24
+ 4. **UX** — how easy the configurator is to understand and operate, how clear the choices are, and how understandable incompatibilities and the ways to resolve them are.
25
+ 5. **3D quality** — how realistic and attractive the configured object and its scene are: shapes, textures, surroundings, lighting and shadows.
26
+ 6. **UI visuals** — how good the user interface looks.
27
+ 7. **Overall** — the user's subjective verdict on the configurator as a whole.
28
+ 8. **Comments** — anything else about the experience, in their own words. This one is free text and may be empty.
29
+
30
+ Rules of the interview:
31
+ - Ask once per conversation. Do not run it again after a later deploy, and do not run it after a deploy that failed.
32
+ - Never choose, guess or adjust a score, and never answer a question for the user. When they skip one, write \`skipped\` for it.
33
+ - When the user declines the evaluation, send nothing and do not ask again.
34
+
35
+ Compose the evaluation from the answers with exactly these labels, one per line, in this order. Write \`none\` when there are no comments, and fill \`Build\` and \`Configurator\` as the report template describes:
36
+ \`\`\`text
37
+ Evaluation
38
+ Minutes spent: <minutes>
39
+ Agent guidance: <score>/10
40
+ Functionality: <score>/10
41
+ UX: <score>/10
42
+ 3D quality: <score>/10
43
+ UI visuals: <score>/10
44
+ Overall: <score>/10
45
+ Comments: <their comments, or none>
46
+ Build: <build id> <Studio build page link>
47
+ Configurator: <live configurator link>
26
48
  \`\`\`
27
- Keep the three labels and their order, so the ratings can be read out of the feedback list. A rating is not a bug report: when the comment names a concrete problem, offer to draft it as its own feedback as the sections above describe.`,n=`## What Never Goes Into Feedback
49
+ Keep the labels and their order, so the scores can be read out of the feedback list and its export. Show the user the text exactly as it would be sent, and send it with \`--type other\` only after a yes; if they edit it, send their version. An evaluation is not a bug report: when a comment names a concrete problem, offer to draft it as its own report.`,a=`## What Never Goes Into Feedback
28
50
  The message leaves this project and is read by the Phoenix team. Never include:
29
- - secrets: API tokens, keys, passwords or \`.env\` values;
30
- - customer or end-user data, or personal data such as names and email addresses, other than a contact the user asked you to add;
31
- - this project's source code, specs or business rules. Describe the shape of the problem in words, or reduce it to a generic example that contains nothing of the project.`;export const generatePhoenixFeedbackSkill=()=>e({name:"phoenix-feedback",description:"Drafting feedback about the Phoenix SDK, CLI, documentation or Studio for the Phoenix team and sending it with the phoenix feedback command once the user approves the draft, covering when to raise it, how to write it, the rating asked for after a deploy and what must never be included. Use when the SDK or CLI misbehaves, a doc or skill misled you, a missing feature forced a workaround, the user asks to send feedback, or phoenix deploy has just gone live and the user should be asked to rate the experience from 0 to 10.",sections:[t,o,"## How to Send It\n```bash\npnpm phoenix feedback '<message>' --type bug --agent '<your agent name>'\n```\n- Put the message in single quotes, so the shell passes backticks, `$` and double quotes through unchanged. In bash or zsh, write a single quote inside the message as `'\\''`; in PowerShell, double it instead.\n- `--type` is `bug`, `idea` or `other` (default `other`). Use `idea` for a missing feature or an improvement.\n- `--agent` names you, the coding agent, so the team can tell agent feedback from a person's.\n- `--contact '<email>'` only when the user wants a reply and gave you the contact to use.\n- The workspace, user, SDK version and the spec's `project` are attached automatically. `--projectSlug <slug>` names another project of the workspace.\n- A message holds at most 5000 characters.\n\nWrite the message for someone who cannot see this project: what you ran or called, what you expected, what happened instead with the error text, and the workaround if you found one. Before quoting an error, replace this project's names, file paths and values in it with generic placeholders, as the list below requires.\n\nThe command needs `phoenix login`. If it answers that you are not signed in, tell the user once and continue; do not sign in yourself, because the sign-in is approved in a browser.\n\nAfter sending, confirm it to the user in one line.",a,n]});
51
+ - secrets and credentials: API tokens, keys, passwords or \`.env\` values, also when they appear inside an error or a log you quote. Replace each with \`<redacted>\`;
52
+ - personal data, or data of the project's customers and end users, such as names, email addresses and orders, other than a contact the user asked you to add.
53
+
54
+ Parts of this project may go into a message: file names and paths, snippets of its specs and code, and the user's own words. They are sent only as part of a draft the user saw and approved, which is why the draft you show is always the full text.`;export const generatePhoenixFeedbackSkill=()=>e({name:"phoenix-feedback",description:"Evaluating a finished run and drafting feedback about the Phoenix SDK, CLI, documentation or Studio for the Phoenix team, sent with the phoenix feedback command once the user approves the draft. Use right after phoenix deploy reports the configurator live or when the user says they are done, to offer the evaluation of the run, and when the SDK or CLI misbehaves, a doc or skill misled you, a missing feature forced a workaround or the user asks to send feedback.",sections:[t,o,"## Report Template\nWrite a report as plain text with the labels below, in this order, so the Phoenix team reads every report the same way. Keep every label; write `none` or `not deployed yet` where you have nothing to put.\n```text\nBuild: <build id> <Studio build page link>\nConfigurator: <live configurator link>\n\nWhat I was doing:\n<the task and the step you were on>\n\nWhat happened and what I expected:\n<what you ran or called, the result you got, and the result you expected>\n\nExact error or agent output:\n<the error text or the output, copied unchanged>\n\nWorkaround:\n<what you did instead, or none>\n\nConversation log:\n<the conversation condensed to the turns that matter: what the user asked, what you tried, what failed>\n```\n- **Build** is the latest build of this project, the one whose sources show the problem. `pnpm phoenix deploy` prints its id as `Build <id> started` and its page as `Follow the build in Studio: <url>`. When that line is missing, compose the link as `https://<workspace>.<studio host>/builder/deployments/<build id>`: the workspace is the one named at `phoenix login`, and the host is `studio.eu.salsita.ai` for the `eu` region, `studio.salsita.ai` for `us` and `studio-staging.salsita.app` for staging. A Studio address passed to login with `--studioUrl` replaces the workspace and host. Never invent a build id: when the project was not deployed, write `not deployed yet`.\n- **Configurator** is the live address `pnpm phoenix deploy` prints once the build is live.\n- The team downloads the sources of the build from its Studio page, so do not paste whole files; quote only the lines that show the problem.\n- Quote errors and output exactly. A report that paraphrases an error cannot be searched for.\n- Condense the conversation log, do not dump it: keep the user's requests, your decisions and the failures, and leave out tool output that the other sections already carry.",n,"## How to Send It\nWrite a report or an evaluation to a text file and send the file, so the shell changes nothing in it:\n```bash\npnpm phoenix feedback --file <path> --type bug --agent '<your agent name>'\n```\nPut the file outside the project, in the system's temporary folder, and delete it after sending. A note of a line or two can go in as an argument instead:\n```bash\npnpm phoenix feedback '<message>' --type idea --agent '<your agent name>'\n```\n- Give either `--file` or the message argument, never both.\n- Put a message argument in single quotes, so the shell passes backticks, `$` and double quotes through unchanged. In bash or zsh, write a single quote inside the message as `'\\''`; in PowerShell, double it instead.\n- `--type` is `bug`, `idea` or `other` (default `other`). Use `idea` for a missing feature or an improvement, and `other` for an evaluation.\n- `--agent` names you, the coding agent, so the team can tell agent feedback from a person's.\n- `--contact '<email>'` only when the user wants a reply and gave you the contact to use.\n- The workspace, user, SDK version and the spec's `project` are attached automatically. `--projectSlug <slug>` names another project of the workspace.\n- A message holds at most 100,000 characters. Shorten the conversation log first when a report is longer.\n\nThe command needs `phoenix login`. If it answers that you are not signed in, tell the user once and continue; do not sign in yourself, because the sign-in is approved in a browser.\n\nAfter sending, confirm it to the user in one line.",a]});
@@ -1 +1 @@
1
- import{renderAgentSkill as e}from"./agent-skill.js";import{sdkGuidanceSections as t}from"./sdk-content.js";export const followsCanaryBuilds=e=>!e.useWorkspace&&e.sdkVersion.includes("-develop.");export const generatePhoenixSkill=({checkLatestCanary:n})=>e({name:"phoenix",description:`Phoenix SDK project conventions, covering project structure, architecture principles, key patterns, file and export rules, webhook membership, versioning the domain model once it is deployed, showing the user the dev preview and the build of a deploy, and how to search the SDK documentation${n?", and the internal-beta check for the latest canary SDK build":""}. Use before writing or changing specs or source code in this Phoenix configurator project.`,sections:[...n?["## Stay on the Latest Canary\nDuring the internal beta this project uses the `canary` build of the SDK, which is published from every change to the platform. Once per conversation, before you change anything, compare the installed version with the newest canary:\n```bash\npnpm list @salsita-ai/sdk --depth 0\nnpm view @salsita-ai/sdk@canary version\n```\nWhen they differ, tell the user which version you are moving to, then update the project and regenerate its files:\n```bash\npnpm add @salsita-ai/sdk@canary\npnpm phoenix sync\n```\n`sync` rewrites the Phoenix skills, so read the ones you use again after it. When the user runs a globally installed `phoenix`, update that too with `npm install -g @salsita-ai/sdk@canary`. If the project no longer builds after the update, stop and tell the user; the `phoenix-feedback` skill covers reporting it."]:[],t.projectStructure,t.architecturePrinciples,t.keyPatterns,t.fileConventions,t.exportRules,t.webhookMembership,t.doNotEdit,t.modelVersioning,"## Show the Preview While You Work\nThe user should see the configurator change as you build it. Before your first change in a conversation, make sure `pnpm phoenix dev` is running (start it in the background when it is not) and open the address it prints as `Preview: <url>` (also written to `.phoenix/phoenix-dev.log`) in the user's browser: `open '<url>'` on macOS, `xdg-open '<url>'` on Linux, `Start-Process '<url>'` in PowerShell, `start \"\" \"<url>\"` in cmd. Keep the quotes, the shell would otherwise read a `?` in the address as a pattern. The log keeps the lines of earlier runs, so when you have just started dev, wait for the `Preview:` line written after your start and use the last one. Open it once; the preview reloads on every change, so tell the user to keep that tab open instead of opening it again.","## Show the Build After a Deploy\n`pnpm phoenix deploy` starts a build on the server and waits for it to go live. Right after `Build <id> started` it prints `Follow the build in Studio: <url>`. Run the deploy so that you can read its output while it runs, and open that link in the user's browser, quoted as above, as soon as it appears, so they can follow the build they just triggered. When the line is missing, the sign-in is older than the link: tell the user the build is listed under Deployments in Studio, and that `pnpm phoenix login <workspace>` brings the link back.","## Code Review\nUse the `phoenix-review` skill to review code changes before committing.",t.sdkDocumentation]});
1
+ import{renderAgentSkill as e}from"./agent-skill.js";import{sdkGuidanceSections as t}from"./sdk-content.js";export const followsCanaryBuilds=e=>!e.useWorkspace&&e.sdkVersion.includes("-develop.");export const generatePhoenixSkill=({checkLatestCanary:n})=>e({name:"phoenix",description:`Phoenix SDK project conventions, covering project structure, architecture principles, key patterns, file and export rules, webhook membership, versioning the domain model once it is deployed, showing the user the dev preview and the build of a deploy, the run evaluation that follows a live deploy, and how to search the SDK documentation${n?", and the internal-beta check for the latest canary SDK build":""}. Use before writing or changing specs or source code in this Phoenix configurator project.`,sections:[...n?["## Stay on the Latest Canary\nDuring the internal beta this project uses the `canary` build of the SDK, which is published from every change to the platform. Once per conversation, before you change anything, compare the installed version with the newest canary:\n```bash\npnpm list @salsita-ai/sdk --depth 0\nnpm view @salsita-ai/sdk@canary version\n```\nWhen they differ, tell the user which version you are moving to, then update the project and regenerate its files:\n```bash\npnpm add @salsita-ai/sdk@canary\npnpm phoenix sync\n```\n`sync` rewrites the Phoenix skills, so read the ones you use again after it. When the user runs a globally installed `phoenix`, update that too with `npm install -g @salsita-ai/sdk@canary`. If the project no longer builds after the update, stop and tell the user; the `phoenix-feedback` skill covers reporting it."]:[],t.projectStructure,t.architecturePrinciples,t.keyPatterns,t.fileConventions,t.exportRules,t.webhookMembership,t.doNotEdit,t.modelVersioning,"## Show the Preview While You Work\nThe user should see the configurator change as you build it. Before your first change in a conversation, make sure `pnpm phoenix dev` is running (start it in the background when it is not) and open the address it prints as `Preview: <url>` (also written to `.phoenix/phoenix-dev.log`) in the user's browser: `open '<url>'` on macOS, `xdg-open '<url>'` on Linux, `Start-Process '<url>'` in PowerShell, `start \"\" \"<url>\"` in cmd. Keep the quotes, the shell would otherwise read a `?` in the address as a pattern. The log keeps the lines of earlier runs, so when you have just started dev, wait for the `Preview:` line written after your start and use the last one. Open it once; the preview reloads on every change, so tell the user to keep that tab open instead of opening it again.",'## Show the Build After a Deploy\n`pnpm phoenix deploy` starts a build on the server and waits for it to go live. Right after `Build <id> started` it prints `Follow the build in Studio: <url>`. Run the deploy so that you can read its output while it runs, and open that link in the user\'s browser, quoted as above, as soon as it appears, so they can follow the build they just triggered. When the line is missing, the sign-in is older than the link: tell the user the build is listed under Deployments in Studio, and that `pnpm phoenix login <workspace>` brings the link back.\n\nWhen the deploy reports the configurator live, load the `phoenix-feedback` skill before you write your final message and follow its "Evaluate the Run" section, so the user is asked about the run while they are still there. It is asked once per conversation, and only once the user has nothing more to build.',"## Code Review\nUse the `phoenix-review` skill to review code changes before committing.",t.sdkDocumentation]});
@@ -25,6 +25,8 @@ export declare class LayoutSystem<ENTITIES extends DomainEntity<string>> {
25
25
  * whatever was true when it was created.
26
26
  */
27
27
  private get _toolbarHelpExists();
28
+ /** Whether the application spec has an `assistant` section; read on demand for the same reason. */
29
+ private get _hasAssistant();
28
30
  initialize: () => void;
29
31
  getCleanUpPropertyPageStateUpdates: (entityId: DomainEntityId<string>, state: DomainState<ENTITIES>) => import("#package/domain").ComponentModification<import("#package/domain").SystemComponentType<"layoutStateComponent", import("./layout-state.js").LayoutStateComponentState, "propertyPageStates">>[];
30
32
  getLayoutConfig: (state: DomainState<ENTITIES>) => {
@@ -1 +1 @@
1
- import{P as t,match as e}from"ts-pattern";import{SystemState as o,applicationConfigComponent as n,customSummaryPageComponent as a,entityHeaderComponent as i,navigationStepsComponent as r,propertyPageComponent as s,sidebarTabsComponent as l,simpleSummaryComponent as d,toolbarComponent as c}from"#package/domain";import{layoutStateComponent as g}from"./layout-state.js";import{toolbarHelpElementName as u}from"./toolbar-help-element.js";export class LayoutSystem{_context;_toolbarCache=new WeakMap;state;constructor(t){this._context=t,this.state=new o({...this._context,selectors:this._context.domainContextSelectors,componentType:g,getInitialState:()=>({propertyPageStates:{}})})}get _toolbarHelpExists(){return void 0!==this._context.specSelectors.getElementSlots().layoutElements[u]}initialize=()=>{this.state.initialize()};getCleanUpPropertyPageStateUpdates=(t,e)=>this.state.getUpdates(e=>{if(!e.propertyPageStates[t])return null;let o={...e.propertyPageStates};return delete o[t],{...e,propertyPageStates:o}},e);getLayoutConfig=t=>{let e=this._context.domainContextSelectors.getComponentReader().readFirstRootComponent({componentTypes:[n]},t),o=e?.state.shareDisabled??!1,a=o||(e?.state.shareOwnerLinkDisabled??!1),i=o||(e?.state.shareReadRevisionLinkDisabled??!1);return{layout:e?.state.layout??"universal",ignoreSelectionInSidebar:e?.state.ignoreSelectionInSidebar??!1,miniSidebar:e?.state.miniSidebar??!1,splitSidebar:e?.state.splitSidebar??!1,showSelectionBackButton:e?.state.showSelectionBackButton??!0,shareDisabled:a&&i,availableLocales:e?.state.availableLocales??["en"],indicatorSwitch:e?.state.indicatorSwitch??!0}};getSidebarSummaryPage=t=>{let e=this._context.domainContextSelectors.getNavigationSystem().getActiveNavigationState(t),o=this._context.domainContextSelectors.getComponentReader().readFirstRootComponent({componentTypes:[a,d]},t),n=e===o?.state.summaryNavigationState,i=o?.state.summaryNavigationState;return{page:o,isSummaryActive:n,summaryNavigationState:i}};getEnterSidebarSummaryUpdates=t=>{let{summaryNavigationState:e}=this.getSidebarSummaryPage(t);return e?[this._context.domainContextSelectors.getNavigationSystem().getPushNavigationUpdate(e,t)]:[]};getLeaveSidebarSummaryUpdates=t=>{let e=this._context.domainContextSelectors.getNavigationSystem().getPopNavigationUpdate(t);if(e)return[e];let o=this._context.domainContextSelectors.getNavigationStates().at(0);return o?(this._context.domainServicesSelectors.getLogger().error("There is no navigation state to pop, falling back to the initial navigation state. Check whether the history stack is not broken by some custom navigation logic."),[this._context.domainContextSelectors.getNavigationSystem().getNavigationUpdate(o)]):(this._context.domainServicesSelectors.getLogger().error("There is no navigation state to pop and no initial navigation state to fallback to, this should not happen"),[])};getPushSidebarStackPageUpdates=(t,e,o,n)=>this.state.getUpdates(n=>{let a=n.propertyPageStates[t]??{customState:{},stackPages:[]};return{...n,propertyPageStates:{...n.propertyPageStates,[t]:{...a,stackPages:[...a.stackPages,{props:o,component:e}]}}}},n);getPopSidebarStackPageUpdates=(t,e)=>this.state.getUpdates(e=>{let o=e.propertyPageStates[t]??{customState:{},stackPages:[]};return{...e,propertyPageStates:{...e.propertyPageStates,[t]:{...o,stackPages:o.stackPages.slice(0,-1)}}}},e);getSidebarBackUpdates=t=>{let e=this._context.domainContextSelectors.getNavigationSystem().getHasStackToPop(t),o=this._context.domainContextSelectors.getNavigationSystem().getIsRootSelected(t),{renderedStackedPage:n,summaryPage:a,activePropertyPageId:i}=this.getSidebarContent(t);if(a)return this.getLeaveSidebarSummaryUpdates(t);if(n&&i)return this.getPopSidebarStackPageUpdates(i,t);if(e){let e=this._context.domainContextSelectors.getNavigationSystem().getPopNavigationUpdate(t);return e?[e]:[]}return o?[]:[this._context.domainContextSelectors.getNavigationSystem().getDeselectUpdate()]};getSidebarContent=t=>{let e=this._context.domainContextSelectors.getNavigationSystem().getActiveSelectionState(t),o=this.state.get(t),n=this.getLayoutConfig(t),a=this._context.domainContextSelectors.getComponentReader().readManyComponents({componentTypes:[l],entityIds:[e]},t).some(t=>t.state.tabs.length>0),r=n.miniSidebar&&!a;r&&a&&this._context.domainServicesSelectors.getLogger().warn("Mini sidebar is not active because the layout contains tabs. Only layouts without tabs can have mini sidebar.");let d=this._context.domainContextSelectors.getComponentReader().readManyComponents({componentTypes:[s]},t),{isSummaryActive:c,page:g}=this.getSidebarSummaryPage(t),u=this._context.domainContextSelectors.getComponentReader().readManyComponents({componentTypes:[i]},t),p=this._context.domainContextSelectors.getCompositionProjections().getTree(t).map.get(e);if(!p)throw Error(`Selected entity ${e} not found in composition tree`);let m=this._context.domainContextSelectors.getCompositionProjections().getTree(t).map.get(t.rootEntityId);if(!m)throw Error(`Root entity ${t.rootEntityId} not found in composition tree`);let S=(n.ignoreSelectionInSidebar?[m]:[p,...p.ancestors()]).map(t=>{let e=n.ignoreSelectionInSidebar?d:d.filter(e=>e.componentInstance.entityId===t.id),o=u.filter(e=>e.componentInstance.entityId===t.id);return{id:t.id,propertyPages:e,header:o[0]??null}}),h=S[0]?.id,b=(h?o.propertyPageStates[h]?.stackPages??[]:[]).at(-1)??null,k=this._context.domainContextSelectors.getNavigationSystem().getHasStackToPop(t),x=this._context.domainContextSelectors.getNavigationSystem().getIsRootSelected(t),y=c&&k||k||!!b||!x&&n.showSelectionBackButton&&!n.ignoreSelectionInSidebar;return c?{summaryPage:g,propertyPages:[],isBackAvailable:y,hasMiniSidebar:r,hasSplitSidebar:n.splitSidebar}:{summaryPage:null,propertyPages:S,renderedStackedPage:b,activePropertyPageId:h,isBackAvailable:y,hasMiniSidebar:r,hasSplitSidebar:n.splitSidebar}};getToolbarState=o=>{let n=this._toolbarHelpExists,a=this._toolbarCache.get(o);if(a&&a.toolbarHelpExists===n)return a.result;let i=this._context.domainContextSelectors.getExpressionContext(),r=this._context.domainContextSelectors.getNavigationSystem().getActiveFullscreenState(o),s="read"===i.mode,l=this._context.domainContextSelectors.getComponentReader().readFirstRootComponent({componentTypes:[c]},o),d=this._context.domainContextSelectors.getDimensionSystem().getDimensionConfig(o),g=this.getLayoutConfig(o),u="read"===this._context.domainContextSelectors.getExpressionContext().mode,p=g.availableLocales.length>1,m=!u&&!g.shareDisabled,S=g.indicatorSwitch,h=d.availableDimensions.length>1||S,b=e(l).returnType().with(t.nonNullable,t=>t.state).with(t.nullish,()=>e([r,s]).returnType().with([!1,!1],()=>({desktop:[{kind:"toolbarButtonGroup",children:[{kind:"enterFullscreenToolbarButton"}]},{kind:"toolbarButtonGroup",children:[{kind:"resetCameraToolbarButton"}]},...p?[{kind:"toolbarButtonGroup",children:[{kind:"languageToolbarButton"}]}]:[],...m||h?[{kind:"toolbarButtonGroup",children:[...m?[{kind:"shareToolbarButton"}]:[],...h?[{kind:"measurementsToolbarButton"}]:[],...n?[{kind:"helpToolbarButton"}]:[]]}]:[],{kind:"toolbarButtonGroup",children:[{kind:"undoToolbarButton"},{kind:"redoToolbarButton"}]}],mobile:[{kind:"toolbarButtonGroup",children:[{kind:"undoToolbarButton"},{kind:"redoToolbarButton"}]},{kind:"toolbarButtonGroup",children:[{kind:"resetCameraToolbarButton"}]},{kind:"toolbarButtonGroup",children:[{kind:"enterFullscreenToolbarButton"}]}],mobileMenu:[...m?[{kind:"shareMenuAction"}]:[],...h?[{kind:"measurementsMenuAction"}]:[],...p?[{kind:"languageMenuAction"}]:[],...n?[{kind:"helpMenuAction"}]:[]]})).with([!0,!1],()=>({desktop:[{kind:"toolbarButtonGroup",children:[{kind:"exitFullscreenToolbarButton"}]}],mobile:[{kind:"toolbarButtonGroup",children:[{kind:"exitFullscreenToolbarButton"}]}],mobileMenu:[]})).with([!1,!0],()=>({desktop:[{kind:"toolbarButtonGroup",children:[{kind:"enterFullscreenToolbarButton"}]},{kind:"toolbarButtonGroup",children:[{kind:"resetCameraToolbarButton"}]},...p?[{kind:"toolbarButtonGroup",children:[{kind:"languageToolbarButton"}]}]:[],...h?[{kind:"toolbarButtonGroup",children:[...h?[{kind:"measurementsToolbarButton"}]:[]]}]:[],...n?[{kind:"helpToolbarButton"}]:[]],mobile:[{kind:"toolbarButtonGroup",children:[{kind:"resetCameraToolbarButton"}]},{kind:"toolbarButtonGroup",children:[{kind:"enterFullscreenToolbarButton"}]}],mobileMenu:[...h?[{kind:"measurementsMenuAction"}]:[],...p?[{kind:"languageMenuAction"}]:[],...n?[{kind:"helpMenuAction"}]:[]]})).with([!0,!0],()=>({desktop:[{kind:"toolbarButtonGroup",children:[{kind:"exitFullscreenToolbarButton"}]}],mobile:[{kind:"toolbarButtonGroup",children:[{kind:"exitFullscreenToolbarButton"}]}],mobileMenu:[]})).exhaustive()).exhaustive();return this._toolbarCache.set(o,{toolbarHelpExists:n,result:b}),b};getNavigationSteps=t=>{let e=this._context.domainContextSelectors.getComponentReader().readFirstRootComponent({componentTypes:[r]},t);return e?.state.steps??[]}}
1
+ import{P as t,match as e}from"ts-pattern";import{SystemState as o,applicationConfigComponent as n,customSummaryPageComponent as a,entityHeaderComponent as i,navigationStepsComponent as r,propertyPageComponent as s,sidebarTabsComponent as l,simpleSummaryComponent as d,toolbarComponent as c}from"#package/domain";import{layoutStateComponent as g}from"./layout-state.js";import{toolbarHelpElementName as u}from"./toolbar-help-element.js";export class LayoutSystem{_context;_toolbarCache=new WeakMap;state;constructor(t){this._context=t,this.state=new o({...this._context,selectors:this._context.domainContextSelectors,componentType:g,getInitialState:()=>({propertyPageStates:{}})})}get _toolbarHelpExists(){return void 0!==this._context.specSelectors.getElementSlots().layoutElements[u]}get _hasAssistant(){return void 0!==this._context.specSelectors.getAssistant()}initialize=()=>{this.state.initialize()};getCleanUpPropertyPageStateUpdates=(t,e)=>this.state.getUpdates(e=>{if(!e.propertyPageStates[t])return null;let o={...e.propertyPageStates};return delete o[t],{...e,propertyPageStates:o}},e);getLayoutConfig=t=>{let e=this._context.domainContextSelectors.getComponentReader().readFirstRootComponent({componentTypes:[n]},t),o=e?.state.shareDisabled??!1,a=o||(e?.state.shareOwnerLinkDisabled??!1),i=o||(e?.state.shareReadRevisionLinkDisabled??!1);return{layout:e?.state.layout??"universal",ignoreSelectionInSidebar:e?.state.ignoreSelectionInSidebar??!1,miniSidebar:e?.state.miniSidebar??!1,splitSidebar:e?.state.splitSidebar??!1,showSelectionBackButton:e?.state.showSelectionBackButton??!0,shareDisabled:a&&i,availableLocales:e?.state.availableLocales??["en"],indicatorSwitch:e?.state.indicatorSwitch??!0}};getSidebarSummaryPage=t=>{let e=this._context.domainContextSelectors.getNavigationSystem().getActiveNavigationState(t),o=this._context.domainContextSelectors.getComponentReader().readFirstRootComponent({componentTypes:[a,d]},t),n=e===o?.state.summaryNavigationState,i=o?.state.summaryNavigationState;return{page:o,isSummaryActive:n,summaryNavigationState:i}};getEnterSidebarSummaryUpdates=t=>{let{summaryNavigationState:e}=this.getSidebarSummaryPage(t);return e?[this._context.domainContextSelectors.getNavigationSystem().getPushNavigationUpdate(e,t)]:[]};getLeaveSidebarSummaryUpdates=t=>{let e=this._context.domainContextSelectors.getNavigationSystem().getPopNavigationUpdate(t);if(e)return[e];let o=this._context.domainContextSelectors.getNavigationStates().at(0);return o?(this._context.domainServicesSelectors.getLogger().error("There is no navigation state to pop, falling back to the initial navigation state. Check whether the history stack is not broken by some custom navigation logic."),[this._context.domainContextSelectors.getNavigationSystem().getNavigationUpdate(o)]):(this._context.domainServicesSelectors.getLogger().error("There is no navigation state to pop and no initial navigation state to fallback to, this should not happen"),[])};getPushSidebarStackPageUpdates=(t,e,o,n)=>this.state.getUpdates(n=>{let a=n.propertyPageStates[t]??{customState:{},stackPages:[]};return{...n,propertyPageStates:{...n.propertyPageStates,[t]:{...a,stackPages:[...a.stackPages,{props:o,component:e}]}}}},n);getPopSidebarStackPageUpdates=(t,e)=>this.state.getUpdates(e=>{let o=e.propertyPageStates[t]??{customState:{},stackPages:[]};return{...e,propertyPageStates:{...e.propertyPageStates,[t]:{...o,stackPages:o.stackPages.slice(0,-1)}}}},e);getSidebarBackUpdates=t=>{let e=this._context.domainContextSelectors.getNavigationSystem().getHasStackToPop(t),o=this._context.domainContextSelectors.getNavigationSystem().getIsRootSelected(t),{renderedStackedPage:n,summaryPage:a,activePropertyPageId:i}=this.getSidebarContent(t);if(a)return this.getLeaveSidebarSummaryUpdates(t);if(n&&i)return this.getPopSidebarStackPageUpdates(i,t);if(e){let e=this._context.domainContextSelectors.getNavigationSystem().getPopNavigationUpdate(t);return e?[e]:[]}return o?[]:[this._context.domainContextSelectors.getNavigationSystem().getDeselectUpdate()]};getSidebarContent=t=>{let e=this._context.domainContextSelectors.getNavigationSystem().getActiveSelectionState(t),o=this.state.get(t),n=this.getLayoutConfig(t),a=this._context.domainContextSelectors.getComponentReader().readManyComponents({componentTypes:[l],entityIds:[e]},t).some(t=>t.state.tabs.length>0),r=n.miniSidebar&&!a;r&&a&&this._context.domainServicesSelectors.getLogger().warn("Mini sidebar is not active because the layout contains tabs. Only layouts without tabs can have mini sidebar.");let d=this._context.domainContextSelectors.getComponentReader().readManyComponents({componentTypes:[s]},t),{isSummaryActive:c,page:g}=this.getSidebarSummaryPage(t),u=this._context.domainContextSelectors.getComponentReader().readManyComponents({componentTypes:[i]},t),p=this._context.domainContextSelectors.getCompositionProjections().getTree(t).map.get(e);if(!p)throw Error(`Selected entity ${e} not found in composition tree`);let m=this._context.domainContextSelectors.getCompositionProjections().getTree(t).map.get(t.rootEntityId);if(!m)throw Error(`Root entity ${t.rootEntityId} not found in composition tree`);let h=(n.ignoreSelectionInSidebar?[m]:[p,...p.ancestors()]).map(t=>{let e=n.ignoreSelectionInSidebar?d:d.filter(e=>e.componentInstance.entityId===t.id),o=u.filter(e=>e.componentInstance.entityId===t.id);return{id:t.id,propertyPages:e,header:o[0]??null}}),S=h[0]?.id,b=(S?o.propertyPageStates[S]?.stackPages??[]:[]).at(-1)??null,k=this._context.domainContextSelectors.getNavigationSystem().getHasStackToPop(t),x=this._context.domainContextSelectors.getNavigationSystem().getIsRootSelected(t),y=c&&k||k||!!b||!x&&n.showSelectionBackButton&&!n.ignoreSelectionInSidebar;return c?{summaryPage:g,propertyPages:[],isBackAvailable:y,hasMiniSidebar:r,hasSplitSidebar:n.splitSidebar}:{summaryPage:null,propertyPages:h,renderedStackedPage:b,activePropertyPageId:S,isBackAvailable:y,hasMiniSidebar:r,hasSplitSidebar:n.splitSidebar}};getToolbarState=o=>{let n=this._toolbarHelpExists,a=this._hasAssistant,i=this._toolbarCache.get(o);if(i&&i.toolbarHelpExists===n&&i.hasAssistant===a)return i.result;let r=this._context.domainContextSelectors.getExpressionContext(),s=this._context.domainContextSelectors.getNavigationSystem().getActiveFullscreenState(o),l="read"===r.mode,d=this._context.domainContextSelectors.getComponentReader().readFirstRootComponent({componentTypes:[c]},o),g=this._context.domainContextSelectors.getDimensionSystem().getDimensionConfig(o),u=this.getLayoutConfig(o),p="read"===this._context.domainContextSelectors.getExpressionContext().mode,m=u.availableLocales.length>1,h=!p&&!u.shareDisabled,S=u.indicatorSwitch,b=g.availableDimensions.length>1||S,k=a?[{kind:"toolbarButtonGroup",children:[{kind:"assistantToolbarButton"}]}]:[],x=e(d).returnType().with(t.nonNullable,t=>t.state).with(t.nullish,()=>e([s,l]).returnType().with([!1,!1],()=>({desktop:[{kind:"toolbarButtonGroup",children:[{kind:"enterFullscreenToolbarButton"}]},{kind:"toolbarButtonGroup",children:[{kind:"resetCameraToolbarButton"}]},...m?[{kind:"toolbarButtonGroup",children:[{kind:"languageToolbarButton"}]}]:[],...h||b?[{kind:"toolbarButtonGroup",children:[...h?[{kind:"shareToolbarButton"}]:[],...b?[{kind:"measurementsToolbarButton"}]:[],...n?[{kind:"helpToolbarButton"}]:[]]}]:[],{kind:"toolbarButtonGroup",children:[{kind:"undoToolbarButton"},{kind:"redoToolbarButton"}]},...k],mobile:[{kind:"toolbarButtonGroup",children:[{kind:"undoToolbarButton"},{kind:"redoToolbarButton"}]},{kind:"toolbarButtonGroup",children:[{kind:"resetCameraToolbarButton"}]},{kind:"toolbarButtonGroup",children:[{kind:"enterFullscreenToolbarButton"}]},...k],mobileMenu:[...h?[{kind:"shareMenuAction"}]:[],...b?[{kind:"measurementsMenuAction"}]:[],...m?[{kind:"languageMenuAction"}]:[],...n?[{kind:"helpMenuAction"}]:[]]})).with([!0,!1],()=>({desktop:[{kind:"toolbarButtonGroup",children:[{kind:"exitFullscreenToolbarButton"}]}],mobile:[{kind:"toolbarButtonGroup",children:[{kind:"exitFullscreenToolbarButton"}]}],mobileMenu:[]})).with([!1,!0],()=>({desktop:[{kind:"toolbarButtonGroup",children:[{kind:"enterFullscreenToolbarButton"}]},{kind:"toolbarButtonGroup",children:[{kind:"resetCameraToolbarButton"}]},...m?[{kind:"toolbarButtonGroup",children:[{kind:"languageToolbarButton"}]}]:[],...b?[{kind:"toolbarButtonGroup",children:[...b?[{kind:"measurementsToolbarButton"}]:[]]}]:[],...n?[{kind:"helpToolbarButton"}]:[],...k],mobile:[{kind:"toolbarButtonGroup",children:[{kind:"resetCameraToolbarButton"}]},{kind:"toolbarButtonGroup",children:[{kind:"enterFullscreenToolbarButton"}]},...k],mobileMenu:[...b?[{kind:"measurementsMenuAction"}]:[],...m?[{kind:"languageMenuAction"}]:[],...n?[{kind:"helpMenuAction"}]:[]]})).with([!0,!0],()=>({desktop:[{kind:"toolbarButtonGroup",children:[{kind:"exitFullscreenToolbarButton"}]}],mobile:[{kind:"toolbarButtonGroup",children:[{kind:"exitFullscreenToolbarButton"}]}],mobileMenu:[]})).exhaustive()).exhaustive();return this._toolbarCache.set(o,{toolbarHelpExists:n,hasAssistant:a,result:x}),x};getNavigationSteps=t=>{let e=this._context.domainContextSelectors.getComponentReader().readFirstRootComponent({componentTypes:[r]},t);return e?.state.steps??[]}}
@@ -1,20 +1,30 @@
1
1
  import { type UIMessage } from '@tanstack/ai-react';
2
- /** Experimental assistant chat state and actions of the current configuration. */
2
+ /** Assistant chat state and actions of the current configuration. */
3
3
  export interface AssistantChatState {
4
4
  /** The thread as painted: hydrated from the server, then streamed. */
5
5
  readonly messages: ReadonlyArray<UIMessage>;
6
6
  /** True while a reply streams or a browser tool runs. */
7
7
  readonly isLoading: boolean;
8
8
  readonly error: Error | undefined;
9
+ /**
10
+ * HTTP status of the latest request the server answered with a failure, for telling a refusal from
11
+ * a dropped connection when `error` is set. Nothing when the latest answered request succeeded or
12
+ * the server was never reached.
13
+ */
14
+ readonly failedStatus: number | undefined;
15
+ /** False when the server said this session may read the thread but not add to it. */
16
+ readonly isContinuable: boolean;
17
+ /** False until the first request of the chat, the thread read, was answered or failed. */
18
+ readonly isLoaded: boolean;
9
19
  /** Sends a user message stamped with its send time. */
10
20
  readonly send: (content: string) => Promise<void>;
11
21
  /** Stops the current reply in the browser and on the server. */
12
22
  readonly stop: () => Promise<void>;
13
23
  }
14
24
  /**
15
- * Experimental: the conversational assistant of the current configuration. The server owns the
16
- * thread — the chat hydrates from it on mount and rejoins a reply still streaming — so the hook
17
- * keeps nothing in browser storage. Each message carries its send time for the model. The model's
18
- * `query` and `apply` calls run here, in the browser, against the configurator this hook is rendered in.
25
+ * The conversational assistant of the current configuration. The server owns the thread — the chat
26
+ * hydrates from it on mount and rejoins a reply still streaming — so the hook keeps nothing in
27
+ * browser storage. Each message carries its send time for the model. The model's `query` and
28
+ * `apply` calls run here, in the browser, against the configurator this hook is rendered in.
19
29
  */
20
30
  export declare const useAssistantChat: () => AssistantChatState;
@@ -1 +1 @@
1
- import{fetchServerSentEvents as t,useChat as e}from"@tanstack/ai-react";import{useCallback as a,useMemo as n}from"react";import{useApplicationRuntimeSelector as s,useAuthorizedClient as r,useConfigurationId as o}from"#package/react-sdk";import{useConfigurator as i}from"../application-runtime/index.js";export const useAssistantChat=()=>{let c=o(),p=r(),l=n(()=>p.getAssistantChatConnection(c),[p,c]),u=n(()=>{let e=async()=>({headers:await l.headers()}),a=t(l.url,e),n=t(`${l.url}/runs`,e);return{...a,joinRun:n.joinRun}},[l]),{assistantSystem:d}=i(),{messages:m,isLoading:h,error:f,sendMessage:g,stop:w,runId:y}=e({threadId:`assistant:${c}`,connection:u,persistence:!0,history:{pageSize:50},tools:s(d.getTools)});return{messages:m,isLoading:h,error:f,send:a(async t=>{let e={sentAt:new Date().toISOString()};await g({content:t,metadata:e})},[g]),stop:a(async()=>{w(),null!=y&&await fetch(`${l.url}/runs/${encodeURIComponent(y)}/cancel`,{method:"POST",headers:await l.headers()})},[l,y,w])}};
1
+ import{fetchServerSentEvents as t,useChat as e}from"@tanstack/ai-react";import{useCallback as a,useMemo as n,useState as r}from"react";import{useApplicationRuntimeSelector as s,useAuthorizedClient as o,useConfigurationId as i,useLogger as c}from"#package/react-sdk";import{useConfigurator as l}from"../application-runtime/index.js";export const useAssistantChat=()=>{let u=i(),h=o(),d=c(),[p,m]=r(),[f,w]=r(!0),[y,g]=r(!1),C=a(()=>h.getAssistantChatConnection(u),[h,u]),k=n(()=>{let e=async(t,e)=>{let a=await fetch(t,e).finally(()=>g(!0)),n=a.headers.get("x-assistant-thread-continuable");return m(a.ok?void 0:a.status),null!=n&&w("false"!==n),a},a=async()=>({headers:await C().headers(),fetchClient:e}),n=t(()=>C().url,a),r=t(()=>`${C().url}/runs`,a);return{...n,joinRun:r.joinRun}},[C]),{assistantSystem:S}=l(),{messages:$,isLoading:j,error:x,sendMessage:A,stop:I,runId:R}=e({threadId:`assistant:${u}`,connection:k,persistence:!0,history:{pageSize:50},tools:s(S.getTools)}),v=a(async t=>{let e={sentAt:new Date().toISOString()};await A({content:t,metadata:e})},[A]),O=a(async()=>{if(I(),null!=R)try{let t=C();await fetch(`${t.url}/runs/${encodeURIComponent(R)}/cancel`,{method:"POST",headers:await t.headers()})}catch(t){d.warn("Could not cancel the assistant run on the server",{error:t})}},[C,d,R,I]);return n(()=>({messages:$,isLoading:j,error:x,failedStatus:p,isContinuable:f,isLoaded:y,send:v,stop:O}),[$,j,x,p,f,y,v,O])};
@@ -0,0 +1,11 @@
1
+ import type { AssistantToolItem } from './thread-items.js';
2
+ export interface AssistantToolChipsProps {
3
+ readonly tools: readonly AssistantToolItem[];
4
+ /** True while the reply runs and this row is its newest block: the assistant is at work on it. */
5
+ readonly isActive: boolean;
6
+ }
7
+ /**
8
+ * A row of notes on what the assistant did in a reply, one per tool call. A click on a note logs
9
+ * the input and the result of its call to the console.
10
+ */
11
+ export declare const AssistantToolChips: import("react").MemoExoticComponent<({ tools, isActive }: AssistantToolChipsProps) => import("react/jsx-runtime").JSX.Element>;
@@ -0,0 +1 @@
1
+ import{jsx as t,jsxs as e}from"react/jsx-runtime";import{Tooltip as a,styled as i}from"@mui/material";import{memo as n,useCallback as s}from"react";import{match as o}from"ts-pattern";import{useLocalizable as r}from"#package/react-sdk";import{PhxIcon as l}from"#package/ui-sdk";import{AssistantPanelName as d}from"./theme.js";let c={id:"sdk.assistant.tool.updateDesign.label",message:"Update design"},m={id:"sdk.assistant.tool.readDesign.label",message:"Read design"},p={id:"sdk.assistant.tool.mistake.hint",message:"The assistant made a mistake."},h=i("div",{name:d,slot:"chips"})(({theme:{palette:t,spacing:e,typography:a}})=>({...a.caption,alignItems:"start",color:t.text.secondary,display:"grid",gap:e(.75),gridTemplateColumns:"min-content minmax(0, 1fr)"})),g=i("span",{name:d,slot:"activity"})(({theme:{typography:t}})=>({blockSize:`calc(${t.caption.lineHeight} * 1em)`,display:"grid",placeItems:"center"})),f=i(l,{name:d,slot:"activityIcon"})({fontSize:14,'[data-chips="active"] &':{animation:"phx-assistant-turn 2.4s linear infinite","@media (prefers-reduced-motion: reduce)":{animation:"none"}},"@keyframes phx-assistant-turn":{to:{rotate:"360deg"}}}),k=i("span",{name:d,slot:"chip"})(({theme:{palette:t,spacing:e}})=>({cursor:"default",display:"inline-block",whiteSpace:"nowrap","&:not(:last-child)::after":{color:t.text.secondary,content:'"\xb7"',display:"inline-block",marginInline:e(.75),textDecoration:"none"},'&[data-chip="failed"]':{color:t.error.main},'&[data-chip="flawed"]':{textDecoration:"line-through",textDecorationColor:t.error.main}})),u=n(({tool:e})=>{let{chip:i,input:n,result:l}=e,d=r(c),h=r(m),g=r(p),f=o(i.label).with({kind:"text"},({text:t})=>t).with({kind:"updateDesign"},()=>d).with({kind:"readDesign"},()=>h).exhaustive(),u=s(()=>{console.info(`Assistant tool call "${f}"`,{input:n,result:l})},[n,f,l]);return t(a,{title:"failed"===i.state||"flawed"===i.state?g:"",placement:"top-start",children:t(k,{onClick:u,"data-chip":i.state,children:f})})});export const AssistantToolChips=n(({tools:a,isActive:i})=>e(h,{"data-chips":i?"active":"done","aria-busy":i,children:[t(g,{children:t(f,{name:"Gear"})}),t("div",{children:a.map(e=>t(u,{tool:e},e.id))})]}));
@@ -0,0 +1,11 @@
1
+ import type { AssistantChatState } from '../../assistant/index.js';
2
+ export interface AssistantComposerProps {
3
+ readonly chat: AssistantChatState | undefined;
4
+ readonly onFocusChange: (isFocused: boolean) => void;
5
+ }
6
+ /**
7
+ * The message input of the panel. Enter sends and Shift+Enter adds a line. While a reply runs the
8
+ * send control is a Stop control and the input stays editable, so the next message can be written.
9
+ * A thread this session may not continue disables the input and says why.
10
+ */
11
+ export declare const AssistantComposer: ({ chat, onFocusChange }: AssistantComposerProps) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1 @@
1
+ import{jsx as e,jsxs as a}from"react/jsx-runtime";import{Trans as r}from"@lingui/react";import{IconButton as o,InputBase as i,Tooltip as t,styled as n}from"@mui/material";import{useCallback as s,useRef as d,useState as l}from"react";import{useLocalizable as m}from"#package/react-sdk";import{PhxIcon as c,useResponsive as p}from"#package/ui-sdk";import{AssistantPanelName as u}from"./theme.js";let b={id:"sdk.assistant.composer.placeholder",message:"Ask or tell me what to change…"},g={id:"sdk.assistant.composer.send.label",message:"Send"},h={id:"sdk.assistant.composer.stop.label",message:"Stop"},k=n("form",{name:u,slot:"composer"})(({theme:{palette:e,spacing:a}})=>({borderBlockStart:`1px solid ${e.divider}`,display:"grid",gap:a(.75),paddingBlock:a(1.25),paddingInline:`var(--phx-sidebar-padding, ${a(2)})`})),f=n("div",{name:u,slot:"inputFrame"})(({theme:{palette:e,spacing:a}})=>({alignItems:"flex-end",border:`1px solid ${e.divider}`,borderRadius:12,display:"flex",gap:a(1),paddingBlock:a(.5),paddingInline:`${a(1.5)} ${a(.5)}`,"&:focus-within":{borderColor:e.primary.main},"&:has(textarea:disabled)":{background:e.action.disabledBackground}})),x=n(i,{name:u,slot:"input"})(({theme:{spacing:e,typography:a}})=>({...a.body2,flex:1,paddingBlock:e(.75)})),v=n(o,{name:u,slot:"sendButton"})(({theme:{palette:e}})=>({background:e.primary.main,borderRadius:8,color:e.primary.contrastText,"&:hover":{background:e.primary.dark},"&.Mui-disabled":{background:e.action.disabledBackground,color:e.action.disabled}})),y=n(o,{name:u,slot:"stopButton"})(({theme:{palette:e}})=>({background:e.text.primary,borderRadius:8,color:e.background.paper,"&:hover":{background:e.text.secondary}})),B=n("span")({background:"currentColor",blockSize:10,borderRadius:2,inlineSize:10,margin:5}),C=n("div",{name:u,slot:"hint"})(({theme:{palette:e,typography:a}})=>({...a.caption,color:e.text.secondary}));export const AssistantComposer=({chat:o,onFocusChange:i})=>{let[n,u]=l(""),R=d(null),{isDesktop:S}=p(),w=m(b),z=m(g),$=m(h),j=o?.isLoading??!1,I=o?.isContinuable??!0,A=n.trim(),D=null!=o&&I&&!j&&A.length>0,P=s(()=>{o&&D&&(u(""),o.send(A),S||R.current?.blur())},[D,o,A,S]),T=s(e=>{e.preventDefault(),P()},[P]),E=s(e=>{"Enter"!==e.key||e.shiftKey||e.nativeEvent.isComposing||(e.preventDefault(),P())},[P]),F=s(()=>{o?.stop()},[o]);return a(k,{onSubmit:T,children:[!I&&e(C,{role:"status",children:e(r,{id:"sdk.assistant.composer.notContinuable",message:"This conversation can be read here, but it can no longer be continued."})}),a(f,{children:[e(x,{multiline:!0,maxRows:6,placeholder:w,value:n,disabled:!I,inputRef:R,onChange:e=>u(e.target.value),onKeyDown:E,onFocus:()=>i(!0),onBlur:()=>i(!1),inputProps:{"aria-label":w}}),j?e(t,{title:$,placement:"top",children:e(y,{size:"small","aria-label":$,onClick:F,children:e(B,{})})}):e(t,{title:z,placement:"top",children:e("span",{children:e(v,{type:"submit",size:"small","aria-label":z,disabled:!D,children:e(c,{name:"PaperPlaneRightBold",fontSize:"small"})})})})]}),e(C,{children:e(r,{id:"sdk.assistant.composer.disclaimer",message:"AI can be wrong. Check the result."})})]})};
@@ -0,0 +1,7 @@
1
+ export interface AssistantMarkdownProps {
2
+ readonly markdown: string;
3
+ /** True while the text is still arriving, so markup the model has not finished is completed. */
4
+ readonly isStreaming: boolean;
5
+ }
6
+ /** Assistant text of the thread: Markdown with lists, tables, links and code, without a bubble. */
7
+ export declare const AssistantMarkdown: import("react").MemoExoticComponent<({ markdown, isStreaming }: AssistantMarkdownProps) => import("react/jsx-runtime").JSX.Element>;
@@ -0,0 +1 @@
1
+ import{jsx as r}from"react/jsx-runtime";import{styled as e}from"@mui/material";import{memo as o}from"react";import a from"react-markdown";import i from"remark-gfm";import{completeStreamedMarkdown as n}from"./streamed-markdown.js";import{AssistantPanelName as t}from"./theme.js";let l=e("div",{name:t,slot:"assistantMessage"})(({theme:{palette:r,shape:e,spacing:o,typography:a}})=>({overflowWrap:"anywhere","& > :first-child":{marginBlockStart:0},"& > :last-child":{marginBlockEnd:0},"p, ul, ol, pre, blockquote":{marginBlock:o(1)},"h1, h2, h3, h4, h5, h6":{...a.subtitle2,marginBlock:o(1.5,.5)},"ul, ol":{paddingInlineStart:o(2.5)},a:{color:r.primary.main},code:{background:r.action.hover,borderRadius:e.borderRadius/2,fontFamily:"ui-monospace, Menlo, monospace",fontSize:"0.9em",paddingInline:o(.5)},pre:{background:r.action.hover,borderRadius:e.borderRadius,overflowX:"auto",padding:o(1),code:{background:"none",padding:0}},blockquote:{borderInlineStart:`2px solid ${r.divider}`,color:r.text.secondary,marginInline:0,paddingInlineStart:o(1.5)},"th, td":{borderBlockEnd:`1px solid ${r.divider}`,padding:o(.5,1),textAlign:"start",verticalAlign:"top"}})),d=e("div")({overflowX:"auto",table:{borderCollapse:"collapse",inlineSize:"100%"}}),m={a:({node:e,children:o,...a})=>r("a",{...a,target:"_blank",rel:"noopener noreferrer",children:o}),table:({node:e,children:o,...a})=>r(d,{children:r("table",{...a,children:o})})},s=[i],c=["img"];export const AssistantMarkdown=o(({markdown:e,isStreaming:o})=>r(l,{children:r(a,{components:m,remarkPlugins:s,disallowedElements:c,children:o?n(e):e})}));
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The attribute the panel carries on its root. The layouts key their styles on it: what the panel
3
+ * covers is hidden, and a sidebar shorter than its slot is stretched to the height the panel needs.
4
+ */
5
+ export declare const ASSISTANT_PANEL_SLOT = "slot[assistant]";
6
+ /**
7
+ * The built-in assistant panel: a header with the title and a close control, the thread and the
8
+ * composer. It covers the sidebar it is rendered in, and renders nothing while it is closed or when
9
+ * the application spec has no `assistant` section.
10
+ */
11
+ export declare const AssistantPanel: () => import("react/jsx-runtime").JSX.Element | null;
@@ -0,0 +1 @@
1
+ import{jsx as e,jsxs as t}from"react/jsx-runtime";import{IconButton as a,Tooltip as s,Typography as n,styled as r}from"@mui/material";import{useCallback as o}from"react";import{useLocalizable as i}from"#package/react-sdk";import{PhxIcon as l}from"#package/ui-sdk";import{AssistantComposer as d}from"./AssistantComposer.js";import{useAssistantPanel as m,useAssistantPanelConversation as p}from"./AssistantPanelProvider.js";import{AssistantThread as c}from"./AssistantThread.js";import{AssistantPanelName as h}from"./theme.js";let u={id:"sdk.assistant.panel.title",message:"Assistant"},g={id:"sdk.assistant.panel.close.label",message:"Close assistant"};export const ASSISTANT_PANEL_SLOT="slot[assistant]";let b=r("section",{name:h,slot:"root"})(({theme:{palette:e}})=>({background:`var(--phx-sidebar-background, ${e.background.paper})`,border:"var(--phx-sidebar-border, 0)",borderRadius:"inherit",display:"grid",gridTemplateColumns:"minmax(0, 1fr)",gridTemplateRows:"min-content minmax(0, 1fr) min-content",inset:0,overflow:"hidden",position:"absolute",zIndex:1})),f=r("header",{name:h,slot:"header"})(({theme:{palette:e,spacing:t}})=>({alignItems:"center",borderBlockEnd:`1px solid ${e.divider}`,display:"flex",gap:t(1),justifyContent:"space-between",paddingBlock:t(1),paddingInline:`var(--phx-sidebar-padding, ${t(2)}) ${t(1)}`})),x=r(n,{name:h,slot:"title"})({fontWeight:600}),k=r(a,{name:h,slot:"closeButton"})({}),j=e=>{e.target instanceof Element&&e.target.closest("button")&&e.preventDefault()};export const AssistantPanel=()=>{let a=m(),n=p(),r=i(u),h=i(g),v=a?.setOpen,A=o(()=>{v?.(!1)},[v]);return a?.open&&n?t(b,{"data-sidebar":"slot[assistant]","aria-label":r,onMouseDown:j,children:[t(f,{children:[e(x,{variant:"subtitle1",component:"h2",children:r}),e(s,{title:h,placement:"top",children:e(k,{size:"small","aria-label":h,onClick:A,children:e(l,{name:"X"})})})]}),e(c,{chat:n.chat,onRetry:n.reloadThread}),e(d,{chat:n.chat,onFocusChange:a.setInputFocused})]}):null};
@@ -0,0 +1,42 @@
1
+ import { type ReactNode } from 'react';
2
+ import { type AssistantChatState } from '../../assistant/index.js';
3
+ /**
4
+ * The built-in assistant panel of a universal layout as the layout and the toolbar button see it:
5
+ * whether it is open, and the few facts about it they render. It changes only when one of those does.
6
+ */
7
+ export interface AssistantPanelState {
8
+ readonly open: boolean;
9
+ readonly setOpen: (open: boolean) => void;
10
+ /** True when a reply finished while the panel was closed and the panel was not opened since. */
11
+ readonly hasUnreadReply: boolean;
12
+ /** True while the message input has focus; a phone layout gives the panel the whole screen then. */
13
+ readonly isInputFocused: boolean;
14
+ readonly setInputFocused: (isInputFocused: boolean) => void;
15
+ }
16
+ /** The conversation of the assistant panel. It changes with every chunk of a streaming reply. */
17
+ export interface AssistantPanelConversation {
18
+ /** Nothing until the chat of the current attempt has mounted. */
19
+ readonly chat: AssistantChatState | undefined;
20
+ /**
21
+ * Loads the thread from the server again, rejoining a reply that is still running there. It never
22
+ * sends a message again, because the server may already hold it; a message that did not reach the
23
+ * server is therefore gone from the thread and has to be written again.
24
+ */
25
+ readonly reloadThread: () => void;
26
+ }
27
+ /**
28
+ * The assistant panel of the layout this is rendered in, or nothing when the application spec has no
29
+ * `assistant` section or the layout does not carry the panel.
30
+ */
31
+ export declare const useAssistantPanel: () => AssistantPanelState | undefined;
32
+ /** The conversation of the assistant panel, under the same conditions as {@link useAssistantPanel}. */
33
+ export declare const useAssistantPanelConversation: () => AssistantPanelConversation | undefined;
34
+ export interface AssistantPanelProviderProps {
35
+ readonly children: ReactNode;
36
+ }
37
+ /**
38
+ * Owns the open state of the assistant panel and the single chat of the layout below it. The chat
39
+ * lives here and not in the panel, so a reply keeps streaming while the panel is closed. Without an
40
+ * `assistant` section in the application spec no chat is mounted and no request reaches the server.
41
+ */
42
+ export declare const AssistantPanelProvider: ({ children }: AssistantPanelProviderProps) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1 @@
1
+ import{jsx as e,jsxs as r}from"react/jsx-runtime";import{createContext as t,useCallback as s,useContext as o,useEffect as n,useMemo as a,useRef as i,useState as l}from"react";import{useLogger as d,useSpecAssistant as u}from"#package/react-sdk";import{useAssistantChat as c}from"../../assistant/index.js";let v=t(void 0),m=t(void 0);export const useAssistantPanel=()=>o(v);export const useAssistantPanelConversation=()=>o(m);let p=({onChange:e})=>{let r=c();return n(()=>{e(r)},[r,e]),n(()=>()=>{e(void 0)},[e]),null};export const AssistantPanelProvider=({children:t})=>{let o=u(),[c,x]=l(),[f,P]=l(!1),[h,j]=l(!1),[g,A]=l(0),[k,C]=l(!1),q=d(),L=s(e=>{P(e),e?j(!1):C(!1)},[]),S=c?.isLoading??!1,T=i(!1),b=s(()=>{T.current=!1,A(e=>e+1)},[]);n(()=>{!T.current||S||f||null==c||j(!0),T.current=S},[c,S,f]);let w=c?.error,y=c?.failedStatus;n(()=>{w&&q.error("The assistant request failed",{status:y},w)},[w,y,q]);let z=a(()=>o?{open:f,setOpen:L,hasUnreadReply:h,isInputFocused:k,setInputFocused:C}:void 0,[o,f,L,h,k]),B=a(()=>o?{chat:c,reloadThread:b}:void 0,[o,c,b]);return e(v.Provider,{value:z,children:r(m.Provider,{value:B,children:[o&&e(p,{onChange:x},g),t]})})};
@@ -0,0 +1,11 @@
1
+ import type { AssistantChatState } from '../../assistant/index.js';
2
+ export interface AssistantThreadProps {
3
+ readonly chat: AssistantChatState | undefined;
4
+ readonly onRetry: () => void;
5
+ }
6
+ /**
7
+ * The conversation: user bubbles, assistant Markdown, tool chips, and what the panel
8
+ * has to say itself, which is a greeting for an empty thread, a "Thinking…" line and a failure with
9
+ * its way out. It follows a streaming reply until the user scrolls up.
10
+ */
11
+ export declare const AssistantThread: ({ chat, onRetry }: AssistantThreadProps) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1 @@
1
+ import{jsx as e,jsxs as i}from"react/jsx-runtime";import{Trans as a}from"@lingui/react";import{Button as s,styled as t}from"@mui/material";import{useMemo as n}from"react";import{match as r}from"ts-pattern";import{PhxIcon as o}from"#package/ui-sdk";import{AssistantToolChips as l}from"./AssistantChips.js";import{AssistantMarkdown as d}from"./AssistantMarkdown.js";import{describeAssistantFailure as m}from"./assistant-failure.js";import{AssistantPanelName as p}from"./theme.js";import{buildThreadView as h}from"./thread-items.js";import{useFollowStream as c}from"./use-follow-stream.js";let g=t("div",{name:p,slot:"thread"})({display:"grid",position:"relative"}),u=t("div",{name:p,slot:"messages"})(({theme:{spacing:e,typography:i}})=>({...i.body2,display:"flex",flexDirection:"column",gap:e(1.5),overflowY:"auto",overscrollBehavior:"contain",paddingBlock:e(2),paddingInline:`var(--phx-sidebar-padding, ${e(2)})`,"& > :first-child":{marginBlockStart:"auto"}})),f=t("div",{name:p,slot:"userMessage"})(({theme:{palette:e,spacing:i}})=>({alignSelf:"flex-end",background:`hsl(from ${e.primary.main} h s l / 12%)`,borderRadius:"14px 14px 4px 14px",maxInlineSize:"85%",overflowWrap:"anywhere",paddingBlock:i(1),paddingInline:i(1.5),whiteSpace:"pre-wrap"})),k=t("div",{name:p,slot:"greeting"})({}),v=t("div",{name:p,slot:"status"})(({theme:{palette:e,typography:i}})=>({...i.caption,color:e.text.secondary})),w=t("div",{name:p,slot:"notice"})(({theme:{palette:e,shape:i,spacing:a}})=>({alignItems:"center",background:e.action.hover,borderRadius:i.borderRadius,display:"flex",flexWrap:"wrap",gap:a(1),justifyContent:"space-between",paddingBlock:a(1),paddingInline:a(1.5)})),b=t(s,{name:p,slot:"newReplyButton"})(({theme:{palette:e,spacing:i}})=>({background:e.text.primary,borderRadius:"calc(infinity * 1px)",color:e.background.paper,insetBlockEnd:i(1),insetInlineStart:"50%",position:"absolute",translate:"-50% 0","&:hover":{background:e.text.secondary}})),x=({item:i,isActive:a})=>r(i).with({kind:"user"},({text:i})=>e(f,{children:i})).with({kind:"text"},({markdown:i,isStreaming:a})=>e(d,{markdown:i,isStreaming:a})).with({kind:"tools"},({tools:i})=>e(l,{tools:i,isActive:a})).exhaustive();export const AssistantThread=({chat:t,onRetry:l})=>{let d=t?.messages,p=t?.isLoading??!1,{items:f,isThinking:y}=n(()=>h(d??[],p),[d,p]),j=n(()=>f.filter(({kind:e})=>"user"===e).length,[f]),I=t?.error?m(t.failedStatus):void 0,{scrollerRef:S,hasUnseenContent:T,handleScroll:A,scrollToEnd:B}=c(n(()=>({messages:d,isThinking:y,failure:I?.kind}),[d,y,I?.kind]),j);return i(g,{children:[i(u,{ref:S,onScroll:A,role:"log","aria-live":"polite",children:[t?.isLoaded===!0&&0===f.length&&!p&&!I&&e(k,{children:e(a,{id:"sdk.assistant.thread.greeting",message:"Hi. I can answer questions about this design and change it with you. Tell me what you need."})}),f.map(i=>e(x,{item:i,isActive:p&&i===f.at(-1)},i.id)),y&&e(v,{children:e(a,{id:"sdk.assistant.thread.thinking",message:"Thinking…"})}),I&&i(w,{role:"alert",children:[e("span",{children:r(I.kind).with("notAvailable",()=>e(a,{id:"sdk.assistant.failure.notAvailable",message:"The assistant is not available here."})).with("busy",()=>e(a,{id:"sdk.assistant.failure.busy",message:"The assistant is still working on an earlier message."})).with("unavailable",()=>e(a,{id:"sdk.assistant.failure.unavailable",message:"The assistant is unavailable right now."})).exhaustive()}),I.canRetry&&e(s,{size:"small",variant:"outlined",color:"inherit",onClick:l,children:e(a,{id:"sdk.assistant.failure.retry.label",message:"Try again"})})]})]}),T&&e(b,{size:"small",onClick:B,endIcon:e(o,{name:"CaretDown"}),children:e(a,{id:"sdk.assistant.thread.newReply.label",message:"New reply"})})]})};
@@ -0,0 +1,18 @@
1
+ /**
2
+ * What the panel tells the user when the assistant failed:
3
+ * - `notAvailable`: the server refuses this configuration or customer, and asking again will not help
4
+ * - `busy`: the server is still answering an earlier message of this thread
5
+ * - `unavailable`: anything else, including a dropped connection and a reply that broke off
6
+ */
7
+ export type AssistantFailureKind = 'notAvailable' | 'busy' | 'unavailable';
8
+ export interface AssistantFailure {
9
+ readonly kind: AssistantFailureKind;
10
+ /** True when loading the thread again may help, so the panel offers "Try again". */
11
+ readonly canRetry: boolean;
12
+ }
13
+ /**
14
+ * Maps a failed assistant request to what the panel shows, by the HTTP status the server answered
15
+ * with. A failure without a status never reached the server or broke off mid-reply; it gets the same
16
+ * generic message as an unknown status, because the chat client does not tell those apart.
17
+ */
18
+ export declare const describeAssistantFailure: (failedStatus: number | undefined) => AssistantFailure;
@@ -0,0 +1 @@
1
+ export const describeAssistantFailure=a=>403===a?{kind:"notAvailable",canRetry:!1}:409===a?{kind:"busy",canRetry:!0}:{kind:"unavailable",canRetry:!0};
@@ -0,0 +1,2 @@
1
+ export { AssistantPanel, ASSISTANT_PANEL_SLOT } from './AssistantPanel.js';
2
+ export { AssistantPanelProvider, useAssistantPanel, type AssistantPanelState } from './AssistantPanelProvider.js';
@@ -0,0 +1 @@
1
+ export{AssistantPanel,ASSISTANT_PANEL_SLOT}from"./AssistantPanel.js";export{AssistantPanelProvider,useAssistantPanel}from"./AssistantPanelProvider.js";
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Makes the Markdown of a reply that is still streaming renderable as it stands, so half-written
3
+ * markup does not flash as raw characters: an open code fence is closed, an inline code span, a link
4
+ * or a table header the model has not finished is held back, and open bold text is closed.
5
+ */
6
+ export declare const completeStreamedMarkdown: (text: string) => string;
@@ -0,0 +1,2 @@
1
+ let e=(e,t)=>e.split(t).length-1,t=/^\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)*\|?\s*$/;export const completeStreamedMarkdown=l=>{if(e(l,"```")%2==1)return`${l}
2
+ \`\`\``;let n=l.lastIndexOf("```"),s=-1===n?"":l.slice(0,n+3),r=l.slice(s.length),i=(e=>{let l=e.split("\n"),n=l.length;for(;n>0&&l[n-1]?.trimStart().startsWith("|");)n-=1;let s=l.slice(n),r=s[1],i=null!=r&&s.length>2&&t.test(r);return 0===s.length||i?e:l.slice(0,n).join("\n")})((e(r,"`")%2==1?r.slice(0,r.lastIndexOf("`")):r).replace(/!?\[([^\]\n]*)\]\([^)\n]*$/,"$1").replace(/!?\[[^\]\n]*$/,"")),c=e(i,"**")%2==1?`${i}**`:i;return`${s}${c}`};
@@ -0,0 +1,2 @@
1
+ /** Theme component name of the built-in assistant panel; its slots are listed in the ui-sdk theme types. */
2
+ export declare const AssistantPanelName: 'PhxAssistantPanel';
@@ -0,0 +1 @@
1
+ export const AssistantPanelName="PhxAssistantPanel";
@@ -0,0 +1,37 @@
1
+ import type { UIMessage } from '@tanstack/ai-react';
2
+ import { type AssistantToolChip } from './tool-chip.js';
3
+ /** A tool call of the thread, with what its chip shows and what a click on the chip logs. */
4
+ export interface AssistantToolItem {
5
+ readonly id: string;
6
+ readonly chip: AssistantToolChip;
7
+ readonly input: unknown;
8
+ readonly result: unknown;
9
+ }
10
+ /**
11
+ * One block of the thread, in reading order: a user bubble, a stretch of assistant Markdown, or a
12
+ * row of chips for the tool calls that follow each other without text in between.
13
+ */
14
+ export type AssistantThreadItem = {
15
+ readonly kind: 'user';
16
+ readonly id: string;
17
+ readonly text: string;
18
+ } | {
19
+ readonly kind: 'text';
20
+ readonly id: string;
21
+ readonly markdown: string;
22
+ readonly isStreaming: boolean;
23
+ } | {
24
+ readonly kind: 'tools';
25
+ readonly id: string;
26
+ readonly tools: readonly AssistantToolItem[];
27
+ };
28
+ export interface AssistantThreadView {
29
+ readonly items: readonly AssistantThreadItem[];
30
+ /** True while a reply runs and neither streaming text nor a row of tool chips shows that it does. */
31
+ readonly isThinking: boolean;
32
+ }
33
+ /**
34
+ * Lays the messages of the chat out as the blocks the thread renders. Reasoning is left out; tool
35
+ * results are not shown on their own but decide the state of the chip of their call.
36
+ */
37
+ export declare const buildThreadView: (messages: ReadonlyArray<UIMessage>, isReplying: boolean) => AssistantThreadView;
@@ -0,0 +1 @@
1
+ import{describeToolCall as t}from"./tool-chip.js";let e=t=>{try{return JSON.parse(t)}catch{return t}},r=t=>t.map(t=>"text"===t.type?t.content:"").join("").trim();export const buildThreadView=(o,n)=>{let l=(t=>{let r=new Map;for(let o of t)for(let t of o.parts)"tool-result"===t.type&&r.set(t.toolCallId,{result:"string"==typeof t.content?e(t.content):t.content,error:"error"===t.state?t.error??"The tool call failed.":void 0});return r})(o),i=[],s=o.at(-1),a=t=>{let e=i.at(-1);e?.kind==="tools"?i[i.length-1]={...e,tools:[...e.tools,t]}:i.push({kind:"tools",id:t.id,tools:[t]})};for(let e of o){if("user"===e.role){let t=r(e.parts);t&&i.push({kind:"user",id:e.id,text:t});continue}let o=n&&e===s;e.parts.forEach((r,n)=>{let s=`${e.id}:${n}`;if("text"===r.type){if(r.content.trim()){let t=o&&n===e.parts.length-1;i.push({kind:"text",id:s,markdown:r.content,isStreaming:t})}return}if("tool-call"!==r.type)return;let p=l.get(r.id),d=r.input,u=r.output??p?.result;a({id:r.id,input:d,result:u,chip:t({name:r.name,input:d,result:u,error:p?.error,isReplyRunning:o})})})}let p=i.at(-1),d=s?.role==="assistant"&&(p?.kind==="text"&&p.isStreaming||p?.kind==="tools");return{items:i,isThinking:n&&!d}};
@@ -0,0 +1,37 @@
1
+ /**
2
+ * How a tool call of the assistant looks in the thread: still running, finished, a change that was
3
+ * not kept (`failed`), or a read that answered with errors (`flawed`), which matters less because
4
+ * the assistant usually just asks again.
5
+ */
6
+ export type AssistantToolChipState = 'running' | 'done' | 'failed' | 'flawed';
7
+ /** What a tool chip is called: the operation names the model gave, a fixed text, or the tool's name. */
8
+ export type AssistantToolChipLabel = {
9
+ readonly kind: 'text';
10
+ readonly text: string;
11
+ } | {
12
+ readonly kind: 'updateDesign';
13
+ } | {
14
+ readonly kind: 'readDesign';
15
+ };
16
+ export interface AssistantToolChip {
17
+ readonly label: AssistantToolChipLabel;
18
+ readonly state: AssistantToolChipState;
19
+ }
20
+ /** A tool call of a message as the chip reads it. */
21
+ export interface AssistantToolCall {
22
+ readonly name: string;
23
+ /** The parsed input of the call; nothing while the model is still writing it. */
24
+ readonly input: unknown;
25
+ /** The result of the call, parsed from JSON; nothing while there is none. */
26
+ readonly result: unknown;
27
+ /** The error text of a call the chat client could not run. */
28
+ readonly error: string | undefined;
29
+ /** True while the reply this call belongs to is still running. */
30
+ readonly isReplyRunning: boolean;
31
+ }
32
+ /**
33
+ * Describes one tool call for its chip: the label in the order operation names, fixed text for the
34
+ * `apply` and `query` tools, tool name; and whether the call runs, finished, or went wrong. Why it
35
+ * went wrong is not part of it: the tool result words that for the model, not for the user.
36
+ */
37
+ export declare const describeToolCall: (call: AssistantToolCall) => AssistantToolChip;
@@ -0,0 +1 @@
1
+ import{Kind as e,parse as t}from"graphql";import{z as n}from"zod";import{applyTool as r,queryTool as a}from"@salsita-ai/contracts/assistant";let o=n.object({operations:n.array(n.object({query:n.string(),operationName:n.string().nullish()}))}),i=n.object({results:n.array(n.object({errors:n.array(n.unknown()).optional()})),committed:n.boolean()});export const describeToolCall=n=>{let s=(({name:n,input:i})=>{let s,l=(s=o.safeParse(i)).success?s.data.operations.flatMap(({query:n,operationName:r})=>r??(n=>{try{let r=t(n).definitions.find(t=>t.kind===e.OPERATION_DEFINITION);return r?.kind===e.OPERATION_DEFINITION?r.name?.value:void 0}catch{return}})(n)??[]):[];return l.length>0?{kind:"text",text:l.join(", ")}:n===r.name?{kind:"updateDesign"}:n===a.name?{kind:"readDesign"}:{kind:"text",text:n}})(n);return null==n.result&&null==n.error?{label:s,state:n.isReplyRunning?"running":"done"}:{label:s,state:(({name:e,result:t,error:n})=>{if(null!=n)return e===a.name?"flawed":"failed";let o=i.safeParse(t);if(!o.success)return"done";let{results:s,committed:l}=o.data;return e===r.name?l?"done":"failed":s.some(({errors:e})=>(e?.length??0)>0)||0===s.length?"flawed":"done"})(n)}};
@@ -0,0 +1,17 @@
1
+ import { type RefObject } from 'react';
2
+ export interface FollowStream<ELEMENT extends HTMLElement> {
3
+ readonly scrollerRef: RefObject<ELEMENT>;
4
+ /** True when content arrived while the user was reading further up. */
5
+ readonly hasUnseenContent: boolean;
6
+ readonly handleScroll: () => void;
7
+ readonly scrollToEnd: () => void;
8
+ }
9
+ /**
10
+ * Keeps a scrolling thread at its end while content arrives, until the user scrolls up; from then on
11
+ * it only reports that there is unseen content, and follows again once the user is back at the end.
12
+ *
13
+ * @param content changes whenever the thread's content does
14
+ * @param followKey changes when the thread must return to its end whatever the user did, such as
15
+ * when the user sends a message
16
+ */
17
+ export declare const useFollowStream: <ELEMENT extends HTMLElement>(content: unknown, followKey: unknown) => FollowStream<ELEMENT>;
@@ -0,0 +1 @@
1
+ import{useCallback as r,useEffect as e,useLayoutEffect as t,useRef as l,useState as c}from"react";export const useFollowStream=(o,n)=>{let s=l(null),u=l(!0),[i,g]=c(!1),h=r(()=>{let r=s.current;u.current=!0,g(!1),r&&(r.scrollTop=r.scrollHeight)},[]),H=r(()=>{let r=s.current;r&&(u.current=r.scrollHeight-r.scrollTop-r.clientHeight<=24,u.current&&g(!1))},[]);return t(()=>{h()},[n,h]),t(()=>{let r=s.current;r&&(u.current?r.scrollTop=r.scrollHeight:r.scrollHeight>r.clientHeight&&g(!0))},[o]),e(()=>{let r=s.current;if(!r)return;let e=new ResizeObserver(()=>{u.current&&(r.scrollTop=r.scrollHeight)});return e.observe(r),()=>{e.disconnect()}},[]),{scrollerRef:s,hasUnseenContent:i,handleScroll:H,scrollToEnd:h}};