@jxsuite/studio 2.3.0 → 2.4.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 (39) hide show
  1. package/dist/chunks/{content-source-56xpvx83.js → content-source-jrvn5vwy.js} +8 -8
  2. package/dist/chunks/{convert-to-repeater-0xh9wwek.js → convert-to-repeater-109dwcj7.js} +7 -7
  3. package/dist/chunks/{extension-sections-g34239vm.js → extension-sections-2zjyty05.js} +11 -11
  4. package/dist/chunks/{files-gkz202dp.js → files-jdbtammm.js} +12 -12
  5. package/dist/chunks/{format-host-pej3srnw.js → format-host-kq6963gs.js} +2 -2
  6. package/dist/chunks/{git-panel-4j6xbh8g.js → git-panel-2stf3ahg.js} +7 -7
  7. package/dist/chunks/{layers-panel-8mvn8sgd.js → layers-panel-a8wjsdjj.js} +10 -10
  8. package/dist/chunks/{publish-panel-ptddc3kg.js → publish-panel-2we6smjv.js} +5 -5
  9. package/dist/chunks/{quick-search-j88azfs2.js → quick-search-hmpwdgys.js} +13 -13
  10. package/dist/chunks/{right-panel-egkaxy89.js → right-panel-hn3y3j0j.js} +9 -9
  11. package/dist/chunks/{studio-0e4h32g6.js → studio-0evba9z5.js} +5 -5
  12. package/dist/chunks/{studio-4058pg09.js → studio-102jzb8r.js} +4 -4
  13. package/dist/chunks/{studio-ncnet0ba.js → studio-1dgqagzz.js} +10 -10
  14. package/dist/chunks/{studio-yxzgjq8q.js → studio-377bbhtz.js} +8 -8
  15. package/dist/chunks/{studio-5ne8mdx9.js → studio-53dn6e6t.js} +2 -2
  16. package/dist/chunks/{studio-50eed4c9.js → studio-6gcrk1bq.js} +2 -2
  17. package/dist/chunks/{studio-5amdrjm4.js → studio-8x4399kc.js} +7 -7
  18. package/dist/chunks/{studio-hk523hby.js → studio-ddgrc5hs.js} +3 -3
  19. package/dist/chunks/{studio-3q65xvzv.js → studio-eqyybj2e.js} +4 -4
  20. package/dist/chunks/{studio-cf4rk7kv.js → studio-kwavhmdr.js} +2 -2
  21. package/dist/chunks/{studio-zdkgwhkv.js → studio-ky68mp85.js} +3 -3
  22. package/dist/chunks/{studio-hs8x23fv.js → studio-ng5gbxkj.js} +8 -8
  23. package/dist/chunks/{studio-4enps87v.js → studio-q07vj7p9.js} +6 -6
  24. package/dist/chunks/{studio-bak2cyjr.js → studio-qd4k0h80.js} +4 -4
  25. package/dist/chunks/{studio-cv606rs1.js → studio-rdsvb808.js} +5 -5
  26. package/dist/chunks/{studio-vdpcpada.js → studio-tqr02xaa.js} +3 -3
  27. package/dist/chunks/{studio-x3etxs0s.js → studio-wxthr2x1.js} +8 -8
  28. package/dist/chunks/{studio-10s469da.js → studio-yk7037sg.js} +20 -10
  29. package/dist/chunks/{studio-a11d3a1b.js → studio-yz06mkfj.js} +5 -5
  30. package/dist/iframe-entry.js +7 -3
  31. package/dist/studio.js +46 -34
  32. package/package.json +6 -6
  33. package/src/canvas/canvas-live-render.ts +2 -4
  34. package/src/canvas/canvas-origin.ts +25 -0
  35. package/src/platforms/cloud.ts +9 -0
  36. package/src/services/ai-models.ts +14 -1
  37. package/src/types.ts +17 -0
  38. package/src/ui/ai-managed-connect.ts +18 -3
  39. package/styles/shell.css +7 -0
