@jxsuite/studio 5.5.0 → 5.6.0

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 (221) hide show
  1. package/LICENSE +1 -1
  2. package/dist/chunks/{2020-g8mwpbkq.js → 2020-zvax9fmb.js} +3 -3
  3. package/dist/chunks/adapter-6zhjbpmd.js +4 -0
  4. package/dist/chunks/cf-account-picker-zfycs4k2.js +4 -0
  5. package/dist/chunks/cf-settings-fpa485q3.js +4 -0
  6. package/dist/chunks/components-b0vh4sq8.js +4 -0
  7. package/dist/chunks/content-source-z5t0d0h9.js +4 -0
  8. package/dist/chunks/convert-to-repeater-x19jr6c7.js +4 -0
  9. package/dist/chunks/editor-7e23n4a8.js +4 -0
  10. package/dist/chunks/entry-commands-5cm6aba2.js +4 -0
  11. package/dist/chunks/extension-sections-y90sa0n4.js +4 -0
  12. package/dist/chunks/file-ops-hv7gf757.js +4 -0
  13. package/dist/chunks/files-kb0epdxf.js +4 -0
  14. package/dist/chunks/format-host-eg1wy2bp.js +4 -0
  15. package/dist/chunks/fs-events-ky3vxfmh.js +4 -0
  16. package/dist/chunks/git-panel-yyg3bzp2.js +4 -0
  17. package/dist/chunks/grid-view-5mqv1cbd.css +1 -0
  18. package/dist/chunks/grid-view-d9mybhn0.js +51 -0
  19. package/dist/chunks/iframe-host-9wdf0hes.js +4 -0
  20. package/dist/chunks/index-2tf0m2ne.js +4 -0
  21. package/dist/chunks/{index-arg188jq.js → index-vnwy9d44.js} +3 -3
  22. package/dist/chunks/{javascript-zcdmye2k.js → javascript-f0nrbdjb.js} +2 -2
  23. package/dist/chunks/{jsonMode-6x5e1nbf.js → jsonMode-nfa90dtf.js} +5 -5
  24. package/dist/chunks/jx-validate-7w11db9v.js +4 -0
  25. package/dist/chunks/layers-7zasrpse.js +4 -0
  26. package/dist/chunks/layers-panel-9nzc4fdq.js +4 -0
  27. package/dist/chunks/lit-html-5qr7nbng.js +4 -0
  28. package/dist/chunks/media-picker-maevn9d4.js +4 -0
  29. package/dist/chunks/menu-wq6q0v3y.js +4 -0
  30. package/dist/chunks/{monaco-binding-4az2kh5p.js → monaco-binding-antn79jf.js} +3 -3
  31. package/dist/chunks/monaco-buffer-9ry2rn5x.js +4 -0
  32. package/dist/chunks/{monaco-setup-d7mwkcx3.js → monaco-setup-k8s40vwg.js} +3 -3
  33. package/dist/chunks/notify-g91gjd6w.js +4 -0
  34. package/dist/chunks/platform-jdwjp2vt.js +4 -0
  35. package/dist/chunks/publish-panel-8yym447p.js +4 -0
  36. package/dist/chunks/quick-search-9bzm9kh3.js +4 -0
  37. package/dist/chunks/references-znssfqvq.js +4 -0
  38. package/dist/chunks/right-panel-czbzktm8.js +4 -0
  39. package/dist/chunks/section-registry-0rtq73qs.js +4 -0
  40. package/dist/chunks/studio-011em5cf.js +5 -0
  41. package/dist/chunks/studio-02y9cb4h.js +6 -0
  42. package/dist/chunks/{studio-7gqyhjns.js → studio-09n3fyy0.js} +4 -4
  43. package/dist/chunks/{studio-xcng8nx7.js → studio-0c3fpdqj.js} +5 -5
  44. package/dist/chunks/studio-0mm38y62.js +5 -0
  45. package/dist/chunks/studio-0nm2skex.js +4 -0
  46. package/dist/chunks/{studio-73czgk6a.js → studio-153mwkyq.js} +4 -4
  47. package/dist/chunks/{studio-f8jyp45j.js → studio-1pxvgx1t.js} +4 -4
  48. package/dist/chunks/studio-27tc8q0h.js +5 -0
  49. package/dist/chunks/studio-28pz4s1k.js +5 -0
  50. package/dist/chunks/{studio-x5esgzct.js → studio-36s6qjgq.js} +4 -4
  51. package/dist/chunks/studio-4yztve2t.js +5 -0
  52. package/dist/chunks/{studio-x9sagq2d.js → studio-5fc0728f.js} +7 -7
  53. package/dist/chunks/{studio-7jazdy6y.js → studio-654fxcr0.js} +4 -4
  54. package/dist/chunks/studio-6exg5z4x.js +4 -0
  55. package/dist/chunks/studio-6k1eswp9.js +5 -0
  56. package/dist/chunks/{studio-5wfrgee9.js → studio-6zyt56ek.js} +4 -4
  57. package/dist/chunks/studio-7djymzt2.js +5 -0
  58. package/dist/chunks/{studio-b4epb26c.js → studio-7w04zw8h.js} +7 -7
  59. package/dist/chunks/studio-805fwp9k.js +5 -0
  60. package/dist/chunks/{studio-k1eaqyzm.js → studio-81twpmzj.js} +4 -4
  61. package/dist/chunks/studio-891t1tv9.js +5 -0
  62. package/dist/chunks/{studio-5n1km79v.js → studio-9n2j9nqk.js} +4 -4
  63. package/dist/chunks/{studio-he2fhrgp.js → studio-9tkaykt5.js} +4 -4
  64. package/dist/chunks/studio-ant1zbp3.js +5 -0
  65. package/dist/chunks/studio-b29h7g7s.js +5 -0
  66. package/dist/chunks/studio-b5rxwkj2.js +5 -0
  67. package/dist/chunks/studio-d007j0z3.js +7 -0
  68. package/dist/chunks/studio-dknr8m7b.js +14 -0
  69. package/dist/chunks/{studio-eq27rrr0.js → studio-dmymvk2j.js} +3 -3
  70. package/dist/chunks/studio-dtmdjmqb.js +5 -0
  71. package/dist/chunks/studio-eb5sty2k.js +5 -0
  72. package/dist/chunks/{studio-hc9q4j24.js → studio-ebt49j3c.js} +5 -5
  73. package/dist/chunks/studio-eegyf5vg.js +5 -0
  74. package/dist/chunks/studio-frv7vjhw.js +5 -0
  75. package/dist/chunks/studio-ft8pfpp0.js +5 -0
  76. package/dist/chunks/{studio-mhhc9btt.js → studio-gj4j9e7k.js} +4 -4
  77. package/dist/chunks/{studio-hf4qdjc8.js → studio-gxna6yta.js} +4 -4
  78. package/dist/chunks/studio-je15p5v8.js +6 -0
  79. package/dist/chunks/{studio-21enxdr8.js → studio-jsr14n1s.js} +4 -4
  80. package/dist/chunks/studio-k9pwvmz8.js +5 -0
  81. package/dist/chunks/{studio-p1eggxcf.js → studio-ks0ecgjx.js} +3 -3
  82. package/dist/chunks/studio-mb8jhtr4.js +64 -0
  83. package/dist/chunks/studio-mf41717j.js +5 -0
  84. package/dist/chunks/{studio-625ezz59.js → studio-mm8x32gg.js} +4 -4
  85. package/dist/chunks/{studio-pkkw32j0.js → studio-n2e133d6.js} +4 -4
  86. package/dist/chunks/{studio-c4pnt1c6.js → studio-njmg0e73.js} +5 -5
  87. package/dist/chunks/studio-nvykrgbj.js +4 -0
  88. package/dist/chunks/studio-nw4q9sdn.js +5 -0
  89. package/dist/chunks/studio-p6avneaq.js +5 -0
  90. package/dist/chunks/studio-pvj970st.js +5 -0
  91. package/dist/chunks/studio-r7yeeqc4.js +5 -0
  92. package/dist/chunks/studio-s85vah66.js +6 -0
  93. package/dist/chunks/studio-sadckcpp.js +7 -0
  94. package/dist/chunks/studio-sgzss898.js +9 -0
  95. package/dist/chunks/{studio-7by9a93k.js → studio-t0v47dpc.js} +4 -4
  96. package/dist/chunks/{studio-jnb285we.js → studio-tj28pxa1.js} +4 -4
  97. package/dist/chunks/{studio-6j2jn55a.js → studio-tzs6njnb.js} +8 -8
  98. package/dist/chunks/studio-vz7b43ng.js +5 -0
  99. package/dist/chunks/studio-w8w9frjy.js +16 -0
  100. package/dist/chunks/{studio-xc8ebsts.js → studio-wr0pq67n.js} +4 -4
  101. package/dist/chunks/{studio-8r4y2x0z.js → studio-y164brht.js} +4 -4
  102. package/dist/chunks/studio-y17505ge.js +928 -0
  103. package/dist/chunks/studio-ymsjnvp9.js +5 -0
  104. package/dist/chunks/studio-z1vdxq0e.js +5 -0
  105. package/dist/chunks/studio-z47mm077.js +10 -0
  106. package/dist/chunks/studio-zkkdbbvd.js +9 -0
  107. package/dist/chunks/{studio-awqghqxv.js → studio-zkvyd3ev.js} +4 -4
  108. package/dist/chunks/{tsMode-rtwt7w1a.js → tsMode-eq26t9gs.js} +5 -5
  109. package/dist/chunks/workspace-emt8454d.js +4 -0
  110. package/dist/chunks/ws-client-3gxqp06s.js +4 -0
  111. package/dist/iframe-entry.js +14 -14
  112. package/dist/manifest.json +62 -59
  113. package/dist/studio.css +1 -1
  114. package/dist/studio.js +115 -117
  115. package/package.json +5 -5
  116. package/src/account/action-flow.ts +294 -0
  117. package/src/account/action-required.ts +68 -0
  118. package/src/account-status.ts +29 -1
  119. package/src/browse/library-model.ts +3 -1
  120. package/src/browse/library-preview.ts +6 -2
  121. package/src/canvas/asset-refs.ts +3 -1
  122. package/src/canvas/asset-resolve.ts +79 -1
  123. package/src/canvas/iframe-entry.ts +38 -1
  124. package/src/canvas/iframe-host.ts +85 -0
  125. package/src/canvas/iframe-protocol.ts +13 -0
  126. package/src/files/asset-versions.ts +294 -0
  127. package/src/files/files.ts +61 -9
  128. package/src/files/fs-events.ts +279 -28
  129. package/src/files/media-meta.ts +2 -1
  130. package/src/files/media-paths.ts +5 -2
  131. package/src/files/media-upload.ts +5 -1
  132. package/src/new-project/add-repo-modal.ts +12 -1
  133. package/src/new-project/new-project-modal.ts +12 -1
  134. package/src/panels/git-panel.ts +25 -2
  135. package/src/platform-errors.ts +61 -1
  136. package/src/platforms/cloud.ts +389 -50
  137. package/src/platforms/devserver.ts +17 -1
  138. package/src/publish/pages-service.ts +17 -7
  139. package/src/services/ai-models.ts +30 -6
  140. package/src/services/ai-system-prompt.ts +81 -51
  141. package/src/settings/preferences-accounts.ts +42 -3
  142. package/src/settings/preferences-dialog.ts +8 -1
  143. package/src/studio.ts +18 -1
  144. package/src/surfaces/dialog.ts +5 -0
  145. package/src/surfaces/welcome.json +86 -1
  146. package/src/surfaces/welcome.ts +61 -2
  147. package/src/types.ts +84 -2
  148. package/src/ui/ai-managed-connect.ts +41 -4
  149. package/src/ui/media-picker.ts +2 -1
  150. package/dist/chunks/adapter-bsbs78x9.js +0 -4
  151. package/dist/chunks/cf-account-picker-gnm7xeqb.js +0 -4
  152. package/dist/chunks/cf-settings-9y6jyscp.js +0 -4
  153. package/dist/chunks/components-s51h3b7c.js +0 -4
  154. package/dist/chunks/content-source-ty3be0qg.js +0 -4
  155. package/dist/chunks/convert-to-repeater-c3e7q61z.js +0 -4
  156. package/dist/chunks/editor-t002b41m.js +0 -4
  157. package/dist/chunks/entry-commands-sb72e9gy.js +0 -4
  158. package/dist/chunks/extension-sections-ktn2ntnj.js +0 -4
  159. package/dist/chunks/file-ops-78zeytkm.js +0 -4
  160. package/dist/chunks/files-gatzrz8v.js +0 -4
  161. package/dist/chunks/format-host-r59b1651.js +0 -4
  162. package/dist/chunks/fs-events-f8h91ndt.js +0 -4
  163. package/dist/chunks/git-panel-y12zf7nj.js +0 -4
  164. package/dist/chunks/grid-view-rfd8b2pg.js +0 -51
  165. package/dist/chunks/grid-view-tafenfm2.css +0 -1
  166. package/dist/chunks/iframe-host-vsrsj20g.js +0 -4
  167. package/dist/chunks/index-5aghyrfh.js +0 -4
  168. package/dist/chunks/jx-validate-653krkrf.js +0 -4
  169. package/dist/chunks/layers-32p8gtae.js +0 -4
  170. package/dist/chunks/layers-panel-rs58m105.js +0 -4
  171. package/dist/chunks/lit-html-54bmkwz3.js +0 -4
  172. package/dist/chunks/media-picker-5bb04wct.js +0 -4
  173. package/dist/chunks/menu-cbha7w2e.js +0 -4
  174. package/dist/chunks/monaco-buffer-9vbbyfr1.js +0 -4
  175. package/dist/chunks/notify-dc1nz5ar.js +0 -4
  176. package/dist/chunks/platform-2m49s1zf.js +0 -4
  177. package/dist/chunks/publish-panel-nx3ycmma.js +0 -4
  178. package/dist/chunks/quick-search-yzrzh9x3.js +0 -4
  179. package/dist/chunks/references-vx7mbj9f.js +0 -4
  180. package/dist/chunks/right-panel-7jzpbxja.js +0 -4
  181. package/dist/chunks/section-registry-gjryse41.js +0 -4
  182. package/dist/chunks/studio-3znfhcxg.js +0 -5
  183. package/dist/chunks/studio-58qam27v.js +0 -4
  184. package/dist/chunks/studio-6j59fwaw.js +0 -5
  185. package/dist/chunks/studio-7z69g1gx.js +0 -5
  186. package/dist/chunks/studio-93j3v6zx.js +0 -5
  187. package/dist/chunks/studio-9gf6v8zy.js +0 -5
  188. package/dist/chunks/studio-9rhmettf.js +0 -4
  189. package/dist/chunks/studio-ac5aq677.js +0 -10
  190. package/dist/chunks/studio-as65vbns.js +0 -5
  191. package/dist/chunks/studio-bkv0sga4.js +0 -6
  192. package/dist/chunks/studio-bttmqef4.js +0 -5
  193. package/dist/chunks/studio-c3y9d41k.js +0 -5
  194. package/dist/chunks/studio-ccegp7gz.js +0 -9
  195. package/dist/chunks/studio-chxabn98.js +0 -6
  196. package/dist/chunks/studio-cktcdsxz.js +0 -16
  197. package/dist/chunks/studio-cnspfke5.js +0 -5
  198. package/dist/chunks/studio-de3p1ktm.js +0 -5
  199. package/dist/chunks/studio-fsbcn530.js +0 -928
  200. package/dist/chunks/studio-gftga7a3.js +0 -64
  201. package/dist/chunks/studio-hf1rpcq0.js +0 -4
  202. package/dist/chunks/studio-hz02hq70.js +0 -5
  203. package/dist/chunks/studio-jsv3s3s1.js +0 -5
  204. package/dist/chunks/studio-ks7cksh0.js +0 -5
  205. package/dist/chunks/studio-nptx0dbs.js +0 -5
  206. package/dist/chunks/studio-pg0z7k98.js +0 -5
  207. package/dist/chunks/studio-pve0b2f2.js +0 -5
  208. package/dist/chunks/studio-q7zmjv3b.js +0 -6
  209. package/dist/chunks/studio-rpmvw7kj.js +0 -5
  210. package/dist/chunks/studio-rwhss52s.js +0 -5
  211. package/dist/chunks/studio-sbg3g3h0.js +0 -5
  212. package/dist/chunks/studio-sq1bn60g.js +0 -14
  213. package/dist/chunks/studio-vwz7rma2.js +0 -5
  214. package/dist/chunks/studio-wa55e8gf.js +0 -5
  215. package/dist/chunks/studio-x0mzdha2.js +0 -5
  216. package/dist/chunks/studio-xrgpakvc.js +0 -9
  217. package/dist/chunks/studio-xt3djk1w.js +0 -7
  218. package/dist/chunks/studio-yh3nggx1.js +0 -7
  219. package/dist/chunks/studio-zmecp6hq.js +0 -5
  220. package/dist/chunks/workspace-g6dvaete.js +0 -4
  221. package/dist/chunks/ws-client-shf41j5p.js +0 -4
