@jxsuite/studio 2.2.2 → 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 (188) hide show
  1. package/data/css-meta.json +1487 -0
  2. package/data/elements-meta.json +370 -0
  3. package/data/html-meta.json +366 -0
  4. package/data/stylebook-meta.json +200 -0
  5. package/data/webdata.json +1621 -0
  6. package/dist/chunks/{Overlay.dev-ymy3et3y.js → Overlay.dev-d2ajy6sj.js} +7 -7
  7. package/dist/chunks/{content-source-r2n7v682.js → content-source-jrvn5vwy.js} +13 -12
  8. package/dist/chunks/{convert-to-repeater-cnpyvj74.js → convert-to-repeater-109dwcj7.js} +12 -11
  9. package/dist/chunks/{extension-sections-vpcm4jvx.js → extension-sections-2zjyty05.js} +17 -16
  10. package/dist/chunks/{files-zeg88k2d.js → files-jdbtammm.js} +18 -19
  11. package/dist/chunks/{format-host-taymsw55.js → format-host-kq6963gs.js} +2 -2
  12. package/dist/chunks/{git-panel-qp4rrwr9.js → git-panel-2stf3ahg.js} +12 -11
  13. package/dist/chunks/{layers-panel-kj93w2zg.js → layers-panel-a8wjsdjj.js} +15 -14
  14. package/dist/chunks/{monaco-setup-xy3bs05d.js → monaco-setup-a5wkmbgh.js} +6 -3
  15. package/dist/chunks/{publish-panel-6qz6kp41.js → publish-panel-2we6smjv.js} +6 -6
  16. package/dist/chunks/{quick-search-9wwa09hh.js → quick-search-hmpwdgys.js} +19 -18
  17. package/dist/chunks/right-panel-hn3y3j0j.js +38 -0
  18. package/dist/chunks/{sp-overlay.dev-h6kdx076.js → sp-overlay.dev-s18hymn9.js} +8 -8
  19. package/dist/chunks/sp-progress-circle.dev-0zxcyz3r.js +15 -0
  20. package/dist/chunks/sp-tooltip.dev-5c6d1kjz.js +15 -0
  21. package/dist/chunks/{studio-753znj4c.js → studio-0evba9z5.js} +5 -5
  22. package/dist/chunks/{studio-qhh8r5fz.js → studio-102jzb8r.js} +4 -4
  23. package/dist/chunks/{studio-22xr4q8q.js → studio-1dgqagzz.js} +18 -29
  24. package/dist/chunks/{studio-6f0am8f2.js → studio-2w11grav.js} +2 -2
  25. package/dist/chunks/{studio-pv8m0h4a.js → studio-377bbhtz.js} +28 -23
  26. package/dist/chunks/{studio-6zzbyx17.js → studio-53dn6e6t.js} +2 -2
  27. package/dist/chunks/studio-53n8bp04.js +16 -0
  28. package/dist/chunks/{studio-tqemhphj.js → studio-6gcrk1bq.js} +4 -4
  29. package/dist/chunks/{studio-3z8jjm2p.js → studio-7ejemhej.js} +3 -3
  30. package/dist/chunks/{studio-nqq8jk8f.js → studio-8x4399kc.js} +14 -14
  31. package/dist/chunks/{studio-rgwcpast.js → studio-ay8wcr2b.js} +2 -2
  32. package/dist/chunks/{studio-xmvbhg31.js → studio-c31yb4t6.js} +2 -2
  33. package/dist/chunks/{studio-5kt960pk.js → studio-ddgrc5hs.js} +133 -147
  34. package/dist/chunks/{studio-zqv3w24n.js → studio-eqyybj2e.js} +5 -5
  35. package/dist/chunks/{studio-p471gg5n.js → studio-kvfx9z2c.js} +7 -3
  36. package/dist/chunks/{studio-8sdyjfd6.js → studio-kwavhmdr.js} +2 -2
  37. package/dist/chunks/{studio-300zsz1k.js → studio-ky68mp85.js} +6 -6
  38. package/dist/chunks/{studio-x0et2z9g.js → studio-ng5gbxkj.js} +15 -15
  39. package/dist/chunks/{studio-hekaagev.js → studio-q07vj7p9.js} +17 -17
  40. package/dist/chunks/{studio-fys6z73y.js → studio-qd4k0h80.js} +7 -7
  41. package/dist/chunks/{studio-yphy4z9m.js → studio-rdsvb808.js} +5 -5
  42. package/dist/chunks/{studio-g8t7g0gp.js → studio-tqr02xaa.js} +3 -3
  43. package/dist/chunks/{studio-ckwz4d3x.js → studio-twsafr1e.js} +2 -2
  44. package/dist/chunks/{studio-qrsfh1w7.js → studio-tzx3xmvx.js} +5 -5
  45. package/dist/chunks/{studio-6a0h7sne.js → studio-vhk5fmfv.js} +2 -2
  46. package/dist/chunks/{studio-hrajjyt2.js → studio-wdt9y92a.js} +2 -2
  47. package/dist/chunks/{studio-r8vx7bkz.js → studio-wxthr2x1.js} +14 -13
  48. package/dist/chunks/{studio-yz9s88s0.js → studio-yk7037sg.js} +40 -14
  49. package/dist/chunks/{studio-1kgfcv7s.js → studio-yz06mkfj.js} +12 -11
  50. package/dist/codicon.ttf +0 -0
  51. package/dist/iframe-entry.js +14 -3
  52. package/dist/manifest.json +82 -0
  53. package/dist/studio.js +247 -106
  54. package/index.html +0 -50
  55. package/package.json +16 -9
  56. package/src/canvas/canvas-live-render.ts +2 -4
  57. package/src/canvas/canvas-origin.ts +25 -0
  58. package/src/canvas/iframe-entry.ts +5 -0
  59. package/src/canvas/iframe-host.ts +45 -4
  60. package/src/content/entry-editor.ts +2 -1
  61. package/src/editor/context-menu.ts +13 -2
  62. package/src/editor/slash-menu.ts +26 -11
  63. package/src/files/files.ts +11 -23
  64. package/src/grid/grid-panel.ts +27 -6
  65. package/src/hosting/document.ts +109 -0
  66. package/src/hosting/layout.ts +211 -0
  67. package/src/hosting/stage.ts +166 -0
  68. package/src/hosting/tsconfig.json +18 -0
  69. package/src/panels/ai-chat/chat-view.ts +25 -14
  70. package/src/panels/git-panel.ts +17 -14
  71. package/src/panels/head-panel.ts +32 -14
  72. package/src/panels/imports-panel.ts +32 -13
  73. package/src/panels/layers-panel.ts +9 -1
  74. package/src/panels/panel-registry.ts +0 -1
  75. package/src/panels/properties-panel.ts +3 -3
  76. package/src/panels/settings-pane.ts +1 -1
  77. package/src/panels/style-panel.ts +3 -3
  78. package/src/panels/stylebook-panel.ts +10 -1
  79. package/src/panels/toolbar.ts +1 -1
  80. package/src/platforms/cloud.ts +9 -0
  81. package/src/services/ai-models.ts +14 -1
  82. package/src/services/bundle-base.ts +69 -0
  83. package/src/services/monaco-setup.ts +19 -13
  84. package/src/shell/tree.ts +155 -0
  85. package/src/shell.ts +0 -2
  86. package/src/studio.ts +22 -2
  87. package/src/types.ts +32 -0
  88. package/src/ui/ai-managed-connect.ts +18 -3
  89. package/src/ui/regions.ts +10 -45
  90. package/styles/overlays.css +35 -0
  91. package/styles/shell.css +7 -0
  92. package/dist/chunks/2020-xp8vnwma.js.map +0 -24
  93. package/dist/chunks/Overlay.dev-ymy3et3y.js.map +0 -9
  94. package/dist/chunks/adapter-jkcyhgtr.js.map +0 -9
  95. package/dist/chunks/cf-settings-g5fbf2zp.js.map +0 -9
  96. package/dist/chunks/components-z6cnkyy1.js.map +0 -9
  97. package/dist/chunks/content-source-r2n7v682.js.map +0 -9
  98. package/dist/chunks/convert-to-repeater-cnpyvj74.js.map +0 -10
  99. package/dist/chunks/editor-cptq5jp1.js.map +0 -9
  100. package/dist/chunks/extension-sections-vpcm4jvx.js.map +0 -11
  101. package/dist/chunks/files-zeg88k2d.js.map +0 -9
  102. package/dist/chunks/flow-qbfgrpeh.js.map +0 -12
  103. package/dist/chunks/focus-trap.esm-ggawf1hy.js.map +0 -11
  104. package/dist/chunks/focus-visible-6eg70z18.js.map +0 -10
  105. package/dist/chunks/format-host-taymsw55.js.map +0 -9
  106. package/dist/chunks/git-panel-qp4rrwr9.js.map +0 -9
  107. package/dist/chunks/index-nj44jpzj.js.map +0 -14
  108. package/dist/chunks/index-zqxmqxmh.js.map +0 -9
  109. package/dist/chunks/javascript-q8kpmfah.js.map +0 -11
  110. package/dist/chunks/jsonMode-dkr040ga.js.map +0 -18
  111. package/dist/chunks/jx-validate-jj9rnt4n.js.map +0 -9
  112. package/dist/chunks/layers-panel-kj93w2zg.js.map +0 -9
  113. package/dist/chunks/monaco-binding-rj6xpx9y.js.map +0 -10
  114. package/dist/chunks/monaco-setup-xy3bs05d.js.map +0 -15
  115. package/dist/chunks/publish-panel-6qz6kp41.js.map +0 -9
  116. package/dist/chunks/quick-search-9wwa09hh.js.map +0 -9
  117. package/dist/chunks/right-panel-faf816ch.js +0 -37
  118. package/dist/chunks/right-panel-faf816ch.js.map +0 -9
  119. package/dist/chunks/section-registry-sx1ykvkw.js.map +0 -9
  120. package/dist/chunks/sp-overlay.dev-h6kdx076.js.map +0 -9
  121. package/dist/chunks/sp-popover.dev-dj9x7e7a.js.map +0 -9
  122. package/dist/chunks/sp-progress-circle.dev-0fpvhm52.js +0 -15
  123. package/dist/chunks/sp-progress-circle.dev-0fpvhm52.js.map +0 -9
  124. package/dist/chunks/sp-tooltip.dev-rvepkw4r.js +0 -15
  125. package/dist/chunks/sp-tooltip.dev-rvepkw4r.js.map +0 -9
  126. package/dist/chunks/sp-tray.dev-64v4pxw8.js.map +0 -12
  127. package/dist/chunks/studio-0a5qrh01.js.map +0 -9
  128. package/dist/chunks/studio-0rqqhrdy.js.map +0 -12
  129. package/dist/chunks/studio-18j3bmcz.js.map +0 -10
  130. package/dist/chunks/studio-1kgfcv7s.js.map +0 -13
  131. package/dist/chunks/studio-22xr4q8q.js.map +0 -13
  132. package/dist/chunks/studio-300zsz1k.js.map +0 -39
  133. package/dist/chunks/studio-3fk1ac9z.js.map +0 -12
  134. package/dist/chunks/studio-3n4ndrzh.js.map +0 -41
  135. package/dist/chunks/studio-3z8jjm2p.js.map +0 -13
  136. package/dist/chunks/studio-4mqb9a0j.js.map +0 -14
  137. package/dist/chunks/studio-4q0wznzf.js.map +0 -73
  138. package/dist/chunks/studio-5ehhackm.js.map +0 -10
  139. package/dist/chunks/studio-5kt960pk.js.map +0 -38
  140. package/dist/chunks/studio-5x5774ak.js.map +0 -10
  141. package/dist/chunks/studio-685s0ytq.js.map +0 -76
  142. package/dist/chunks/studio-6a0h7sne.js.map +0 -15
  143. package/dist/chunks/studio-6f0am8f2.js.map +0 -10
  144. package/dist/chunks/studio-6kv3apa9.js.map +0 -10
  145. package/dist/chunks/studio-6zzbyx17.js.map +0 -12
  146. package/dist/chunks/studio-753znj4c.js.map +0 -16
  147. package/dist/chunks/studio-7afd21nw.js.map +0 -10
  148. package/dist/chunks/studio-8jpzqk5s.js.map +0 -10
  149. package/dist/chunks/studio-8q517dfr.js.map +0 -10
  150. package/dist/chunks/studio-8sdyjfd6.js.map +0 -10
  151. package/dist/chunks/studio-a0tswy0w.js.map +0 -9
  152. package/dist/chunks/studio-a7pzveq1.js.map +0 -11
  153. package/dist/chunks/studio-ahnzd9rg.js.map +0 -10
  154. package/dist/chunks/studio-b9b6t1dg.js.map +0 -651
  155. package/dist/chunks/studio-bw5dz6km.js.map +0 -21
  156. package/dist/chunks/studio-cjm7fdjf.js.map +0 -14
  157. package/dist/chunks/studio-ckwz4d3x.js.map +0 -12
  158. package/dist/chunks/studio-d1ks410h.js.map +0 -10
  159. package/dist/chunks/studio-dg7zyt5x.js.map +0 -10
  160. package/dist/chunks/studio-fys6z73y.js.map +0 -10
  161. package/dist/chunks/studio-g1wdche0.js.map +0 -56
  162. package/dist/chunks/studio-g8t7g0gp.js.map +0 -10
  163. package/dist/chunks/studio-h4rxscfc.js.map +0 -10
  164. package/dist/chunks/studio-hekaagev.js.map +0 -18
  165. package/dist/chunks/studio-hrajjyt2.js.map +0 -10
  166. package/dist/chunks/studio-nqq8jk8f.js.map +0 -14
  167. package/dist/chunks/studio-p471gg5n.js.map +0 -14
  168. package/dist/chunks/studio-pv8m0h4a.js.map +0 -98
  169. package/dist/chunks/studio-qhh8r5fz.js.map +0 -13
  170. package/dist/chunks/studio-qrsfh1w7.js.map +0 -41
  171. package/dist/chunks/studio-r8vx7bkz.js.map +0 -235
  172. package/dist/chunks/studio-rgwcpast.js.map +0 -12
  173. package/dist/chunks/studio-s519zc99.js.map +0 -10
  174. package/dist/chunks/studio-sas7mt3k.js.map +0 -10
  175. package/dist/chunks/studio-t5wqwzdh.js.map +0 -10
  176. package/dist/chunks/studio-tqemhphj.js.map +0 -11
  177. package/dist/chunks/studio-tr20m8g8.js.map +0 -9
  178. package/dist/chunks/studio-tvgyw9xw.js.map +0 -14
  179. package/dist/chunks/studio-x0et2z9g.js.map +0 -32
  180. package/dist/chunks/studio-xm3dbxvn.js.map +0 -19
  181. package/dist/chunks/studio-xmvbhg31.js.map +0 -10
  182. package/dist/chunks/studio-ynxwkrzd.js.map +0 -246
  183. package/dist/chunks/studio-yphy4z9m.js.map +0 -10
  184. package/dist/chunks/studio-yz9s88s0.js.map +0 -54
  185. package/dist/chunks/studio-z6nw2ek2.js.map +0 -10
  186. package/dist/chunks/studio-zqv3w24n.js.map +0 -11
  187. package/dist/chunks/tsMode-76kr53q0.js.map +0 -13
  188. package/dist/chunks/ws-client-6shc5rze.js.map +0 -12