package/dist/studio.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  seedPublishConnected
3
- } from "./chunks/studio-vdpcpada.js";
3
+ } from "./chunks/studio-tqr02xaa.js";
4
4
  import"./chunks/studio-wdt9y92a.js";
5
5
  import {
6
6
  ElementResolutionController,
@@ -67,7 +67,7 @@ import {
67
67
  registerStyleCommands,
68
68
  render as render3,
69
69
  setInspectorTab
70
- } from "./chunks/studio-5amdrjm4.js";
70
+ } from "./chunks/studio-8x4399kc.js";
71
71
  import {
72
72
  CommandUnavailableError,
73
73
  chordFromEvent,
@@ -99,7 +99,7 @@ import {
99
99
  showSlashMenu,
100
100
  showSlashMenuAtRect,
101
101
  suppressBlockActionBar
102
- } from "./chunks/studio-yxzgjq8q.js";
102
+ } from "./chunks/studio-377bbhtz.js";
103
103
  import {
104
104
  addableContexts,
105
105
  defaultCommands,
@@ -111,7 +111,7 @@ import {
111
111
  tokenLabel,
112
112
  transposeStylebookStyle,
113
113
  writeTokenOverride
114
- } from "./chunks/studio-a11d3a1b.js";
114
+ } from "./chunks/studio-yz06mkfj.js";
115
115
  import {
116
116
  DERIVE_PRESETS,
117
117
  JxColorPopover,
@@ -144,7 +144,7 @@ import {
144
144
  spNumberField,
145
145
  spTextArea,
146
146
  spTextField
147
- } from "./chunks/studio-hs8x23fv.js";
147
+ } from "./chunks/studio-ng5gbxkj.js";
148
148
  import {
149
149
  LitElement,
150
150
  css
@@ -160,7 +160,7 @@ import {
160
160
  import {
161
161
  initQuickSearch,
162
162
  openQuickSearch
163
- } from "./chunks/studio-bak2cyjr.js";
163
+ } from "./chunks/studio-qd4k0h80.js";
164
164
  import {
165
165
  addRecentProject,
166
166
  clearRecentProjects,
@@ -187,10 +187,10 @@ import {
187
187
  removeRecentProject,
188
188
  renderFilesTemplate,
189
189
  seedEntry
190
- } from "./chunks/studio-ncnet0ba.js";
190
+ } from "./chunks/studio-1dgqagzz.js";
191
191
  import {
192
192
  computeWindow
193
- } from "./chunks/studio-cf4rk7kv.js";
193
+ } from "./chunks/studio-kwavhmdr.js";
194
194
  import {
195
195
  autoSyncProjectOnOpen,
196
196
  cleanupGitPanel,
@@ -211,13 +211,13 @@ import {
211
211
  registerSourceControlCommands,
212
212
  renderGitPanel,
213
213
  stripRange
214
- } from "./chunks/studio-4enps87v.js";
214
+ } from "./chunks/studio-q07vj7p9.js";
215
215
  import {
216
216
  createPanelScheduler
217
217
  } from "./chunks/studio-8jpzqk5s.js";
218
218
  import {
219
219
  selectContributedEntry
220
- } from "./chunks/studio-cv606rs1.js";
220
+ } from "./chunks/studio-rdsvb808.js";
221
221
  import {
222
222
  notifySettingsDocument,
223
223
  onSettingsDocumentChanged,
@@ -238,7 +238,7 @@ import {
238
238
  renderForm,
239
239
  renderProvenanceChip,
240
240
  resolveContextPointer
241
- } from "./chunks/studio-zdkgwhkv.js";
241
+ } from "./chunks/studio-ky68mp85.js";
242
242
  import {
243
243
  validateDoc,
244
244
  validateProjectConfig
@@ -263,7 +263,7 @@ import {
263
263
  registerGridCommands,
264
264
  scanLibrary,
265
265
  uploadDirForCategory
266
- } from "./chunks/studio-0e4h32g6.js";
266
+ } from "./chunks/studio-0evba9z5.js";
267
267
  import {
268
268
  live
269
269
  } from "./chunks/studio-c31yb4t6.js";