@@ -1,11 +1,10 @@
1
1
  /// <reference lib="dom" />
2
2
  /**
3
- * Pages-service — Cloudflare Pages publish domain logic over the PAL's
4
- * `cfApi` passthrough. Works identically on every platform that provides it:
5
- * the dev server / desktop (user API token via cf-settings → /__studio/cf/proxy)
6
- * and the cloud platform (hosted OAuth). Publish state is written to
7
- * project.json `build.deploy` — it travels with the repo, so any Studio can
8
- * tell whether the publish workflow already exists.
3
+ * Pages-service — Cloudflare Pages publish domain logic over the PAL's `cfApi` passthrough. Works
4
+ * identically on every platform that provides it: the dev server / desktop (user API token via
5
+ * cf-settings → /__studio/cf/proxy) and the cloud platform (hosted OAuth). Publish state is written
6
+ * to project.json `build.deploy` — it travels with the repo, so any Studio can tell whether the
7
+ * publish workflow already exists.
9
8
  *
10
9
  * @license MIT
11
10
  */
@@ -13,6 +12,7 @@
13
12
  import { updateWranglerConfig } from "@jxsuite/create/scaffold";
14
13
  import type { DeployConfig, ProjectConfig } from "@jxsuite/schema/types";
15
14
  import { getPlatform } from "../platform";