@@ -742,7 +742,15 @@ export function onTreeKeydown(
742
742
  */
743
743
  export function startLayerTitleEdit(path: JxPath, rerender: () => void) {
744
744
  const key = pathKey(path);
745
- const row = document.querySelector(`.layer-row[data-path="${key}"]`);
745
+ /* Scoped to THIS panel's tree, and escaped, through the same helper every other row lookup in
746
+ this file uses. The predecessor was a bare `document.querySelector` with the key interpolated
747
+ raw, which is wrong twice over: a second pane — or the stylebook, which draws a layer tree of
748
+ its own — puts more than one `.layer-row[data-path=…]` in the document, and the first match
749
+ wins rather than the right one; and an unescaped key containing a quote or a bracket is not a
750
+ selector the parser accepts, so a legitimately-named node throws instead of renaming.
751
+ `_outlineList` is null before the first render, which is the same "row the window does not
752
+ hold" case the reveal-and-repaint path below already handles. */
753
+ const row = _outlineList ? rowElementFor(_outlineList, key) : null;
746
754
  if (!row) {
747
755
  // Renaming through a command (⌘↵, the palette, the block bar) can name a row the window does
748
756
  // Not hold. Scroll to it and repaint; the row exists on the next pass, and the caller's own
@@ -83,7 +83,6 @@ export interface NavigatorPanelDeps {
83
83
  registerLayersDnD: () => void;
84
84
  registerElementsDnD: () => void;
85
85
  registerComponentsDnD: () => void;
86
- setupTreeKeyboard: (tree: HTMLElement) => void;
87
86
  registerFileTreeDnD: (ctx: { renderLeftPanel: () => void }) => void;
88
87
  setGitDiffState: (state: GitDiffState | null) => void;
89
88
  cloneRepository?: () => void;
@@ -544,7 +544,7 @@ function renderLinkTargetField(node: JxMutableNode, path: JxPath) {
544
544
  <sp-picker
545
545
  class="link-target-kind"
546
546
  size="s"
547
- value=${kind}
547
+ .value=${live(kind)}
548
548
  @change=${(e: Event) => {
549
549
  const nextKind = (e.target as HTMLInputElement).value as LinkKind;
550
550
  // Switching kind reinterprets the current value under the new kind.
@@ -566,7 +566,7 @@ function renderLinkTargetField(node: JxMutableNode, path: JxPath) {
566
566
  <sp-picker
567
567
  class="link-target-value"
568
568
  size="s"
569
- value=${value}
569
+ .value=${live(value)}
570
570
  @change=${(e: Event) => commit("internal", (e.target as HTMLInputElement).value)}
571
571
  >
572
572
  ${knownValue ? html`<sp-menu-item value=${value}>${value}</sp-menu-item>` : nothing}
@@ -624,7 +624,7 @@ function renderTargetField(node: JxMutableNode, path: JxPath, entry: HtmlMetaEnt
624
624
  <sp-picker
625
625
  class="link-target-window"
626
626
  size="s"
627
- value=${current}
627
+ .value=${live(current)}
628
628
  @change=${(e: Event) =>
629
629
  transactDoc(activeTab.value!, (t) =>
630
630
  mutateUpdateAttribute(
@@ -18,7 +18,7 @@
18
18
  * `settings/settings-document.ts`, which the app loads when it registers its commands.
19
19
  *
20
20
  * Nothing here is stamped with a region of its own. `#canvas-wrap` already carries `pane.primary`
21
- * (`ui/regions.ts`'s `SHELL_REGION_HOSTS`), the entry rows and the entry editor are stamped by
21
+ * (the frame, `shell/tree.ts`), the entry rows and the entry editor are stamped by
22
22
  * `settings/contributed-section.ts` as `pane.primary/entry:<key>` and `pane.primary/editor`, and a
23
23
  * third id for "the settings body" would be a hand-stamped region with nothing to say.
24
24
  *
@@ -1215,7 +1215,7 @@ function styleSidebarTemplate(
1215
1215
  return html`
1216
1216
  <sp-accordion-item
1217
1217
  label=${sec.label}
1218
- .open=${isOpen}
1218
+ .open=${live(isOpen)}
1219
1219
  @sp-accordion-item-toggle=${(e: Event) => {
1220
1220
  activeTab.value!.session.ui.styleSections = {
1221
1221
  ...activeTab.value!.session.ui.styleSections,
@@ -1235,7 +1235,7 @@ function styleSidebarTemplate(
1235
1235
  const customSectionT = html`
1236
1236
  <sp-accordion-item
1237
1237
  label="Custom"
1238
- .open=${customIsOpen}
1238
+ .open=${live(customIsOpen)}
1239
1239
  @sp-accordion-item-toggle=${(e: Event) => {
1240
1240
  activeTab.value!.session.ui.styleSections = {
1241
1241
  ...activeTab.value!.session.ui.styleSections,
@@ -1319,7 +1319,7 @@ function styleSidebarTemplate(
1319
1319
  ? html`
1320
1320
  <sp-accordion-item
1321
1321
  label="Relative Styling"
1322
- .open=${nestedIsOpen}
1322
+ .open=${live(nestedIsOpen)}
1323
1323
  @sp-accordion-item-toggle=${(e: Event) => {
1324
1324
  activeTab.value!.session.ui.styleSections = {
1325
1325
  ...activeTab.value!.session.ui.styleSections,
@@ -14,6 +14,7 @@
14
14
  */
15
15
 
16
16
  import { html, render as litRender } from "lit-html";
17
+ import { repeat } from "lit-html/directives/repeat.js";
17
18
  import { ref } from "lit-html/directives/ref.js";
18
19
  import { classMap } from "lit-html/directives/class-map.js";
19
20
  import { live } from "lit-html/directives/live.js";
@@ -169,7 +170,15 @@ export function renderStylebookMode(surface: CanvasSurface, ctx: StylebookCtx) {
169
170
  }
170
171
  })}
171
172
  >
172
- ${panelEntries.map((e) => e.tpl)}
173
+ ${repeat(
174
+ panelEntries,
175
+ /* Keyed on mediaName. Each entry owns a canvas IFRAME, and the breakpoint set changes
176
+ with the project's media definitions — so position-based reuse hands one breakpoint's
177
+ iframe to another's width. That is a live document rendered at the wrong viewport,
178
+ not a cosmetic diff. */
179
+ (e) => e.panel.mediaName,
180
+ (e) => e.tpl,
181
+ )}
173
182
  </div>
174
183
  `,
175
184
  /** @type {HTMLElement} */ canvasWrap,
@@ -644,7 +644,7 @@ function dockTogglesTpl(registry: CommandRegistry) {
644
644
  /**
645
645
  * Mount the Command Bar.
646
646
  *
647
- * @param rootEl The `#toolbar` host, stamped `commandbar` by `stampShellRegions()`.
647
+ * @param rootEl The `#toolbar` host, stamped `commandbar` by the frame (`shell/tree.ts`).
648
648
  * @param _ctx Ignored — see {@link ToolbarCtx}.
649
649
  */
650
650
  export function mount(rootEl: HTMLElement, _ctx: ToolbarCtx = {}) {
@@ -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
  }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Where the studio bundle was loaded from — the one fact a shipped asset can be located against.
3
+ *
4
+ * `import.meta.url` is NOT that fact, and the difference has been shipping a silent bug. Only the
5
+ * two ENTRIES are guaranteed to be emitted at `dist/<name>.js` (studio.md §11.1 — four consumers
6
+ * address those paths literally, so they are never hashed). Every other module is subject to
7
+ * `splitting: true` and may be hoisted into a content-hashed chunk under `dist/chunks/` — and
8
+ * `services/monaco-setup` was, by the code-split in 78d85ba2. Its `new URL("workers/…",
9
+ * import.meta.url)` therefore resolved against `dist/chunks/`, while all three distribution paths
10
+ * stage the workers beside the ENTRY. Verified in the built output: the string `workers/` appears
11
+ * in `dist/chunks/monaco-setup-<hash>.js` and in neither `dist/studio.js` nor
12
+ * `dist/iframe-entry.js`, and `dist/chunks/workers/` does not exist.
13
+ *
14
+ * The failure mode is why this is worth a module rather than a comment: a Monaco worker that 404s
15
+ * takes the whole JSON language service with it — no schema validation, no completion, no hover —
16
+ * and reports nothing. Nobody sees a red anything.
17
+ *
18
+ * So the entries publish their own location and everything else reads it. The base is the entry's
19
+ * DIRECTORY, so `bundleUrl("workers/json.worker.js")` lands beside the entry on every host without
20
+ * the host being asked for anything: `/packages/studio/dist/` on the repo dev server,
21
+ * `views://studio/dist/` in the packaged app, `/__studio__/dist/` on the desktop loopback server,
22
+ * and whatever prefix a cloud host staged the tree under.
23
+ *
24
+ * **Only an entry may call {@link setBundleBase}.** `tests/entry-anchors.test.ts` enforces both
25
+ * halves — that both entries call it, and that no other module under `src/` so much as mentions
26
+ * `import.meta.url` — because the next refactor that moves a module into a chunk must not be able
27
+ * to reintroduce this quietly.
28
+ */
29
+
30
+ /** The entry's directory, with a trailing slash. `undefined` until an entry sets it. */
31
+ let base: string | undefined;
32
+
33
+ /**
34
+ * Record the calling ENTRY's own URL. Call as `setBundleBase(import.meta.url)`, first statement
35
+ * after the imports, from `src/studio.ts` or `src/canvas/iframe-entry.ts` only.
36
+ *
37
+ * @param entryUrl The entry module's `import.meta.url`.
38
+ */
39
+ export function setBundleBase(entryUrl: string): void {
40
+ // Directory, not file: `new URL("./", <file url>)` strips the last segment.
41
+ base = new URL("./", entryUrl).href;
42
+ }
43
+
44
+ /**
45
+ * Resolve a path relative to the bundle's own directory.
46
+ *
47
+ * Throws rather than guessing, because its predecessor's whole problem was failing silently: an
48
+ * unset base means an entry did not run, and a URL invented here would 404 somewhere the user
49
+ * cannot see.
50
+ *
51
+ * @param path Bundle-relative path, e.g. `workers/json.worker.js` or `../canvas.html`.
52
+ * @returns Absolute URL.
53
+ * @throws {Error} When no entry has called {@link setBundleBase}.
54
+ */
55
+ export function bundleUrl(path: string): string {
56
+ if (base === undefined) {
57
+ throw new Error(
58
+ `bundleUrl(${JSON.stringify(path)}) before setBundleBase(). An entry module ` +
59
+ `(src/studio.ts or src/canvas/iframe-entry.ts) must call setBundleBase(import.meta.url) ` +
60
+ `first; a test needs tests/with-dom.ts, which sets one.`,
61
+ );
62
+ }
63
+ return new URL(path, base).href;
64
+ }
65
+
66
+ /** Test seam: forget the recorded base. Not called by application code. */
67
+ export function resetBundleBase(): void {
68
+ base = undefined;
69
+ }
@@ -80,6 +80,7 @@ import "monaco-editor/languages/definitions/javascript/register";
80
80
  import { flattenSchema } from "@jxsuite/schema/project-schemas";
81
81
  import jxSchema from "@jxsuite/schema/schema.json";
82
82
  import projectSchema from "@jxsuite/schema/project-schema.json";
83
+ import { bundleUrl } from "./bundle-base";
83
84
 
84
85
  /* Re-exported for the modules that already imported it from here. New callers should reach for
85
86
  `./model-uri` directly — it is Monaco-free, and importing this module loads the editor. */
@@ -93,28 +94,33 @@ const WORKER_FILES: Record<string, string> = {
93
94
  };
94
95
 
95
96
  /**
96
- * Resolve a Monaco worker to the PRE-BUNDLED copy that ships NEXT TO this bundle — `workers/`
97
+ * Resolve a Monaco worker to the PRE-BUNDLED copy that ships NEXT TO THE ENTRY — `dist/workers/`
97
98
  * beside `dist/studio.js`, written by scripts/build-workers.ts.
98
99
  *
99
- * Self-location via `import.meta.url` (Bun emits it verbatim for browser targets) is what makes one
100
- * expression correct on every host, because none of them agree on a prefix and two of them cannot
101
- * be served by one: the repo dev server mounts the shell at `/packages/studio/index.html`, the
102
- * packaged desktop app at `views://studio/index.html`, the desktop loopback server under
103
- * `/__studio__/`, and the cloud at `/edit/:owner/:repo@:branch` — a DEEP path, so even a
104
- * document-relative URL would resolve into the wrong directory there. The only invariant is the
105
- * bundle's own location.
100
+ * Self-location is what makes one expression correct on every host, because none of them agree on a
101
+ * prefix and two of them cannot be served by one: the repo dev server mounts the shell at
102
+ * `/packages/studio/index.html`, the packaged desktop app at `views://studio/index.html`, the
103
+ * desktop loopback server under `/__studio__/`, and a cloud host under whatever prefix it staged
104
+ * the tree at. The only invariant is the bundle's own location.
106
105
  *
107
- * A root-absolute `/monaco-editor/esm/vs/...` (what this used to emit) resolved on exactly one of
108
- * them, the repo dev server, which serves bare specifiers out of the monorepo's `node_modules`.
109
- * Everywhere else it 404s — and a Monaco worker that fails to start takes the whole JSON language
110
- * service with it: no schema validation, no completion, no hover, and no error the user can see.
106
+ * **But `import.meta.url` is not that location, and this function used to think it was.** It is
107
+ * THIS MODULE's url, and since the code-split (78d85ba2) this module is emitted into
108
+ * `dist/chunks/monaco-setup-<hash>.js` — so the expression resolved to `chunks/workers/<name>`, a
109
+ * directory that has never existed in any distribution. Only the two entries have a contractual
110
+ * emitted path (studio.md §11.1), which is exactly what `services/bundle-base` records.
111
+ *
112
+ * The bug this closes is invisible by construction, which is why it survived: a Monaco worker that
113
+ * 404s takes the whole JSON language service with it — no schema validation, no completion, no
114
+ * hover — and surfaces no error at all. The same silence covered the predecessor of the
115
+ * `import.meta.url` form, a root-absolute `/monaco-editor/esm/vs/...` that resolved on the repo dev
116
+ * server alone.
111
117
  *
112
118
  * @param {string} label - Monaco worker label ("json", "typescript", …)
113
119
  * @returns {string} Absolute URL of the worker bundle
114
120
  */
115
121
  export function workerUrl(label: string): string {
116
122
  const file = WORKER_FILES[label] || WORKER_FILES.editorWorkerService!;
117
- return new URL(`workers/${file}`, import.meta.url).href;
123
+ return bundleUrl(`workers/${file}`);
118
124
  }
119
125
 
120
126
  self.MonacoEnvironment = {
@@ -0,0 +1,155 @@
1
+ /**
2
+ * The shell's mount tree — the one definition of what the application frame IS.
3
+ *
4
+ * It was `index.html`'s body, and, in drifted copies, the body of twenty-three test files. The copy
5
+ * in `tests/studio-shell-fixture.ts` had lost `#resize-bottom`, `#bottom-dock` and `#layer-toast`,
6
+ * so every shell-boot test ran against a shell with no bottom dock and no toast host — a difference
7
+ * no test could report, because the fixture WAS the thing under test.
8
+ *
9
+ * Nothing here is dynamic: `mountShellTree()` renders once at boot, before `initShellRefs()` adopts
10
+ * the hosts, and each region then renders into its own host through its own effect (see
11
+ * studio-ui-guidelines.md §9.3). It is a lit template rather than a string because that makes the
12
+ * region attributes bindings the tree owns, and because the twenty-three test files can now ask for
13
+ * the real shell instead of describing one.
14
+ */
15
+
16
+ import { html, render as litRender } from "lit-html";
17
+ import type { TemplateResult } from "lit-html";
18
+
19
+ /**
20
+ * Render a frame template into `host`, surviving a host that was emptied since the last mount.
21
+ *
22
+ * `textContent = ""` (or `innerHTML = ""`) removes the comment nodes lit uses as a ChildPart's
23
+ * markers but leaves lit's private `_$litPart$` reference behind, so the next render reuses a part
24
+ * whose markers are detached from the DOM and throws — or, worse, quietly renders nothing. Test
25
+ * fixtures clear the body between cases constantly, and the app does the same thing to a stage on
26
+ * every mode transition; `canvas/canvas-render.ts`'s hardClearCanvasWrap carries the same ejection
27
+ * for the same reason.
28
+ *
29
+ * Mounting is a one-shot, so ejecting unconditionally costs nothing and makes it idempotent.
30
+ */
31
+ function mountInto(host: ParentNode, tpl: TemplateResult): void {
32
+ /* Both halves, in this order. Clearing alone leaves lit's private part reference pointing at
33
+ comment markers that are no longer in the document, so the next render throws or paints
34
+ nothing. Ejecting alone leaves the OLD nodes in place and lit starts a fresh part beside them,
35
+ so a second mount produces two frames rather than one. */
36
+ (host as HTMLElement).textContent = "";
37
+ // @ts-expect-error -- _$litPart$ is lit's private render-part marker, not in the DOM types
38
+ delete (host as HTMLElement)["_$litPart$"];
39
+ litRender(tpl, host as HTMLElement);
40
+ }
41
+
42
+ /**
43
+ * The four overlay layers that `ui/layers.ts` renders into, in stacking order.
44
+ *
45
+ * Exported apart from {@link shellTree} because it is the piece a unit test usually wants alone
46
+ * (through `mountOverlayLayers` in the test harness) — twenty fixtures were describing this set by
47
+ * hand, and they had already stopped agreeing: most carried three layers and one carried four, so
48
+ * whether a toast host existed at all depended on which test file you happened to be in.
49
+ */
50
+ export function overlayLayers(): TemplateResult {
51
+ return html`
52
+ <!-- The four overlay layers, in stacking order. Their z-indices were inline style
53
+ attributes, which put the one piece of ordering the whole overlay system depends on
54
+ outside the reach of check-styles.ts's stacking rule — the check that exists because a
55
+ blocking progress modal once shipped above its own scrim with no reachable exit. They are
56
+ classes in styles/overlays.css now, and the toast host is ABOVE the dialog host because
57
+ an operation started from a dialog reports its outcome to the person still looking at
58
+ it. -->
59
+ <div id="layer-popover" class="jx-layer jx-layer--popover"></div>
60
+ <div id="layer-modal" class="jx-layer jx-layer--modal"></div>
61
+ <div id="layer-dialog" class="jx-layer jx-layer--dialog"></div>
62
+ <!-- role="status" sits on the HOST, so a stack of toasts is announced as one live region
63
+ rather than one region per notification. It carries its region id whether or not a toast
64
+ has ever been raised: a region that only exists once something has gone wrong is one a
65
+ screenshot cannot address and focus cannot be moved into. -->
66
+ <div
67
+ id="layer-toast"
68
+ class="jx-layer jx-layer--toast"
69
+ role="status"
70
+ aria-live="polite"
71
+ data-jx-region="overlay.toasts"
72
+ ></div>
73
+ `;
74
+ }
75
+
76
+ /**
77
+ * The application frame: the Spectrum theme wrapper, the app grid, and the four overlay layers.
78
+ *
79
+ * The theme's three attributes are BINDINGS rather than literals, and that is not decoration. A
80
+ * literal lives in the template lit clones with `importNode`, and cloning a registered custom
81
+ * element makes the DOM fire `attributeChangedCallback` before its constructor has run — Spectrum's
82
+ * Theme then reaches for `_provideSystemContext` and finds nothing. A binding is applied after the
83
+ * element exists. `shell.ts` overwrites `color` on the first theme effect anyway, so the literal
84
+ * was never the source of truth.
85
+ *
86
+ * Region ids are stamped HERE. They used to live in a selector-to-id map in `ui/regions.ts`, whose
87
+ * comment gave the reason plainly — these were "bare `<div id>` in index.html, so it cannot stamp
88
+ * itself". A template can, so the map is gone and the id sits on the element it names.
89
+ */
90
+ export function shellTree(): TemplateResult {
91
+ return html`
92
+ <div id="app">
93
+ <div id="toolbar" data-jx-region="commandbar"></div>
94
+ <!-- The PANE GRID (studio.md §18.1). One cell per pane, each holding that pane's own strip,
95
+ jump bar, chrome layer and stage — the four surfaces that used to be flat siblings of
96
+ THIS grid, which is to say application rows that only ever described one pane. There is
97
+ nothing to see here in markup: panels/pane-grid.ts reconciles the cells against
98
+ workspace.panes, because how many there are is a fact about the workspace and a
99
+ single div can only ever be one of them. -->
100
+ <div id="pane-grid"></div>
101
+ <div id="activity-bar" data-jx-region="rail"></div>
102
+ <div id="left-panel" data-jx-region="navigator"></div>
103
+ <div id="resize-left" class="resize-handle"></div>
104
+ <!-- The Bottom dock (⌘J). It sits in the PANE's column, under the stage and above the
105
+ status bar, so opening it never narrows the Navigator or the Inspector (studio.md §12
106
+ region ⑪). Its handle resizes on the other axis; panels/bottom-dock.ts stamps
107
+ dock.bottom on this host while it is open, and nothing while it is closed. -->
108
+ <div id="resize-bottom" class="resize-handle resize-handle-row"></div>
109
+ <div id="bottom-dock"></div>
110
+ <div id="resize-right" class="resize-handle"></div>
111
+ <!-- The Inspector dock. Four tabs — Content · Style · Logic · Assistant (studio.md §12
112
+ region ⑨) — all inside this one host. -->
113
+ <div id="right-panel" data-jx-region="inspector"></div>
114
+ <!-- The app's only status channel: announce what lands in it (studio.md §12 region ⑫). -->
115
+ <div id="statusbar" role="status" aria-live="polite" data-jx-region="statusbar"></div>
116
+ </div>
117
+ ${overlayLayers()}
118
+ `;
119
+ }
120
+
121
+ /**
122
+ * Render {@link shellTree} into `host`.
123
+ *
124
+ * The entry calls this once, before `initShellRefs()`. Tests call it instead of pasting an
125
+ * approximation of the frame — which is the whole reason it is a function.
126
+ *
127
+ * @param host Where the frame goes. Defaults to the document body.
128
+ */
129
+ export function mountShellTree(host: ParentNode = document.body): void {
130
+ mountInto(themeHost(host), shellTree());
131
+ }
132
+
133
+ /**
134
+ * The Spectrum theme wrapper, created once and reused.
135
+ *
136
+ * It is NOT part of {@link shellTree}, and the reason is a real constraint rather than taste. lit
137
+ * instantiates a template by cloning it with `importNode`, and cloning a registered custom element
138
+ * enqueues its `attributeChangedCallback` before the constructor body has run — Spectrum's Theme
139
+ * then reaches for a field it assigns in that body and throws. `createElement` runs the constructor
140
+ * first, so the attributes are safe to set afterwards.
141
+ *
142
+ * `shell.ts` rewrites `color` on the first theme effect, so the value here is only the first frame.
143
+ */
144
+ function themeHost(host: ParentNode): HTMLElement {
145
+ const existing = host.querySelector?.("sp-theme");
146
+ if (existing) {
147
+ return existing as HTMLElement;
148
+ }
149
+ const theme = document.createElement("sp-theme");
150
+ theme.setAttribute("color", "dark");
151
+ theme.setAttribute("scale", "medium");
152
+ theme.setAttribute("system", "spectrum");
153
+ (host as HTMLElement).append(theme);
154
+ return theme;
155
+ }
package/src/shell.ts CHANGED
@@ -24,7 +24,6 @@
24
24
  import { effect, effectScope, reactive } from "./reactivity";
25
25
  import { loadedMonaco } from "./services/monaco-lazy";
26
26
  import { applyStartupProfile } from "./services/profile";
27
- import { stampShellRegions } from "./ui/regions";
28
27
  import { workspace } from "./workspace/workspace";
29
28
  import { captureSession, readSession } from "./workspace/session";
30
29
  import type { PersistedSession } from "./workspace/session";
@@ -1044,7 +1043,6 @@ export function mountShell(): void {
1044
1043
  }
1045
1044
  // The six shell hosts are bare `<div id>`s in index.html, so they cannot stamp their own region
1046
1045
  // The way a panel or an overlay slot does. One table, applied once the tree exists.
1047
- stampShellRegions();
1048
1046
  // The Bottom dock mounts from here rather than from the bootstrap because it is a DOCK: it is
1049
1047
  // Projected onto the grid by the same record and the same effect as the other two, and a second
1050
1048
  // Mounting site would be a second place the shell's own layout is decided. Every surface's own
package/src/studio.ts CHANGED
@@ -112,7 +112,6 @@ import {
112
112
  openLastSessionOrHome,
113
113
  registerFileTreeDnD,
114
114
  reloadCleanTab,
115
- setupTreeKeyboard,
116
115
  } from "./files/files";
117
116
  import { startFsSync } from "./files/fs-events";
118
117
  import { invalidateParamValues } from "./page-params";
@@ -233,6 +232,24 @@ import { convertToComponent } from "./editor/convert-to-component";
233
232
  import type { GitDiffState } from "./types";
234
233
  import type { Tab } from "./tabs/tab";
235
234
  import type { JxMutableNode, ProjectConfig } from "@jxsuite/schema/types";
235
+ import { setBundleBase } from "./services/bundle-base";
236
+ import { mountShellTree } from "./shell/tree";
237
+
238
+ /**
239
+ * Anchor every shipped-asset URL to THIS module's directory.
240
+ *
241
+ * Only an ENTRY may do this, and it must be an entry: `splitting: true` may hoist any other module
242
+ * into a content-hashed chunk under `dist/chunks/`, where `import.meta.url` means something else
243
+ * entirely. That is not hypothetical — it is the bug this call fixes, in `services/monaco-setup`.
244
+ * `tests/entry-anchors.test.ts` holds the line in both directions.
245
+ *
246
+ * Placed before any other statement so nothing can read the base before it exists. In ESM the
247
+ * entry's body runs AFTER its whole static import graph, so a module that called `bundleUrl()`
248
+ * during evaluation would still throw — deliberately, and loudly. Nothing does today: Monaco is
249
+ * reached only through the dynamic `services/monaco-lazy`, and `workerUrl` runs inside
250
+ * `MonacoEnvironment.getWorker`.
251
+ */
252
+ setBundleBase(import.meta.url);
236
253
 
237
254
  void _swc;
238
255
 
@@ -419,6 +436,10 @@ mountResizeEdges();
419
436
 
420
437
  // ─── Render loop ──────────────────────────────────────────────────────────────
421
438
 
439
+ /* The application frame, before anything adopts a host out of it. index.html carries an empty body
440
+ and this is the only definition — see src/shell/tree.ts for what that fixed. */
441
+ mountShellTree();
442
+
422
443
  initShellRefs();
423
444
  // One effect projects the dock record onto the shell grid — collapse classes and column widths.
424
445
  mountShell();
@@ -815,7 +836,6 @@ leftPanelMod.mount({
815
836
  setGitDiffState: (state: GitDiffState | null) => {
816
837
  shell.git.diffState = state;
817
838
  },
818
- setupTreeKeyboard,
819
839
  webdata,
820
840
  });
821
841
 
package/src/types.ts CHANGED
@@ -166,6 +166,38 @@ export interface StudioPlatform {
166
166
  * "desktop"`, but only chromium sets this.
167
167
  */
168
168
  canvasUrl?: string;
169
+ /**
170
+ * True when this platform resolves {@link canvasUrl} ASYNCHRONOUSLY.
171
+ *
172
+ * Electrobun does: the url is this window's loopback port, fetched over RPC inside `activate()`.
173
+ * Until the canvas fallback was anchored to the bundle it did not matter — the old literal
174
+ * `/packages/studio/canvas.html` resolved to nothing servable under `views://`, so an early frame
175
+ * simply failed and the host rebuilt when the real url landed. Now the fallback RESOLVES:
176
+ * `views://studio/canvas.html` is a document electrobun really stages. An early frame would boot
177
+ * the whole canvas bundle inside the SHELL's app-privileged origin, in a CEF instance running
178
+ * `disable-site-isolation-trials` — and the cross-origin loopback canvas exists precisely so that
179
+ * cannot happen. Declaring it makes the host wait instead.
180
+ *
181
+ * Not keyed on `id`: chromium and electrobun both report `"desktop"`, and only electrobun defers.
182
+ */
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;
169
201
  activate: (root?: string) => Promise<void>;
170
202
  openProject: () => Promise<{
171
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>