@@ -307,7 +307,7 @@ import {
307
307
  setIncludeDrafts,
308
308
  startFsSync,
309
309
  writesForTurn
310
- } from "./chunks/studio-x3etxs0s.js";
310
+ } from "./chunks/studio-wxthr2x1.js";
311
311
  import"./chunks/studio-2w11grav.js";
312
312
  import"./chunks/studio-bw5dz6km.js";
313
313
  import {
@@ -316,18 +316,18 @@ import {
316
316
  isImage,
317
317
  setMediaChangedHandler,
318
318
  uploadAssets
319
- } from "./chunks/studio-4058pg09.js";
319
+ } from "./chunks/studio-102jzb8r.js";
320
320
  import {
321
321
  beginActivity,
322
322
  registerActivityPanel,
323
323
  showProgressModal
324
- } from "./chunks/studio-3q65xvzv.js";
324
+ } from "./chunks/studio-eqyybj2e.js";
325
325
  import {
326
326
  currentDeploy,
327
327
  latestDeployment,
328
328
  noteDeployment,
329
329
  platformSupportsPublish
330
- } from "./chunks/studio-5ne8mdx9.js";
330
+ } from "./chunks/studio-53dn6e6t.js";
331
331
  import {
332
332
  CATEGORIES,
333
333
  SPAN_FULL_RENDER,
@@ -439,7 +439,7 @@ import {
439
439
  timeSpan,
440
440
  timeSpanAsync,
441
441
  updateActivePanelHeaders
442
- } from "./chunks/studio-10s469da.js";
442
+ } from "./chunks/studio-yk7037sg.js";
443
443
  import {
444
444
  BOTTOM_TAB_IDS,
445
445
  BUFFER_COMMIT,
@@ -606,7 +606,7 @@ import {
606
606
  updateSiteConfig,
607
607
  view,
608
608
  workspace
609
- } from "./chunks/studio-hk523hby.js";
609
+ } from "./chunks/studio-ddgrc5hs.js";
610
610
  import {
611
611
  classMap,
612
612
  ifDefined,
@@ -669,7 +669,7 @@ import {
669
669
  mountStillWanted,
670
670
  refreshExtensionUi,
671
671
  refreshFormats
672
- } from "./chunks/studio-50eed4c9.js";
672
+ } from "./chunks/studio-6gcrk1bq.js";
673
673
  import {
674
674
  getPlatform,
675
675
  hasPlatform,
@@ -16807,7 +16807,7 @@ function entryIdsFor(collection) {
16807
16807
  if (known) {
16808
16808
  return known;
16809
16809
  }
16810
- const pending = import("./chunks/content-source-56xpvx83.js").then((m) => m.listCollectionEntryIds(collection));
16810
+ const pending = import("./chunks/content-source-jrvn5vwy.js").then((m) => m.listCollectionEntryIds(collection));
16811
16811
  entryIdCache.set(collection, pending);
16812
16812
  return pending;
16813
16813
  }
@@ -17068,7 +17068,7 @@ function renderEntryMode(surface, tab) {
17068
17068
  });
17069
17069
  }