15
+ import { actIfRequired } from "../account/action-flow";
16
16
  import { updateSiteConfig } from "../site-context";
17
17
 
18
18
  export interface CfAccount {
@@ -52,12 +52,22 @@ export function platformSupportsPublish(): boolean {
52
52
  }
53
53
  }
54
54
 
55
+ /**
56
+ * One Cloudflare call. A refusal that offers an action (desktop.md §10.4) is offered here, once for
57
+ * every publish step, and still thrown: the step that asked failed, and its caller reports that in
58
+ * its own words while the offer says how to fix it.
59
+ */
55
60
  async function cfApi<T>(path: string, init?: { method?: string; body?: unknown }): Promise<T> {
56
61
  const api = getPlatform().cfApi;
57
62
  if (!api) {
58
63
  throw new Error("This platform cannot reach the Cloudflare API");
59
64
  }
60
- return (await api(path, init)) as T;
65
+ try {
66
+ return (await api(path, init)) as T;
67
+ } catch (error) {
68
+ void actIfRequired(error);
69
+ throw error;
70
+ }
61
71
  }
62
72
 
63
73
  export async function listAccounts(): Promise<CfAccount[]> {
@@ -2,17 +2,17 @@
2
2
  /**
3
3
  * Ai-models.js — available-model listing from the studio AI proxy.
4
4
  *
5
- * Fetches the proxy's /models endpoint (the chat endpoint's sibling), forwarding the
6
- * stored API key as X-Api-Key and the endpoint override as X-Api-Base-URL so the proxy
7
- * lists models from the user's chosen provider. Shared by the credentials form and the
8
- * chat composer's model picker; results are cached module-wide until invalidated (e.g.
9
- * after credentials change).
5
+ * Fetches the proxy's /models endpoint (the chat endpoint's sibling), forwarding the stored API key
6
+ * as X-Api-Key and the endpoint override as X-Api-Base-URL so the proxy lists models from the
7
+ * user's chosen provider. Shared by the credentials form and the chat composer's model picker;
8
+ * results are cached module-wide until invalidated (e.g. after credentials change).
10
9
  *
11
10
  * @license MIT
12
11
  */
13
12
 
14
- import type { AiModelsResponse } from "@jxsuite/protocol";
13
+ import type { AiModelsResponse, OfferedAction } from "@jxsuite/protocol";
15
14
  import { getPlatform } from "../platform";
15
+ import { offeredActions } from "../platform-errors";
16
16
  import { getBaseUrl, getOpenAiKey, hasOpenAiKey, storedModel } from "./ai-settings";
17
17
  import { SETTINGS } from "./settings/definitions";
18
18
  import { onSettingsChanged } from "./settings/kernel";
@@ -76,6 +76,14 @@ let proxyManaged = false;
76
76
  let proxyDefaultModel = "";
77
77
  let proxyCode: AiModelsResponse["code"];
78
78
  let proxyModelsError = "";
79
+ /** What an `action_required` probe offered; null for every other answer. */
80
+ let proxyAction: ProxyActionOffer | null = null;
81
+
82
+ /** What a managed backend says the assistant needs first, in the backend's own words. */
83
+ export interface ProxyActionOffer {
84
+ detail?: string;
85
+ actions: OfferedAction[];
86
+ }
79
87
 
80
88
  /**
81
89
  * One-shot capability probe shared by every credentials gate, plus the hosts to repaint when it
@@ -118,6 +126,7 @@ export function resetModelCache() {
118
126
  proxyDefaultModel = "";
119
127
  proxyCode = undefined;
120
128
  proxyModelsError = "";
129
+ proxyAction = null;
121
130
  /* The probe's result IS the flags above. Clearing them while keeping the settled promise
122
131
  would strand every gate on a permanent "unconfigured, unmanaged" reading — ensureProxyProbe
123
132
  would no-op forever and the managed option would vanish until a full reload. */
@@ -278,6 +287,14 @@ export function proxyModelsErrorMessage(): string {
278
287
  return proxyModelsError;
279
288
  }
280
289
 
290
+ /**
291
+ * The offer the last probe carried — set only when the backend answered `action_required` (ai.md
292
+ * §2.1), and null for every other answer, so no gate can offer an action the backend did not name.
293
+ */
294
+ export function proxyActionOffer(): ProxyActionOffer | null {
295
+ return proxyAction;
296
+ }
297
+
281
298
  /** The proxy's preferred model id ("" when it does not declare one). */
282
299
  export function getProxyDefaultModel(): string {
283
300
  return proxyDefaultModel;
@@ -369,6 +386,13 @@ export async function fetchAvailableModels(
369
386
  proxyDefaultModel = data.defaultModel ?? "";
370
387
  proxyCode = data.code;
371
388
  proxyModelsError = data.upstreamError !== undefined ? (data.upstreamMessage ?? "") : "";
389
+ proxyAction =
390
+ data.code === "action_required"
391
+ ? {
392
+ actions: offeredActions(data.actions),
393
+ ...(typeof data.detail === "string" && data.detail ? { detail: data.detail } : {}),
394
+ }
395
+ : null;
372
396
  /* Capabilities are kept, not dropped. The backend has reported `toolSupport` all along and the
373
397
  ingest mapped `{id, name}` only, so a Workers AI model that cannot call tools looked exactly
374
398
  like one that can — the agent loop ran, called nothing, and answered as if that were normal.
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Ai-system-prompt.js — Dynamic system prompt builder for the Jx AI assistant
3
3
  *
4
- * Constructs the system prompt based on the current project context, open document,
5
- * and available components. The quality of AI output depends critically on this file.
4
+ * Constructs the system prompt based on the current project context, open document, and available
5
+ * components. The quality of AI output depends critically on this file.
6
6
  *
7
7
  * @license MIT
8
8
  */
@@ -656,6 +656,75 @@ When asked to build a site with multiple pages:
656
656
 
657
657
  // ─── System prompt builder ───────────────────────────────────────────────────
658
658
 
659
+ const ROLE = `You are an expert Jx builder assistant embedded in Jx Studio. You help users build websites, components, pages, and layouts using the Jx JSON schema. The live jxsuite.com marketing site is built entirely with Jx — you can produce production-quality Jx code.`;
660
+
661
+ /**
662
+ * What to do when a tool call fails. Static: it names tools and validation messages, never the
663
+ * state, so it belongs to the cacheable prefix rather than after the dynamic sections.
664
+ */
665
+ const ERROR_RECOVERY = `## Error Recovery
666
+
667
+ If a tool call fails (returns { success: false }):
668
+ 1. Read the error message carefully — it includes a "→ Fix:" hint telling you exactly how to correct the error.
669
+ 2. Each error points to a specific path in the document and a specific rule violation.
670
+ 3. Apply the suggested fix using set_property, delete_node, or add_child as appropriate.
671
+ 4. Do NOT re-issue the exact same tool call with the same arguments — you must CHANGE something.
672
+ 5. If you see the SAME error after 2 attempts, try a completely different approach (e.g., remove and re-add the node instead of patching it).
673
+
674
+ ### Common validation errors and their fixes:
675
+
676
+ | Error pattern | What happened | How to fix |
677
+ |---|---|---|
678
+ | "must NOT have additional property" in style | You used a non-camelCase CSS property (e.g. "background-color") or put an HTML attribute directly on the element. | Use camelCase: "backgroundColor". Put aria-*, data-*, role, and other non-IDL attributes inside the "attributes" object: { "attributes": { "aria-label": "..." } } |
679
+ | "must match pattern" on tagName | A custom element tag name doesn't contain a hyphen. | Add a hyphen: "newsletter-form" not "newsletter". Standard HTML elements use their exact name ("div", "p", "input"). |
680
+ | "must be string" | A value is an unquoted number, boolean, or bare word. | Wrap the value in quotes: "10px" not 10px. All CSS values and text must be strings. |
681
+ | "must be number" / "must be integer" | A numeric field (like index, tabIndex) is wrapped in quotes. | Remove the quotes: use 0 not "0". |
682
+ | "must have required property" | A required field is missing from the node. | Add the missing property. Every element must have "tagName". |
683
+ | "must be object" | A field that expects an object (like style or attributes) received a string or other type. | Use {} not a string. |
684
+ | "No node exists at path" | The path you provided doesn't point to an existing node. | Call read_document first to see the current structure and valid paths, then use the correct path. |
685
+
686
+ ### If you keep getting errors:
687
+ - Call read_document again — the document may have changed since you last read it.
688
+ - Remove the problematic node entirely with delete_node, then re-create it correctly with add_child.
689
+ - If the error message points to a different path than you expected, the node might have moved due to previous edits.`;
690
+
691
+ /** What every section of the prompt is joined with. */
692
+ const SECTION_SEPARATOR = "\n\n---\n\n";
693
+
694
+ /**
695
+ * The part of every system prompt that does not depend on the state, byte for byte: the role, the
696
+ * Jx reference sections, and the error-recovery guidance, each followed by the section separator.
697
+ *
698
+ * It exists for providers' prompt caches. A cache matches on a PREFIX of the request, and the
699
+ * system prompt is the request's first message, so the reference material (most of the prompt's
700
+ * tokens) only hits when nothing that changes between rounds comes before it. The prompt used to
701
+ * open with the role, the workflow for the current state and its tool list, so opening a document
702
+ * or registering one more command tool moved the first changed byte to the second paragraph and
703
+ * every request after it paid for the whole reference again. Everything dynamic — the mode, the
704
+ * tool list, the document, project and file summaries — now follows this prefix, and the closing
705
+ * instructions stay last. {@link buildSystemPrompt} always starts with it, and its tests hold that
706
+ * for every combination of the state flags.
707
+ */
708
+ export const SYSTEM_PROMPT_STATIC_PREFIX =
709
+ [
710
+ // 1. Role
711
+ ROLE,
712
+ // 2. Jx schema reference — kept in ALL modes (pre-project the model plans starter content)
713
+ JX_SCHEMA_REFERENCE,
714
+ // 2a. State shape decision tree
715
+ STATE_SHAPE_DECISION_TREE,
716
+ // 2b. Real-world patterns
717
+ REAL_WORLD_PATTERNS,
718
+ // 2c. Design principles — spacing, type, color, layout, restraint
719
+ DESIGN_PRINCIPLES,
720
+ // 2d. Control flow & reactivity — signals, list rendering ($map), conditionals ($switch)
721
+ CONTROL_FLOW_PATTERNS,
722
+ // 2e. Multi-page site building — layouts, file-based routing, navigation
723
+ MULTI_PAGE_PATTERNS,
724
+ // 3. Error recovery guidance
725
+ ERROR_RECOVERY,
726
+ ].join(SECTION_SEPARATOR) + SECTION_SEPARATOR;
727
+
659
728
  /**
660
729
  * Build a dynamic system prompt for the AI assistant.
661
730
  *
@@ -681,7 +750,7 @@ export function buildSystemPrompt({
681
750
  }: BuildSystemPromptOptions = {}) {
682
751
  const hasDocument = Boolean(document);
683
752
 
684
- // 1. Role, state-appropriate workflow, and the tool list for the current state.
753
+ // The state-appropriate workflow and the tool list for the current state.
685
754
  // The list the model is TOLD about and the list the gate will honour are the same filter, so a
686
755
  // Refusal is never a surprise to it. The command-projected tools arrive already filtered by
687
756
  // Their records' own gates, for the same reason — one function answers both the prompt and the
@@ -695,8 +764,6 @@ export function buildSystemPrompt({
695
764
  .map((blurb) => `- ${blurb}`)
696
765
  .join("\n");
697
766
 
698
- const role = `You are an expert Jx builder assistant embedded in Jx Studio. You help users build websites, components, pages, and layouts using the Jx JSON schema. The live jxsuite.com marketing site is built entirely with Jx — you can produce production-quality Jx code.`;
699
-
700
767
  let workflow: string;
701
768
  if (!hasProject && !hasDocument) {
702
769
  workflow = `No project is open yet. Your first job is to bootstrap one:
@@ -734,23 +801,10 @@ ${ASKING_THE_USER}
734
801
 
735
802
  Be concise. Don't explain what Jx is unless asked. Just build.`;
736
803
 
737
- const sections = [`${role}\n\n${workflow}\n\n${closing}`];
738
-
739
- // eslint-disable-next-line unicorn/no-immediate-mutation -- conditional section builder: later sections are pushed only when their context exists
740
- sections.push(
741
- // 2. Jx schema reference — kept in ALL modes (pre-project the model plans starter content)
742
- JX_SCHEMA_REFERENCE,
743
- // 3. State shape decision tree
744
- STATE_SHAPE_DECISION_TREE,
745
- // 4. Real-world patterns
746
- REAL_WORLD_PATTERNS,
747
- // 4a. Design principles — spacing, type, color, layout, restraint
748
- DESIGN_PRINCIPLES,
749
- // 4b. Control flow & reactivity — signals, list rendering ($map), conditionals ($switch)
750
- CONTROL_FLOW_PATTERNS,
751
- // 4c. Multi-page site building — layouts, file-based routing, navigation
752
- MULTI_PAGE_PATTERNS,
753
- );
804
+ /* 4. The mode. Everything from here on is a function of what is open right now, so it all follows
805
+ SYSTEM_PROMPT_STATIC_PREFIX: a provider's prompt cache matches on a prefix, and the first byte
806
+ that differs between two requests ends the match. */
807
+ const sections = [`## Current Mode\n\n${workflow}`];
754
808
 
755
809
  // 5. Current document context
756
810
  if (document) {
@@ -758,7 +812,7 @@ Be concise. Don't explain what Jx is unless asked. Just build.`;
758
812
  sections.push(`## Current Document\n\n${summary}`);
759
813
  }
760
814
 
761
- // 6. Project context
815
+ // 5a. Project context
762
816
  if (hasProject && (projectConfig || components || projectRoot)) {
763
817
  const projectSummary = buildProjectSummary({
764
818
  components,
@@ -771,7 +825,7 @@ Be concise. Don't explain what Jx is unless asked. Just build.`;
771
825
  }
772
826
  }
773
827
 
774
- // 6a. File inventory — a compact map of the project for cross-file work
828
+ // 5b. File inventory — a compact map of the project for cross-file work
775
829
  if (hasProject && fileInventory && fileInventory.length > 0) {
776
830
  const capped = fileInventory.slice(0, FILE_INVENTORY_CAP);
777
831
  const more =
@@ -781,34 +835,10 @@ Be concise. Don't explain what Jx is unless asked. Just build.`;
781
835
  sections.push(`## Project Files\n\n${capped.join("\n")}${more}`);
782
836
  }
783
837
 
784
- // 7. Error recovery guidance
785
- sections.push(`## Error Recovery
786
-
787
- If a tool call fails (returns { success: false }):
788
- 1. Read the error message carefully — it includes a "→ Fix:" hint telling you exactly how to correct the error.
789
- 2. Each error points to a specific path in the document and a specific rule violation.
790
- 3. Apply the suggested fix using set_property, delete_node, or add_child as appropriate.
791
- 4. Do NOT re-issue the exact same tool call with the same arguments — you must CHANGE something.
792
- 5. If you see the SAME error after 2 attempts, try a completely different approach (e.g., remove and re-add the node instead of patching it).
793
-
794
- ### Common validation errors and their fixes:
795
-
796
- | Error pattern | What happened | How to fix |
797
- |---|---|---|
798
- | "must NOT have additional property" in style | You used a non-camelCase CSS property (e.g. "background-color") or put an HTML attribute directly on the element. | Use camelCase: "backgroundColor". Put aria-*, data-*, role, and other non-IDL attributes inside the "attributes" object: { "attributes": { "aria-label": "..." } } |
799
- | "must match pattern" on tagName | A custom element tag name doesn't contain a hyphen. | Add a hyphen: "newsletter-form" not "newsletter". Standard HTML elements use their exact name ("div", "p", "input"). |
800
- | "must be string" | A value is an unquoted number, boolean, or bare word. | Wrap the value in quotes: "10px" not 10px. All CSS values and text must be strings. |
801
- | "must be number" / "must be integer" | A numeric field (like index, tabIndex) is wrapped in quotes. | Remove the quotes: use 0 not "0". |
802
- | "must have required property" | A required field is missing from the node. | Add the missing property. Every element must have "tagName". |
803
- | "must be object" | A field that expects an object (like style or attributes) received a string or other type. | Use {} not a string. |
804
- | "No node exists at path" | The path you provided doesn't point to an existing node. | Call read_document first to see the current structure and valid paths, then use the correct path. |
805
-
806
- ### If you keep getting errors:
807
- - Call read_document again — the document may have changed since you last read it.
808
- - Remove the problematic node entirely with delete_node, then re-create it correctly with add_child.
809
- - If the error message points to a different path than you expected, the node might have moved due to previous edits.`);
838
+ // 6. Closing — last, where a long prompt's final instructions carry the most weight.
839
+ sections.push(closing);
810
840
 
811
- return sections.join("\n\n---\n\n");
841
+ return SYSTEM_PROMPT_STATIC_PREFIX + sections.join(SECTION_SEPARATOR);
812
842
  }
813
843
 
814
844
  // ─── Helpers ─────────────────────────────────────────────────────────────────
@@ -30,7 +30,9 @@ import { clearCfConnection, getCfAccountId, getCfToken } from "../services/cf-se
30
30
  import { clearAiProvider, getBaseUrl, hasOpenAiKey } from "../services/ai-settings";
31
31
  import { preferredModel, resetModelCache } from "../services/ai-models";
32
32
  import { getPlatform, hasPlatform } from "../platform";
33
- import type { CfConnection } from "../types";
33
+ import { getAccountEntries } from "../account-status";
34
+ import { runOfferedAction } from "../account/action-flow";
35
+ import type { AccountEntry, CfConnection, OfferedAction } from "../types";
34
36
 
35
37
  /** One thing a row can do besides existing. */
36
38
  export interface AccountAction {
@@ -228,11 +230,47 @@ function localCloudflareRecord(): AccountRecord {
228
230
  };
229
231
  }
230
232
 
233
+ // ─── The platform's own rows ─────────────────────────────────────────────────
234
+
235
+ /**
236
+ * One row the platform adds (desktop.md §10.4) — a membership, a role, a plan; Studio does not know
237
+ * which, and draws it in the platform's words.
238
+ *
239
+ * Its id is namespaced so no platform can shadow one of Studio's own rows by naming it `github`.
240
+ * `revoke` does nothing: whatever the row describes is not a credential this app holds, so there is
241
+ * nothing here to forget, and its verbs are the actions the platform offers with it.
242
+ */
243
+ function platformAction(action: OfferedAction): AccountAction {
244
+ const verb: AccountAction = {
245
+ id: action.id,
246
+ label: action.label,
247
+ run: async () => {
248
+ await runOfferedAction(action);
249
+ },
250
+ };
251
+ if (action.primary) {
252
+ verb.variant = "accent";
253
+ }
254
+ return verb;
255
+ }
256
+
257
+ function platformRecord(entry: AccountEntry): AccountRecord {
258
+ return {
259
+ actions: (entry.actions ?? []).map((action) => platformAction(action)),
260
+ connected: entry.connected ?? false,
261
+ detail: entry.detail,
262
+ id: `platform:${entry.id}`,
263
+ label: entry.label,
264
+ revoke: () => {},
265
+ };
266
+ }
267
+
231
268
  /**
232
- * The three accounts, always all three.
269
+ * The three accounts, always all three — then any rows the platform adds.
233
270
  *
234
271
  * A disconnected account still gets a row: "you are not signed in to GitHub" is information, and a
235
- * list that hides what is absent cannot answer the question the section exists to answer.
272
+ * list that hides what is absent cannot answer the question the section exists to answer. The
273
+ * platform's rows are the platform's to include or leave out; Studio adds none of its own.
236
274
  */
237
275
  export function listAccounts(): AccountRecord[] {
238
276
  const githubToken = githubTokenStored();
@@ -260,6 +298,7 @@ export function listAccounts(): AccountRecord[] {
260
298
  revoke: clearAiProvider,
261
299
  },
262
300
  platformBrokersCf() ? brokeredCloudflareRecord() : localCloudflareRecord(),
301
+ ...getAccountEntries().map((entry) => platformRecord(entry)),
263
302
  ];
264
303
  }
265
304
 
@@ -19,7 +19,7 @@
19
19
  * this, `clearGithubToken()` had zero callers: signing out of GitHub was not expressible.
20
20
  * - **Keyboard** — GENERATED from `commands/reference.ts`'s `shortcutReference()`, the same
21
21
  * projection `docs/studio/interface/shortcuts.md` is built from. The registry is the one place a
22
- * chord is declared, so the sheet cannot drift from the app or from the docs. Per CLAUDE.md's
22
+ * chord is declared, so the sheet cannot drift from the app or from the docs. Per AGENTS.md's
23
23
  * Screenshot Policy there is deliberately no screenshot of it — photographing generated content
24
24
  * is a bug. It is searchable two ways, because there are two questions ("what is the shortcut for
25
25
  * X" and "what did I just press"), and rebindable — as a LAYER over the registry
@@ -44,6 +44,8 @@
44
44
 
45
45
  import { CHROME_THEMES, setChromeTheme, shell } from "../shell";
46
46
  import { layerHost } from "../ui/layers";
47
+ import { hydrateAccountStatus } from "../account-status";
48
+ import { getPlatform, hasPlatform } from "../platform";
47
49
  import { createAiCredentialsForm } from "../ui/ai-credentials-form";
48
50
  import { createManagedConnect } from "../ui/ai-managed-connect";
49
51
  import { activeRegistry } from "../commands/active-registry";
@@ -563,6 +565,11 @@ export function openPreferences(section?: string): Promise<null> {
563
565
  grant can lapse between visits, and a row that repaints the previous answer first is stating
564
566
  something it has no reason to believe. */
565
567
  resetCfConnectionCache();
568
+ /* And the platform's own account rows, for the same reason: whatever they report can change
569
+ between visits. They repaint when the answer lands; until then they show the last one. */
570
+ if (hasPlatform() && getPlatform().getAccountStatus) {
571
+ void hydrateAccountStatus().then(repaint);
572
+ }
566
573
  _section = section !== undefined && isPreferencesSection(section) ? section : _section;
567
574
  if (_surface) {
568
575
  repaint();
package/src/studio.ts CHANGED
@@ -126,8 +126,10 @@ import {
126
126
  openLastSessionOrHome,
127
127
  registerFileTreeDnD,
128
128
  reloadCleanTab,
129
+ reloadTabsIfChanged,
129
130
  } from "./files/files";
130
131
  import { resetIgnoreCache } from "./files/gitignore";
132
+ import { resetAssetVersions } from "./files/asset-versions";
131
133
  import { startFsSync } from "./files/fs-events";
132
134
  import { invalidateParamValues } from "./page-params";
133
135
  import {
@@ -221,6 +223,8 @@ import {
221
223
  import { initCssData } from "./panels/style-utils";
222
224
  import { initQuickSearch } from "./panels/quick-search";
223
225
  import { hydrateAccountStatus } from "./account-status";
226
+ import { showToastNotices, wireActionReports } from "./account/action-flow";
227
+ import { isActionRequired } from "./platform-errors";
224
228
  import { hydrateProjectList } from "./project-list";
225
229
  import { addRecentProject, hydrateRecentProjects, removeRecentProject } from "./recent-projects";
226
230
  import {
@@ -507,6 +511,9 @@ toolbarPanel.mount(toolbarEl, {
507
511
  });
508
512
 
509
513
  initLayers();
514
+ /* Before anything reaches the backend: an `action-required` refusal (desktop.md §10.4) can be the
515
+ answer to the very first request, and the dialog that offers its actions needs the layers above. */
516
+ wireActionReports();
510
517
  initQuickSearch({ openRecentProject: (root: string) => openRecentProject(root) });
511
518
 
512
519
  /* The pane's four surfaces come from its CELL, not from `document.querySelector`.
@@ -959,6 +966,9 @@ function ensureFsSync() {
959
966
  paneContext.resetParamValues();
960
967
  },
961
968
  onContentChange: reloadCleanTab,
969
+ /* A reconnect re-reads the clean, non-collab tabs, and leaves every one whose content did not
970
+ change untouched: most reconnects changed nothing, and a reload is a repaint per tab. */
971
+ onResyncContent: reloadTabsIfChanged,
962
972
  renderLeftPanel,
963
973
  });
964
974
  }
@@ -979,7 +989,10 @@ if (!_projectParam) {
979
989
  // No project is bound yet, so this call only warms the canvasUrl; a backend that refuses it
980
990
  // Still gets the render below rather than an unhandled rejection.
981
991
  ?.catch((error: unknown) => {
982
- console.error("Boot activation failed:", error);
992
+ // A refusal that offers an action is the action flow's to explain; it is not a fault.
993
+ if (!isActionRequired(error)) {
994
+ console.error("Boot activation failed:", error);
995
+ }
983
996
  })
984
997
  .then(() => {
985
998
  render();
@@ -1022,6 +1035,7 @@ if (_projectParam) {
1022
1035
  }
1023
1036
 
1024
1037
  resetIgnoreCache();
1038
+ resetAssetVersions();
1025
1039
  setProjectState({
1026
1040
  dirs: new Map(),
1027
1041
  expanded: new Set(),
@@ -1188,6 +1202,8 @@ void hydrateProjectList().then(() => {
1188
1202
  // The welcome screen's install prompt. No-op on platforms without getAccountStatus.
1189
1203
  // oxlint-disable-next-line unicorn/prefer-top-level-await -- deliberate fire-and-forget: hydration must not block initial render
1190
1204
  void hydrateAccountStatus().then(() => {
1205
+ // After the status: the platform's one-time notices ride on it (desktop.md §10.4).
1206
+ showToastNotices();
1191
1207
  render();
1192
1208
  });
1193
1209
 
@@ -1355,6 +1371,7 @@ async function openRecentProject(root: string) {
1355
1371
 
1356
1372
  closeAllTabs();
1357
1373
  resetIgnoreCache();
1374
+ resetAssetVersions();
1358
1375
 
1359
1376
  setProjectState({
1360
1377
  ...projectState,
@@ -96,6 +96,8 @@ export interface DialogSurfacePatch {
96
96
  */
97
97
  confirmLabel?: string;
98
98
  cancelLabel?: string;
99
+ /** The secondary button's text; the empty string takes the button away. */
100
+ secondaryLabel?: string;
99
101
  /** The sentence under the headline. Cleared by the empty string. */
100
102
  message?: string;
101
103
  value?: string;
@@ -298,6 +300,9 @@ export function openDialogSurface(options: DialogSurfaceOptions): DialogSurfaceH
298
300
  if (patch.cancelLabel !== undefined) {
299
301
  scope.cancelLabel = patch.cancelLabel;
300
302
  }
303
+ if (patch.secondaryLabel !== undefined) {
304
+ scope.secondaryLabel = patch.secondaryLabel;
305
+ }
301
306
  if (patch.message !== undefined) {
302
307
  scope.message = patch.message;
303
308
  scope.hasMessage = patch.message !== "";
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://jxsuite.com/schema/v1",
3
3
  "$id": "StudioWelcome",
4
- "$description": "The Start pane, shown when no project is loaded and no document is open. The adapter hands in the start actions, whether the GitHub App still needs installing, the recent projects with their distinguishing folders and last-opened labels, and the platform's catalogue; the document draws them in the order a first run needs them.",
4
+ "$description": "The Start pane, shown when no project is loaded and no document is open. The adapter hands in the start actions, whether the GitHub App still needs installing, the platform's own notices with the actions each offers, the recent projects with their distinguishing folders and last-opened labels, and the platform's catalogue; the document draws them in the order a first run needs them.",
5
5
  "tagName": "div",
6
6
  "attributes": {
7
7
  "part": "screen"
@@ -311,6 +311,91 @@
311
311
  }
312
312
  }
313
313
  },
314
+ {
315
+ "$prototype": "Array",
316
+ "items": {
317
+ "$ref": "#/state/notices"
318
+ },
319
+ "key": {
320
+ "$ref": "$map/item/id"
321
+ },
322
+ "map": {
323
+ "tagName": "section",
324
+ "attributes": {
325
+ "part": "section",
326
+ "data-section": "notice",
327
+ "data-notice": "${$map.item.id}",
328
+ "data-level": "${$map.item.level}"
329
+ },
330
+ "children": [
331
+ {
332
+ "tagName": "h2",
333
+ "attributes": {
334
+ "part": "section-title"
335
+ },
336
+ "textContent": "${$map.item.title}"
337
+ },
338
+ {
339
+ "tagName": "p",
340
+ "attributes": {
341
+ "part": "note"
342
+ },
343
+ "textContent": "${$map.item.message}"
344
+ },
345
+ {
346
+ "tagName": "div",
347
+ "attributes": {
348
+ "part": "actions"
349
+ },
350
+ "children": [
351
+ {
352
+ "$prototype": "Array",
353
+ "items": {
354
+ "$ref": "$map/item/actions"
355
+ },
356
+ "key": {
357
+ "$ref": "$map/item/key"
358
+ },
359
+ "map": {
360
+ "tagName": "jx-button",
361
+ "$props": {
362
+ "variant": {
363
+ "$ref": "$map/item/variant"
364
+ }
365
+ },
366
+ "attributes": {
367
+ "part": "notice-action",
368
+ "data-action": "${$map.item.key}"
369
+ },
370
+ "onclick": {
371
+ "$prototype": "Function",
372
+ "body": [
373
+ {
374
+ "operator": "call",
375
+ "target": {
376
+ "$ref": "#/state/noticeRun"
377
+ },
378
+ "value": [
379
+ {
380
+ "$ref": "$map/item/key"
381
+ }
382
+ ]
383
+ }
384
+ ]
385
+ },
386
+ "children": [
387
+ {
388
+ "tagName": "span",
389
+ "textContent": "${$map.item.label}"
390
+ }
391
+ ]
392
+ }
393
+ }
394
+ ]
395
+ }
396
+ ]
397
+ }
398
+ },
314
399
  {
315
400
  "tagName": "div",
316
401
  "attributes": {