17070
17070
  async function openEntryEditor(path) {
17071
- const { openFileInTab: openFileInTab2 } = await import("./chunks/files-gkz202dp.js");
17071
+ const { openFileInTab: openFileInTab2 } = await import("./chunks/files-jdbtammm.js");
17072
17072
  await openFileInTab2(path);
17073
17073
  for (const [id, tab] of workspace.tabs.entries()) {
17074
17074
  if (tab.documentPath !== path) {
@@ -17129,7 +17129,7 @@ async function createEntry(collectionName) {
17129
17129
  });
17130
17130
  return null;
17131
17131
  }
17132
- const { createFileIn: createFileIn2 } = await import("./chunks/files-gkz202dp.js");
17132
+ const { createFileIn: createFileIn2 } = await import("./chunks/files-jdbtammm.js");
17133
17133
  const created = await createFileIn2({
17134
17134
  content,
17135
17135
  dir: collection.dir,
@@ -22349,7 +22349,7 @@ function groupProblems(records = problems) {
22349
22349
  return groups;
22350
22350
  }
22351
22351
  function openProblemPath(path) {
22352
- import("./chunks/files-gkz202dp.js").then((m) => m.openFileInTab(path));
22352
+ import("./chunks/files-jdbtammm.js").then((m) => m.openFileInTab(path));
22353
22353
  }
22354
22354
  function actionTpl(record) {
22355
22355
  const id = record.action;
@@ -27264,6 +27264,7 @@ var cache2 = null;
27264
27264
  var proxyConfigured = false;
27265
27265
  var proxyManaged = false;
27266
27266
  var proxyDefaultModel = "";
27267
+ var proxyCode;
27267
27268
  var proxyProbe = null;
27268
27269
  var probeListeners = new Set;
27269
27270
  function invalidateModelCache() {
@@ -27271,6 +27272,7 @@ function invalidateModelCache() {
27271
27272
  proxyConfigured = false;
27272
27273
  proxyManaged = false;
27273
27274
  proxyDefaultModel = "";
27275
+ proxyCode = undefined;
27274
27276
  proxyProbe = null;
27275
27277
  }
27276
27278
  function ensureProxyProbe(onSettled) {
@@ -27292,6 +27294,9 @@ function isProxyConfigured() {
27292
27294
  function isManagedProxy() {
27293
27295
  return proxyManaged;
27294
27296
  }
27297
+ function proxyStateCode() {
27298
+ return proxyCode;
27299
+ }
27295
27300
  function siblingRoute(chatUrl, segment) {
27296
27301
  let url;
27297
27302
  try {
@@ -27326,6 +27331,7 @@ async function fetchAvailableModels(opts = {}) {
27326
27331
  proxyConfigured = data.configured === true;
27327
27332
  proxyManaged = data.managed === true;
27328
27333
  proxyDefaultModel = data.defaultModel ?? "";
27334
+ proxyCode = data.code;
27329
27335
  cache2 = (data.models || []).map((m) => ({ id: m.id, name: m.name || m.id }));
27330
27336
  return cache2;
27331
27337
  }
@@ -27490,17 +27496,22 @@ function createManagedConnect(opts) {
27490
27496
  if (!canOffer()) {
27491
27497
  return nothing;
27492
27498
  }
27499
+ const lapsed = proxyStateCode() === "cf_reconnect_required";
27500
+ const busyLabel = lapsed ? "Reconnecting…" : "Connecting…";
27493
27501
  return html`
27494
- <div class="ai-managed-connect">
27495
- <div>Use Workers AI on your own Cloudflare account — no API key needed.</div>
27502
+ <div class="ai-managed-connect" data-jx-recommended="cloudflare">
27503
+ <div class="ai-managed-connect-lede">
27504
+ ${lapsed ? "Your Cloudflare connection has expired. Reconnect to keep using the assistant." : "Recommended — run the assistant on Workers AI in your own Cloudflare account. No API key to create, copy or rotate."}
27505
+ </div>
27496
27506
  <sp-button
27497
27507
  size="s"
27508
+ variant="accent"
27498
27509
  ?disabled=${busy}
27499
27510
  @click=${() => {
27500
27511
  connect();
27501
27512
  }}
27502
27513
  >
27503
- ${busy ? "Connecting…" : "Connect Cloudflare"}
27514
+ ${busy ? busyLabel : lapsed ? "Reconnect Cloudflare" : "Connect Cloudflare"}
27504
27515
  </sp-button>
27505
27516
  ${connectError ? html`<div class="ai-managed-connect-error">${connectError}</div>` : nothing}
27506
27517
  <div class="ai-managed-connect-divider">— or bring your own key —</div>
@@ -28900,6 +28911,7 @@ function createCloudPlatform(project) {
28900
28911
  id: "cloud",
28901
28912
  projectRoot: root,
28902
28913
  canvasUrl: "/canvas.html",
28914
+ ...base ? { documentBaseUrl: `${base}/raw/` } : {},
28903
28915
  openProjectPicker: "repo-list",
28904
28916
  createDestination: "repo",
28905
28917
  async activate() {
@@ -58579,9 +58591,9 @@ function renderDefsEditor(container) {
58579
58591
 
58580
58592
  // src/version.ts
58581
58593
  var APP_NAME = "Jx Studio";
58582
- var VERSION = "2.3.0";
58583
- var BUILD_DATE = "2026-08-23T02:43:07.261Z";
58584
- var GIT_COMMIT = "968e4d4";
58594
+ var VERSION = "2.4.0";
58595
+ var BUILD_DATE = "2026-08-23T19:11:17.175Z";
58596
+ var GIT_COMMIT = "bea72f9";
58585
58597
  var LINKS = {
58586
58598
  github: "https://github.com/jxsuite/jx",
58587
58599
  docs: "https://github.com/jxsuite/jx#readme",
@@ -59658,7 +59670,7 @@ async function openProjectSettings(section) {
59658
59670
  showSettingsDocument();
59659
59671
  notifySettingsDocument();
59660
59672
  try {
59661
- const { syncExtensionSettingsSections } = await import("./chunks/extension-sections-g34239vm.js");
59673
+ const { syncExtensionSettingsSections } = await import("./chunks/extension-sections-2zjyty05.js");
59662
59674
  await syncExtensionSettingsSections();
59663
59675
  } catch (error) {
59664
59676
  notify.warn(`Could not load extension settings sections — ${errorMessage(error)}`, {
@@ -60282,7 +60294,7 @@ function publishCommands() {
60282
60294
  name: "set_up_publishing"
60283
60295
  },
60284
60296
  run: async () => {
60285
- const { openPublishPanel } = await import("./chunks/publish-panel-ptddc3kg.js");
60297
+ const { openPublishPanel } = await import("./chunks/publish-panel-2we6smjv.js");
60286
60298
  openPublishPanel();
60287
60299
  },
60288
60300
  title: "Set Up Publishing"
@@ -61004,7 +61016,7 @@ function i18nCommands() {
61004
61016
  });
61005
61017
  return;
61006
61018
  }
61007
- const { openFileInTab: openFileInTab2 } = await import("./chunks/files-gkz202dp.js");
61019
+ const { openFileInTab: openFileInTab2 } = await import("./chunks/files-jdbtammm.js");
61008
61020
  await openFileInTab2(path);
61009
61021
  },
61010
61022
  title: "Open Translation"
@@ -61051,7 +61063,7 @@ function i18nCommands() {
61051
61063
  return;
61052
61064
  }
61053
61065
  const cut = path.lastIndexOf("/");
61054
- const { createFileIn: createFileIn2, openFileInTab: openFileInTab2 } = await import("./chunks/files-gkz202dp.js");
61066
+ const { createFileIn: createFileIn2, openFileInTab: openFileInTab2 } = await import("./chunks/files-jdbtammm.js");
61055
61067
  const created = await createFileIn2({
61056
61068
  content,
61057
61069
  dir: cut === -1 ? "." : path.slice(0, cut),
@@ -61864,5 +61876,5 @@ export {
61864
61876
  hasUnsavedTabs
61865
61877
  };
61866
61878
 
61867
- //# debugId=7167B13925BF1BB364756E2164756E21
61879
+ //# debugId=5769FAD30A86F19564756E2164756E21
61868
61880
  //# sourceMappingURL=studio.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/studio",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "Jx Studio — visual builder for Jx documents",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -57,13 +57,13 @@
57
57
  "dependencies": {
58
58
  "@atlaskit/pragmatic-drag-and-drop": "^3.0.0",
59
59
  "@atlaskit/pragmatic-drag-and-drop-hitbox": "^2.1.0",
60
- "@jxsuite/ai": "^0.36.1",
60
+ "@jxsuite/ai": "^0.36.2",
61
61
  "@jxsuite/collab": "^0.8.1",
62
- "@jxsuite/create": "^1.3.3",
63
- "@jxsuite/formulas": "^0.0.9",
62
+ "@jxsuite/create": "^1.3.4",
63
+ "@jxsuite/formulas": "^0.0.10",
64
64
  "@jxsuite/markup": "^0.4.5",
65
- "@jxsuite/protocol": "^1.1.2",
66
- "@jxsuite/runtime": "^2.0.2",
65
+ "@jxsuite/protocol": "^1.2.0",
66
+ "@jxsuite/runtime": "^2.0.3",
67
67
  "@jxsuite/schema": "^1.8.0",
68
68
  "@spectrum-web-components/accordion": "^1.12.2",
69
69
  "@spectrum-web-components/action-bar": "1.12.2",
@@ -19,7 +19,7 @@ import {
19
19
  getEffectiveMedia,
20
20
  resolveLayoutDoc,
21
21
  } from "../site-context";
22
- import { canvasBaseOrigin } from "./canvas-origin";
22
+ import { documentBase } from "./canvas-origin";
23
23
  import { componentRegistry, computeRelativePath } from "../files/components";
24
24
  import { prepareForEditMode } from "../utils/edit-display";
25
25
  import {
@@ -179,10 +179,8 @@ export async function resolveCanvasDocument(
179
179
  }
180
180
  }
181
181
 
182
- const root = projectState?.projectRoot || "";
183
- const docPrefix = root ? `${root}/` : "";
184
182
  const docBase = S.documentPath
185
- ? `${canvasBaseOrigin()}/${docPrefix}${S.documentPath}`
183
+ ? new URL(S.documentPath, documentBase(projectState?.projectRoot)).href
186
184
  : undefined;
187
185
 
188
186
  // Substitute chosen dynamic route params ({$ref: "#/$params/x"} → literal), inject state.$page,
@@ -25,6 +25,31 @@ export function canvasBaseOrigin(): string {
25
25
  return location.origin;
26
26
  }
27
27
 
28
+ /**
29
+ * The base the canvas fetches PROJECT FILES against — component `$ref`s, `$src` modules, images.
30
+ *
31
+ * Two shapes, and the platform chooses. Most hosts serve the project tree from their web root, so
32
+ * `<canvas origin>/<projectRoot>/` is a real URL: the dev server serves by path (absolute
33
+ * filesystem paths included) and the desktop loopback does the same. Those set nothing.
34
+ *
35
+ * A host whose `projectRoot` is an IDENTIFIER rather than a served path sets `documentBaseUrl`. Jx
36
+ * Cloud is one — its root is `owner/repo@branch`, and the default produced a URL nothing answered,
37
+ * so every `$ref` fetch fell through to the SPA fallback. That fallback returns the marketing page
38
+ * at **HTTP 200**, so `res.ok` passed and the runtime's `res.json()` died on `Unexpected token
39
+ * '<'`; images failed the same way and reported nothing.
40
+ *
41
+ * @param {string | undefined} projectRoot - The active project root, for the default shape.
42
+ * @returns {string} A base URL ending in `/`, safe to pass to `new URL(relativePath, base)`.
43
+ */
44
+ export function documentBase(projectRoot?: string): string {
45
+ const declared = hasPlatform() ? getPlatform().documentBaseUrl : undefined;
46
+ if (declared) {
47
+ return declared.endsWith("/") ? declared : `${declared}/`;
48
+ }
49
+ const root = projectRoot || "";
50
+ return `${canvasBaseOrigin()}/${root ? `${root}/` : ""}`;
51
+ }
52
+
28
53
  /**
29
54
  * Build an absolute loopback-origin src for a PROJECT asset path referenced by a PARENT-realm
30
55
  * (shell) preview <img>. On the views:// shell a relative "/images/foo.png" resolves to
@@ -223,6 +223,15 @@ export function createCloudPlatform(project: CloudProject | null): StudioPlatfor
223
223
  id: "cloud",
224
224
  projectRoot: root,
225
225
  canvasUrl: "/canvas.html",
226
+ /* The projectRoot here is the IDENTIFIER "owner/repo@branch", not a served path, so the canvas's
227
+ default base — <origin>/<projectRoot>/ — addressed nothing. Every component $ref fetch landed
228
+ on the SPA fallback, which answers the marketing page at HTTP 200, so res.ok passed and the
229
+ runtime's res.json() died on "Unexpected token '<'". Images failed the same way, silently.
230
+ The session serves the project tree at /raw.
231
+
232
+ Conditional because `base` is "" until a project is bound, and "/raw/" would be a confident
233
+ wrong answer — worse than the default, which at least fails visibly. */
234
+ ...(base ? { documentBaseUrl: `${base}/raw/` } : {}),
226
235
  /* Open Project routes through Studio's repo picker (write-access repos via listRepos +
227
236
  importProject) — sessions are URL-bound, so openProject() below never opens a dialog. */
228
237
  openProjectPicker: "repo-list",
@@ -24,6 +24,7 @@ let cache: AiModel[] | null = null;
24
24
  let proxyConfigured = false;
25
25
  let proxyManaged = false;
26
26
  let proxyDefaultModel = "";
27
+ let proxyCode: AiModelsResponse["code"];
27
28
 
28
29
  /**
29
30
  * One-shot capability probe shared by every credentials gate, plus the hosts to repaint when it
@@ -39,7 +40,8 @@ export function invalidateModelCache() {
39
40
  proxyConfigured = false;
40
41
  proxyManaged = false;
41
42
  proxyDefaultModel = "";
42
- /* The probe's result IS the three flags above. Clearing them while keeping the settled promise
43
+ proxyCode = undefined;
44
+ /* The probe's result IS the flags above. Clearing them while keeping the settled promise
43
45
  would strand every gate on a permanent "unconfigured, unmanaged" reading — ensureProxyProbe
44
46
  would no-op forever and the managed option would vanish until a full reload. */
45
47
  proxyProbe = null;
@@ -91,6 +93,16 @@ export function isManagedProxy(): boolean {
91
93
  return proxyManaged;
92
94
  }
93
95
 
96
+ /**
97
+ * Why the proxy reported itself unconfigured, when it said so.
98
+ *
99
+ * Undefined on any backend that does not send one — every gate must treat that as "no reason given"
100
+ * and fall back to the plain connect wording.
101
+ */
102
+ export function proxyStateCode(): AiModelsResponse["code"] {
103
+ return proxyCode;
104
+ }
105
+
94
106
  /** The proxy's preferred model id ("" when it does not declare one). */
95
107
  export function getProxyDefaultModel(): string {
96
108
  return proxyDefaultModel;
@@ -161,6 +173,7 @@ export async function fetchAvailableModels(
161
173
  proxyConfigured = data.configured === true;
162
174
  proxyManaged = data.managed === true;
163
175
  proxyDefaultModel = data.defaultModel ?? "";
176
+ proxyCode = data.code;
164
177
  cache = (data.models || []).map((m) => ({ id: m.id, name: m.name || m.id }));
165
178
  return cache;
166
179
  }
package/src/types.ts CHANGED
@@ -181,6 +181,23 @@ export interface StudioPlatform {
181
181
  * Not keyed on `id`: chromium and electrobun both report `"desktop"`, and only electrobun defers.
182
182
  */
183
183
  canvasUrlDeferred?: boolean;
184
+ /**
185
+ * Base URL the canvas fetches PROJECT FILES from — component `$ref`s, `$src` modules, images.
186
+ *
187
+ * The renderer resolves a `$ref` with `fetch(url).then(r => r.json())` from inside the iframe, so
188
+ * project files have to exist at a URL, not merely behind {@link readFile}. Hosts that serve the
189
+ * project tree from their web root need no value here: the default is `<canvas
190
+ * origin>/<projectRoot>/`, which is what the dev server and the desktop loopback both already
191
+ * answer.
192
+ *
193
+ * A host whose project files are NOT at a URL-shaped `projectRoot` must set this. Jx Cloud is
194
+ * one: its `projectRoot` is the identifier `owner/repo@branch`, nothing served the tree, and
195
+ * every `$ref` fetch landed on the SPA fallback — which answers HTML at **200**, so `res.ok`
196
+ * passed and the parse died on `Unexpected token '<'`. Images failed the same way in silence.
197
+ *
198
+ * Must end in `/`; a project-relative path is appended to it verbatim.
199
+ */
200
+ documentBaseUrl?: string;
184
201
  activate: (root?: string) => Promise<void>;
185
202
  openProject: () => Promise<{
186
203
  config: ProjectConfig;
@@ -24,6 +24,7 @@ import {
24
24
  fetchAvailableModels,
25
25
  isManagedProxy,
26
26
  isProxyConfigured,
27
+ proxyStateCode,
27
28
  } from "../services/ai-models";
28
29
 
29
30
  export interface ManagedConnectOptions {
@@ -91,17 +92,31 @@ export function createManagedConnect(opts: ManagedConnectOptions): ManagedConnec
91
92
  if (!canOffer()) {
92
93
  return nothing;
93
94
  }
95
+ /*
96
+ * A lapsed grant and a fresh one get different words. The backend distinguishes them because
97
+ * "connect" is an invitation and "reconnect" is an explanation — and on a managed platform the
98
+ * lapsed case is the common one: a Cloudflare access token lives an hour.
99
+ */
100
+ const lapsed = proxyStateCode() === "cf_reconnect_required";
101
+ const busyLabel = lapsed ? "Reconnecting…" : "Connecting…";
94
102
  return html`
95
- <div class="ai-managed-connect">
96
- <div>Use Workers AI on your own Cloudflare account — no API key needed.</div>
103
+ <div class="ai-managed-connect" data-jx-recommended="cloudflare">
104
+ <div class="ai-managed-connect-lede">
105
+ ${
106
+ lapsed
107
+ ? "Your Cloudflare connection has expired. Reconnect to keep using the assistant."
108
+ : "Recommended — run the assistant on Workers AI in your own Cloudflare account. No API key to create, copy or rotate."
109
+ }
110
+ </div>
97
111
  <sp-button
98
112
  size="s"
113
+ variant="accent"
99
114
  ?disabled=${busy}
100
115
  @click=${() => {
101
116
  void connect();
102
117
  }}
103
118
  >
104
- ${busy ? "Connecting…" : "Connect Cloudflare"}
119
+ ${busy ? busyLabel : lapsed ? "Reconnect Cloudflare" : "Connect Cloudflare"}
105
120
  </sp-button>
106
121
  ${connectError ? html`<div class="ai-managed-connect-error">${connectError}</div>` : nothing}
107
122
  <div class="ai-managed-connect-divider">— or bring your own key —</div>
package/styles/shell.css CHANGED
@@ -753,6 +753,13 @@
753
753
  gap: 10px;
754
754
  width: 100%;
755
755
  }
756
+ /* The lede reads as the recommendation, so it carries weight the divider beneath it does not. */
757
+ .ai-managed-connect-lede {
758
+ color: var(--fg);
759
+ font-size: var(--spectrum-font-size-75, 12px);
760
+ max-width: 280px;
761
+ text-align: center;
762
+ }
756
763
  .ai-managed-connect-error {
757
764
  color: var(--danger);
758
765
  font-size: var(--spectrum-font-size-50, 11px);