@jxsuite/studio 10.0.0 → 10.2.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 (244) hide show
  1. package/dist/chunks/{2020-j56903f1.js → 2020-bx702wqa.js} +3 -3
  2. package/dist/chunks/adapter-pp011m5x.js +4 -0
  3. package/dist/chunks/add-new-flow-ecqmzz76.js +4 -0
  4. package/dist/chunks/cf-account-picker-c07znkqt.js +4 -0
  5. package/dist/chunks/cf-settings-0v2bmqy0.js +4 -0
  6. package/dist/chunks/components-yn0dn2pq.js +4 -0
  7. package/dist/chunks/content-source-g0j41yy6.js +4 -0
  8. package/dist/chunks/{convert-to-repeater-fhdwyg14.js → convert-to-repeater-sbjk7tgn.js} +3 -3
  9. package/dist/chunks/editor-xz02jeh1.js +4 -0
  10. package/dist/chunks/entry-commands-kmt02crp.js +4 -0
  11. package/dist/chunks/extension-sections-0rprf12d.js +4 -0
  12. package/dist/chunks/file-ops-mexj5qed.js +4 -0
  13. package/dist/chunks/files-b2tcppdd.js +4 -0
  14. package/dist/chunks/format-host-btsevv0c.js +4 -0
  15. package/dist/chunks/fs-events-r0apejva.js +4 -0
  16. package/dist/chunks/grid-open-n2a8xkht.js +4 -0
  17. package/dist/chunks/{grid-view-ka55z38f.js → grid-view-tghcwrrt.js} +12 -12
  18. package/dist/chunks/iframe-host-gx0ypyh3.js +4 -0
  19. package/dist/chunks/index-bs4k9tr6.js +4 -0
  20. package/dist/chunks/{index-qb98xhse.js → index-vr006kbt.js} +3 -3
  21. package/dist/chunks/{javascript-ces13wny.js → javascript-z1g3s9fd.js} +2 -2
  22. package/dist/chunks/{jsonMode-6md16d4v.js → jsonMode-7szwg8c4.js} +5 -5
  23. package/dist/chunks/jx-validate-ymf10teh.js +4 -0
  24. package/dist/chunks/layers-p9nfya38.js +4 -0
  25. package/dist/chunks/layers-panel-tt1hzhhf.js +4 -0
  26. package/dist/chunks/lit-html-x18n2sfz.js +4 -0
  27. package/dist/chunks/media-library-0n45gv8r.js +4 -0
  28. package/dist/chunks/media-picker-s09at95z.js +4 -0
  29. package/dist/chunks/{monaco-binding-p8j80s4w.js → monaco-binding-v42w9c1v.js} +3 -3
  30. package/dist/chunks/monaco-buffer-een2ea0v.js +4 -0
  31. package/dist/chunks/{monaco-setup-wasktzfg.js → monaco-setup-7bffxd6v.js} +3 -3
  32. package/dist/chunks/notify-qwb8radt.js +4 -0
  33. package/dist/chunks/parse-v77xtwv7.js +4 -0
  34. package/dist/chunks/platform-8jnez0wr.js +4 -0
  35. package/dist/chunks/publish-panel-aysnj611.js +4 -0
  36. package/dist/chunks/{publish-quick-vr695j26.js → publish-quick-kpbqs9mz.js} +3 -3
  37. package/dist/chunks/publish-workspace-nd0a8q99.js +4 -0
  38. package/dist/chunks/quick-search-vg0jkhk2.js +4 -0
  39. package/dist/chunks/references-a7v6a0e2.js +4 -0
  40. package/dist/chunks/right-panel-cvk1vxx5.js +4 -0
  41. package/dist/chunks/source-control-fsqzc6he.js +4 -0
  42. package/dist/chunks/studio-05jfwbmf.js +5 -0
  43. package/dist/chunks/{studio-hy4ye0dx.js → studio-0q5jwddh.js} +7 -7
  44. package/dist/chunks/{studio-jttyqrmh.js → studio-0w2dr0q8.js} +4 -4
  45. package/dist/chunks/{studio-xkxetm65.js → studio-1tzpds97.js} +5 -5
  46. package/dist/chunks/{studio-m48v13s3.js → studio-216b27sv.js} +3 -3
  47. package/dist/chunks/studio-2ejhqns4.js +5 -0
  48. package/dist/chunks/studio-2r90cts3.js +16 -0
  49. package/dist/chunks/{studio-h310mbqm.js → studio-399a25za.js} +4 -4
  50. package/dist/chunks/studio-399xknkb.js +6 -0
  51. package/dist/chunks/{studio-wxhy1yh7.js → studio-43y04qck.js} +4 -4
  52. package/dist/chunks/{studio-06j4en5h.js → studio-4g8yb6jn.js} +2 -2
  53. package/dist/chunks/{studio-jw0q5j6k.js → studio-4jhw38b2.js} +4 -4
  54. package/dist/chunks/{studio-3v5cagy9.js → studio-4ncmwqw5.js} +4 -4
  55. package/dist/chunks/studio-5atwjgkj.js +7 -0
  56. package/dist/chunks/{studio-gr36vnte.js → studio-5frqk7c0.js} +4 -4
  57. package/dist/chunks/{studio-9dz76yqc.js → studio-646nn2ys.js} +5 -5
  58. package/dist/chunks/{studio-tj2s0tks.js → studio-6cwj6xrx.js} +4 -4
  59. package/dist/chunks/studio-6hzdjfv0.js +5 -0
  60. package/dist/chunks/{studio-hcsba201.js → studio-71m92v9s.js} +7 -7
  61. package/dist/chunks/{studio-3c8pp7pb.js → studio-75kcdbsk.js} +4 -4
  62. package/dist/chunks/{studio-gwrhcrss.js → studio-78pywnp2.js} +5 -5
  63. package/dist/chunks/{studio-9cxmp61c.js → studio-86r8hp7r.js} +5 -5
  64. package/dist/chunks/studio-886t6ztg.js +5 -0
  65. package/dist/chunks/studio-8wnvtr4z.js +5 -0
  66. package/dist/chunks/studio-8z66jk4z.js +5 -0
  67. package/dist/chunks/{studio-ajw4k8hp.js → studio-9rbe2xpw.js} +3 -3
  68. package/dist/chunks/{studio-y26hx9re.js → studio-awxwjawq.js} +4 -4
  69. package/dist/chunks/studio-b2qpt0xy.js +14 -0
  70. package/dist/chunks/{studio-dx73md4n.js → studio-bvs9kpst.js} +7 -7
  71. package/dist/chunks/studio-c71806xz.js +7 -0
  72. package/dist/chunks/{studio-7dzyz9n8.js → studio-cbwks69x.js} +144 -144
  73. package/dist/chunks/studio-cg1bfyhf.js +9 -0
  74. package/dist/chunks/studio-cg710sk9.js +5 -0
  75. package/dist/chunks/studio-cw0wb9qk.js +7 -0
  76. package/dist/chunks/studio-cyheys7k.js +5 -0
  77. package/dist/chunks/studio-ea5m9kg5.js +5 -0
  78. package/dist/chunks/{studio-ry81j4q1.js → studio-ea74s8c1.js} +4 -4
  79. package/dist/chunks/{studio-xdpz9rpe.js → studio-efdxjwyp.js} +34 -34
  80. package/dist/chunks/{studio-agyvzfm4.js → studio-egmw2837.js} +4 -4
  81. package/dist/chunks/{studio-5tzgq6fx.js → studio-er5f383a.js} +4 -4
  82. package/dist/chunks/studio-f746039f.js +5 -0
  83. package/dist/chunks/studio-fmdymd9v.js +5 -0
  84. package/dist/chunks/studio-gcm12z00.js +6 -0
  85. package/dist/chunks/{studio-vswcpzk7.js → studio-gj98m8gy.js} +5 -5
  86. package/dist/chunks/{studio-yg3djpnm.js → studio-hasbgjah.js} +4 -4
  87. package/dist/chunks/studio-hhp4x8xf.js +6 -0
  88. package/dist/chunks/{studio-3w35qvay.js → studio-hz5n7x27.js} +4 -4
  89. package/dist/chunks/{studio-7xag4na8.js → studio-j3d2fjqz.js} +4 -4
  90. package/dist/chunks/studio-jeraxdgv.js +7 -0
  91. package/dist/chunks/studio-jvgek9xt.js +5 -0
  92. package/dist/chunks/studio-jws1a87n.js +5 -0
  93. package/dist/chunks/studio-k2sz13zg.js +5 -0
  94. package/dist/chunks/studio-k3m4jkdk.js +5 -0
  95. package/dist/chunks/{studio-r17asgs0.js → studio-kg1abnz0.js} +4 -4
  96. package/dist/chunks/{studio-s0fg2wav.js → studio-kg5ya5r2.js} +4 -4
  97. package/dist/chunks/studio-khwbgrtg.js +5 -0
  98. package/dist/chunks/studio-mx6wzh25.js +5 -0
  99. package/dist/chunks/studio-mxs3ar9t.js +5 -0
  100. package/dist/chunks/studio-nm4szey4.js +5 -0
  101. package/dist/chunks/studio-nqxqf8p2.js +4 -0
  102. package/dist/chunks/{studio-h5tpz5ke.js → studio-pjtntksy.js} +4 -4
  103. package/dist/chunks/studio-qh8berdz.js +5 -0
  104. package/dist/chunks/studio-qn7z4krm.js +5 -0
  105. package/dist/chunks/studio-qyed0ah6.js +5 -0
  106. package/dist/chunks/{studio-x97ay0v4.js → studio-r4bcdwhq.js} +4 -4
  107. package/dist/chunks/{studio-wxdcd42g.js → studio-r7pf0rkv.js} +6 -6
  108. package/dist/chunks/studio-rtfnhrdg.js +5 -0
  109. package/dist/chunks/{studio-fy7jzj9j.js → studio-rzqaz21g.js} +4 -4
  110. package/dist/chunks/studio-s5tzw84t.js +5 -0
  111. package/dist/chunks/{studio-1e8dw2w2.js → studio-sevbc3xv.js} +4 -4
  112. package/dist/chunks/{studio-ewarnzm4.js → studio-sxdb9h3q.js} +6 -6
  113. package/dist/chunks/studio-t51y8v1d.js +5 -0
  114. package/dist/chunks/{studio-k3bcy02e.js → studio-t98vq4r6.js} +4 -4
  115. package/dist/chunks/studio-th7z3fg7.js +5 -0
  116. package/dist/chunks/studio-tsxpqvh0.js +6 -0
  117. package/dist/chunks/{studio-396srjbk.js → studio-v1yascv2.js} +4 -4
  118. package/dist/chunks/{studio-ekgtrndg.js → studio-wc6za3zs.js} +4 -4
  119. package/dist/chunks/{studio-ndskgqef.js → studio-wxq6v5sv.js} +4 -4
  120. package/dist/chunks/{studio-4kzbrr5t.js → studio-y60sts62.js} +3 -3
  121. package/dist/chunks/studio-ybnwymxw.js +5 -0
  122. package/dist/chunks/studio-zndrqawb.js +5 -0
  123. package/dist/chunks/studio-zwaxn5s2.js +5 -0
  124. package/dist/chunks/{tsMode-h6tkz0ea.js → tsMode-f54j66ym.js} +5 -5
  125. package/dist/chunks/workspace-home-x1787wpv.js +4 -0
  126. package/dist/chunks/workspace-zpe1np43.js +4 -0
  127. package/dist/chunks/{ws-client-e951my95.js → ws-client-e8kj684q.js} +3 -3
  128. package/dist/manifest.json +76 -75
  129. package/dist/studio.js +83 -81
  130. package/package.json +4 -4
  131. package/src/browse/library-model.ts +56 -23
  132. package/src/commands/context.ts +1 -0
  133. package/src/editor/shortcuts.ts +3 -0
  134. package/src/panels/data-grid.ts +0 -4
  135. package/src/panels/settings-menu.ts +3 -3
  136. package/src/panels/settings-pane.ts +42 -56
  137. package/src/platform-errors.ts +17 -1
  138. package/src/platforms/cloud.ts +14 -0
  139. package/src/publish/publish-changes.ts +136 -4
  140. package/src/services/ai-import-tools.ts +6 -3
  141. package/src/services/ai-models.ts +65 -11
  142. package/src/services/ai-settings.ts +45 -5
  143. package/src/services/document-assistant.ts +5 -2
  144. package/src/services/settings/definitions.ts +19 -0
  145. package/src/settings/contributed-section.ts +16 -7
  146. package/src/settings/css-vars-editor.ts +4 -4
  147. package/src/settings/general-settings.ts +1 -4
  148. package/src/settings/preferences-accounts.ts +2 -2
  149. package/src/settings/preferences-dialog.ts +4 -2
  150. package/src/settings/project-sections.ts +1 -1
  151. package/src/settings/section-registry.ts +5 -4
  152. package/src/settings/settings-document.ts +33 -24
  153. package/src/settings/settings-workspace.ts +205 -0
  154. package/src/shell.ts +31 -6
  155. package/src/surfaces/ai-credentials-form.json +59 -5
  156. package/src/surfaces/ai-credentials-form.ts +15 -2
  157. package/src/surfaces/commandbar.ts +7 -3
  158. package/src/surfaces/settings-contributed.json +4 -4
  159. package/src/surfaces/settings-contributed.ts +2 -2
  160. package/src/surfaces/settings-pane.json +262 -180
  161. package/src/surfaces/settings-pane.ts +63 -14
  162. package/src/types.ts +14 -0
  163. package/src/ui/ai-credentials-form.ts +128 -11
  164. package/src/ui/regions.ts +5 -5
  165. package/src/workspace/tab-workspace.ts +3 -2
  166. package/src/workspace/workspace-home.ts +18 -4
  167. package/src/workspace/workspace-ids.ts +40 -7
  168. package/dist/chunks/adapter-wc5zvg9a.js +0 -4
  169. package/dist/chunks/add-new-flow-snx1s4b0.js +0 -4
  170. package/dist/chunks/cf-account-picker-xyqw0jex.js +0 -4
  171. package/dist/chunks/cf-settings-hp1pjm0n.js +0 -4
  172. package/dist/chunks/components-nkwphrx3.js +0 -4
  173. package/dist/chunks/content-source-6a253wmk.js +0 -4
  174. package/dist/chunks/editor-wqrc68ca.js +0 -4
  175. package/dist/chunks/entry-commands-nef0wrmp.js +0 -4
  176. package/dist/chunks/extension-sections-8ez2rrk8.js +0 -4
  177. package/dist/chunks/file-ops-y3w71k1n.js +0 -4
  178. package/dist/chunks/files-de90apn9.js +0 -4
  179. package/dist/chunks/format-host-zfjz4rzm.js +0 -4
  180. package/dist/chunks/fs-events-77c0mr0x.js +0 -4
  181. package/dist/chunks/grid-open-4n4fjfpf.js +0 -4
  182. package/dist/chunks/iframe-host-na2mywz4.js +0 -4
  183. package/dist/chunks/index-ewmgdb7c.js +0 -4
  184. package/dist/chunks/jx-validate-b309c020.js +0 -4
  185. package/dist/chunks/layers-8rn8y6qv.js +0 -4
  186. package/dist/chunks/layers-panel-eyah7h9z.js +0 -4
  187. package/dist/chunks/lit-html-wyqw04y2.js +0 -4
  188. package/dist/chunks/media-library-gs3np47y.js +0 -4
  189. package/dist/chunks/media-picker-tt7dk4tb.js +0 -4
  190. package/dist/chunks/monaco-buffer-wsf6cmnq.js +0 -4
  191. package/dist/chunks/notify-qwst108r.js +0 -4
  192. package/dist/chunks/parse-n8dfxnqf.js +0 -4
  193. package/dist/chunks/platform-c16mqvmp.js +0 -4
  194. package/dist/chunks/publish-panel-d34bhvwj.js +0 -4
  195. package/dist/chunks/publish-workspace-2yr60mt4.js +0 -4
  196. package/dist/chunks/quick-search-e6xsdv0d.js +0 -4
  197. package/dist/chunks/references-gsj6trdv.js +0 -4
  198. package/dist/chunks/right-panel-33jv1knc.js +0 -4
  199. package/dist/chunks/source-control-bj9bejjv.js +0 -4
  200. package/dist/chunks/studio-03mqh8ax.js +0 -5
  201. package/dist/chunks/studio-0ydxv2mr.js +0 -7
  202. package/dist/chunks/studio-2hsqbq7b.js +0 -7
  203. package/dist/chunks/studio-2j86y34y.js +0 -6
  204. package/dist/chunks/studio-2ks6ka4g.js +0 -5
  205. package/dist/chunks/studio-34jrz12k.js +0 -5
  206. package/dist/chunks/studio-35vrtpdq.js +0 -5
  207. package/dist/chunks/studio-38g0jdz7.js +0 -5
  208. package/dist/chunks/studio-3zge48z4.js +0 -7
  209. package/dist/chunks/studio-45gyxpcq.js +0 -5
  210. package/dist/chunks/studio-5wzdy1jc.js +0 -16
  211. package/dist/chunks/studio-732mw0g8.js +0 -5
  212. package/dist/chunks/studio-771k2n12.js +0 -5
  213. package/dist/chunks/studio-88bmt9rh.js +0 -5
  214. package/dist/chunks/studio-c1pxszat.js +0 -5
  215. package/dist/chunks/studio-cpkamjdn.js +0 -14
  216. package/dist/chunks/studio-d038kqbs.js +0 -5
  217. package/dist/chunks/studio-d83m0wyq.js +0 -5
  218. package/dist/chunks/studio-dbxv7w4s.js +0 -5
  219. package/dist/chunks/studio-de1s2x9v.js +0 -5
  220. package/dist/chunks/studio-epenqmvk.js +0 -7
  221. package/dist/chunks/studio-et2q4s8n.js +0 -5
  222. package/dist/chunks/studio-h7weqf2q.js +0 -5
  223. package/dist/chunks/studio-hwejq03c.js +0 -5
  224. package/dist/chunks/studio-k3a8rp3d.js +0 -5
  225. package/dist/chunks/studio-p1n2xjda.js +0 -5
  226. package/dist/chunks/studio-pgm83a2f.js +0 -6
  227. package/dist/chunks/studio-pqqq6v85.js +0 -5
  228. package/dist/chunks/studio-psh8mwnw.js +0 -4
  229. package/dist/chunks/studio-rzg8hnkf.js +0 -5
  230. package/dist/chunks/studio-tyw7tk0x.js +0 -5
  231. package/dist/chunks/studio-v9ywz2se.js +0 -6
  232. package/dist/chunks/studio-vzgmyt0v.js +0 -5
  233. package/dist/chunks/studio-x08a3m0a.js +0 -5
  234. package/dist/chunks/studio-x3mjr5cc.js +0 -5
  235. package/dist/chunks/studio-xh683qzy.js +0 -5
  236. package/dist/chunks/studio-yx8hzqja.js +0 -5
  237. package/dist/chunks/studio-zg16eeq1.js +0 -5
  238. package/dist/chunks/studio-zgx7svjd.js +0 -9
  239. package/dist/chunks/studio-zkq3r91p.js +0 -5
  240. package/dist/chunks/workspace-home-zkkrxq3r.js +0 -4
  241. package/dist/chunks/workspace-tdqqjqaa.js +0 -4
  242. package/src/settings/settings-dialog.ts +0 -192
  243. package/src/surfaces/settings-dialog.json +0 -51
  244. package/src/surfaces/settings-dialog.ts +0 -169
@@ -27,7 +27,7 @@
27
27
 
28
28
  import { clearGithubToken, githubTokenLocation, githubTokenStored } from "../github/github-auth";
29
29
  import { clearCfConnection, getCfAccountId, getCfToken } from "../services/cf-settings";
30
- import { clearAiProvider, getBaseUrl, hasOpenAiKey } from "../services/ai-settings";
30
+ import { clearAiProvider, getAiProvider, getBaseUrl, hasOpenAiKey } from "../services/ai-settings";
31
31
  import { preferredModel, resetModelCache } from "../services/ai-models";
32
32
  import { getPlatform, hasPlatform } from "../platform";
33
33
  import { getAccountEntries } from "../account-status";
@@ -292,7 +292,7 @@ export function listAccounts(): AccountRecord[] {
292
292
  id: "ai",
293
293
  label: "AI provider",
294
294
  detail: hasOpenAiKey()
295
- ? `Key stored. Model ${model}${endpoint ? ` via ${endpoint}` : ""}.`
295
+ ? `${getAiProvider() === "anthropic" ? "Anthropic key" : "Key"} stored. Model ${model}${endpoint ? ` via ${endpoint}` : ""}.`
296
296
  : "No key stored. The Assistant section is where one is entered.",
297
297
  connected: hasOpenAiKey(),
298
298
  revoke: clearAiProvider,
@@ -143,8 +143,10 @@ function sectionShown(): void {
143
143
  const credsForm = createAiCredentialsForm({
144
144
  /* A sentence rather than a template: the form is a document now, and a document renders text
145
145
  rather than another module's markup. */
146
- intro:
147
- "Any OpenAI-compatible key works. Stored locally on this machine; sent only to the Studio proxy (never to a third party except your chosen endpoint).",
146
+ intro: (offersProviders) =>
147
+ offersProviders
148
+ ? "An Anthropic key for Claude, or any OpenAI-compatible key. Stored locally on this machine; sent only to the Studio proxy (never to a third party except your chosen provider)."
149
+ : "Any OpenAI-compatible key works. Stored locally on this machine; sent only to the Studio proxy (never to a third party except your chosen endpoint).",
148
150
  // Both buttons keep the sheet open: Preferences is a place, not a wizard step, and the previous
149
151
  // Dialog's habit of vanishing on Save is what made "did that take?" unanswerable.
150
152
  onCancel: repaint,
@@ -13,7 +13,7 @@
13
13
  *
14
14
  * **Raw JSON does not open a second editor.** It shows what is on disk and hands the author to the
15
15
  * Code editor over the `project.json` tab (§17.1): the host says how, through `editAsCode`, because
16
- * the section sits in the settings dialog and the tab it opens is the Styles workspace's.
16
+ * the section sits in the Settings workspace and the tab it opens is the Styles workspace's.
17
17
  *
18
18
  * **Deploy is no longer a Project Settings section.** Hosting moved to the Publish workspace
19
19
  * (§17.1's Moved footnote), which hosts `renderDeploySection`; the renderer is unchanged.
@@ -3,10 +3,11 @@
3
3
  *
4
4
  * `registerSettingsSection` is a **preserved public contract** (specs/extensions.md §9.1): an
5
5
  * extension's `project` class declares a `$studio.settings` block and `settings/extension-sections`
6
- * turns it into one of these records. Project Settings has moved host twice — a modal, then a
7
- * document in the pane, and now a dialog again (§17.1) — and the registry moved with neither,
8
- * because a section is a contribution to CONFIGURATION, not to whatever draws it. Splitting it out
9
- * is what lets the host be replaced without the contribution point being touched.
6
+ * turns it into one of these records. Project Settings has moved host three times — a modal, then a
7
+ * document in the pane, then a dialog again, and now a workspace of its own (§17.1, §22.9) — and
8
+ * the registry moved with none of them, because a section is a contribution to CONFIGURATION, not
9
+ * to whatever draws it. Splitting it out is what lets the host be replaced without the contribution
10
+ * point being touched.
10
11
  *
11
12
  * **This module holds no renderer and imports nothing.** That is load-bearing rather than tidy: the
12
13
  * gear menu (`panels/settings-menu.ts`) enumerates these sections from the top bar, so anything
@@ -3,18 +3,18 @@
3
3
  * Project Settings: the built-in sections, the `project.json` tab's editors, and the two commands
4
4
  * that reach them.
5
5
  *
6
- * **Settings is a dialog; the configuration DOCUMENT is still a tab.** `project.json` is a real Tab
7
- * under the transaction log with one write chokepoint (`tabs/project-config.ts`, §17.1), and it has
8
- * three editors, the Styles workspace's three views: Elements (`stylebook`), Variables (`tokens`,
9
- * `style/variables-view.ts`) and Code (`source`). Project Settings is neither of them: it is a
10
- * dialog in the dialog layer (`./settings-dialog.ts`) hosting the section list, reachable from
11
- * every workspace, and every section in it writes through the same chokepoint the tab's editors do,
12
- * so the file has one history whichever door changed it.
6
+ * **Settings is a workspace; the configuration DOCUMENT is still a tab.** `project.json` is a real
7
+ * Tab under the transaction log with one write chokepoint (`tabs/project-config.ts`, §17.1), and it
8
+ * has three editors, the Styles workspace's three views: Elements (`stylebook`), Variables
9
+ * (`tokens`, `style/variables-view.ts`) and Code (`source`). Project Settings is neither of them:
10
+ * it is the Settings workspace, whose home hosts the section list (`./settings-workspace.ts`),
11
+ * reached from every workspace, and every section in it writes through the same chokepoint the
12
+ * tab's editors do, so the file has one history whichever door changed it.
13
13
  *
14
14
  * **`settings.open` did not change what it means.** It still means "reach project settings,
15
15
  * optionally at a named section and entry", its args schema still declares `section` and `entry`,
16
- * and a key nobody registered still fails the call. What changed is the `run`: a dialog instead of
17
- * a tab mode.
16
+ * and a key nobody registered still fails the call. What changed is the `run`: a workspace switch
17
+ * instead of a dialog.
18
18
  *
19
19
  * @docs studio/projects/settings
20
20
  */
@@ -31,7 +31,8 @@ import { notify } from "../services/notify";
31
31
  import { projectState } from "../state";
32
32
  import { PROJECT_CONFIG_PATH } from "../tabs/tab";
33
33
  import { lendProjectConfigLayout } from "../tabs/project-config";
34
- import { focusPane, openTab, workspace } from "../workspace/workspace";
34
+ import { focusPane, openTab, paneOfTab, workspace } from "../workspace/workspace";
35
+ import { workspaceHomeTabId } from "../workspace/workspace-ids";
35
36
  import {
36
37
  notifySettingsDocument,
37
38
  registerSettingsSection,
@@ -47,7 +48,7 @@ import { renderGeneralSettings } from "./general-settings";
47
48
  import { renderHeadEditor } from "./head-editor";
48
49
  import { renderLocalesSection } from "./locales-section";
49
50
  import { selectContributedEntry } from "./contributed-section";
50
- import { closeSettingsDialog, openSettingsDialog } from "./settings-dialog";
51
+ import { focusSettingsHome } from "./settings-workspace";
51
52
  // The NAME and the wire value, kept apart by that module on purpose — see its docstring.
52
53
  import {
53
54
  PROJECT_STYLES_TITLE,
@@ -79,7 +80,7 @@ const PROJECT_TAB_MODES = STYLES_TAB_MODES;
79
80
 
80
81
  Two sections that used to be here are not, and their renderers are not deleted: CSS Variables
81
82
  (`./css-vars-editor.ts`) belongs to the Styles workspace, and Deploy (`renderDeploySection`) to
82
- the Publish workspace. The dialog's Moved footnote says where each went.
83
+ the Publish workspace. Settings' Moved footnote says where each went.
83
84
 
84
85
  `icon` is a KEY into the kit's glyph manifest (`@jxsuite/ui/icons`), not a tag: `studio.md`
85
86
  §13.5 is about exactly this distinction, and a key with no row draws nothing and says nothing.
@@ -156,9 +157,9 @@ registerSettingsSection({
156
157
  render: renderDependenciesEditor,
157
158
  });
158
159
  /*
159
- * Raw JSON's "Edit as code" leaves the dialog: the Code editor is the `project.json` tab's, which is
160
- * the Styles workspace's, so the dialog closes and the tab opens in its source editor. Activating
161
- * it is what switches the workspace (`workspace/tab-workspace.ts`).
160
+ * Raw JSON's "Edit as code" leaves Settings: the Code editor is the `project.json` tab's, which is
161
+ * the Styles workspace's, so the tab opens in its source editor, and activating it is what switches
162
+ * the workspace (`workspace/tab-workspace.ts`).
162
163
  */
163
164
  registerSettingsSection({
164
165
  group: "advanced",
@@ -169,7 +170,6 @@ registerSettingsSection({
169
170
  render: (container) => {
170
171
  renderRawJsonSection(container, {
171
172
  editAsCode: () => {
172
- closeSettingsDialog();
173
173
  showSettingsDocument(SOURCE_MODE);
174
174
  },
175
175
  });
@@ -271,12 +271,13 @@ function revealTab(tab: Tab): Tab {
271
271
  /**
272
272
  * Reach Project Settings, optionally at a named section.
273
273
  *
274
- * Opens the dialog on the requested section (or on the one it last showed, which the registry
275
- * remembers for the session), then awaits the contributed-section sync before returning, so a
276
- * caller that then asks whether a section exists is asking the complete registry. The sync is
277
- * COALESCED (`extension-sections.ts`): `refreshExtensionUi` fires the same function on project
278
- * activation and after every `project.json` write, and a second, interleaved run could otherwise
279
- * take the caller's section down between the promise resolving and the frame being drawn.
274
+ * Switches to the Settings workspace on the requested section (or on the one it last showed, which
275
+ * the registry remembers for the session), puts the keyboard on that section's row, then awaits the
276
+ * contributed-section sync before returning, so a caller that then asks whether a section exists is
277
+ * asking the complete registry. The sync is COALESCED (`extension-sections.ts`):
278
+ * `refreshExtensionUi` fires the same function on project activation and after every `project.json`
279
+ * write, and a second, interleaved run could otherwise take the caller's section down between the
280
+ * promise resolving and the frame being drawn.
280
281
  *
281
282
  * @param {string} [section]
282
283
  */
@@ -284,7 +285,15 @@ export async function openProjectSettings(section?: string): Promise<void> {
284
285
  if (section !== undefined) {
285
286
  setSettingsSection(section);
286
287
  }
287
- void openSettingsDialog();
288
+ const registry = activeRegistry();
289
+ if (!registry) {
290
+ throw new Error('command "settings.open" needs the window\'s command registry');
291
+ }
292
+ await registry.run("view.setWorkspace", { workspace: "settings" });
293
+ const paneId = paneOfTab(workspaceHomeTabId("settings"))?.id;
294
+ if (paneId !== undefined) {
295
+ void focusSettingsHome(paneId);
296
+ }
288
297
  try {
289
298
  const { syncExtensionSettingsSections } = await import("./extension-sections");
290
299
  await syncExtensionSettingsSections();
@@ -318,7 +327,7 @@ export async function settingsSectionsReady(): Promise<void> {
318
327
  * step are the same call.
319
328
  *
320
329
  * The validation is deliberately asynchronous. Extension-contributed sections (`connections`,
321
- * `data`, `content`) register a tick after the dialog opens, so refusing synchronously would reject
330
+ * `data`, `content`) register a tick after Settings opens, so refusing synchronously would reject
322
331
  * three legitimate section ids; the command opens, awaits the sync, and only then decides whether
323
332
  * the caller named something real. A wrong key therefore fails the call instead of rendering an
324
333
  * empty body.
@@ -0,0 +1,205 @@
1
+ /// <reference lib="dom" />
2
+ /**
3
+ * Project Settings as a workspace (§17.1, §22.9): the home that hosts the settings editor, mounted
4
+ * into a pane's stage, and the two ways out of it.
5
+ *
6
+ * Project Settings was a dialog, and before that the `project.json` tab under a `settings` editor
7
+ * mode. The dialog was reachable from every workspace, but a dialog is a box of fixed size over a
8
+ * window that has a whole stage to spare, and the map-layout sections (Content types, Data tables,
9
+ * Connections) are two columns of their own inside the editor's two columns: four columns in 900px.
10
+ * A workspace gives the editor the stage, and keeps what the dialog was for: Settings is reached
11
+ * from every workspace through its links (the gear menu, ⌘⇧, and every `settings.open` deep link),
12
+ * and **Back** returns to the workspace it was opened from, which the top bar cannot say because it
13
+ * draws no tab for Settings.
14
+ *
15
+ * **Per pane**, for the reason `publish/publish-workspace.ts` gives: the home is a tab, so two
16
+ * panes can each show it. Each mount is the editor, built when the home is drawn and taken down
17
+ * when the pane's stage moves on (a switch to another workspace, a project closing). A visit is
18
+ * therefore a fresh editor: a half-typed entry or a failed save's error from the last visit, or
19
+ * from another project's, does not come back. **Which section shows is the registry's**, not this
20
+ * module's (`settings/section-registry.ts`), and the registry outlives every mount, which is what
21
+ * makes Settings reopen where the author left it for the rest of the session.
22
+ *
23
+ * @docs studio/projects/settings
24
+ */
25
+
26
+ import { activeRegistry } from "../commands/active-registry";
27
+ import { runActiveReported } from "../commands/run-reported";
28
+ import { mountSettingsPane } from "../panels/settings-pane";
29
+ import { previousWorkspace } from "../shell";
30
+ import { paneRegion } from "../ui/regions";
31
+ import { DEFAULT_WORKSPACE, workspaceInfo } from "../workspace/workspace-ids";
32
+ import type { CanvasSurface } from "../canvas/canvas-surface";
33
+ import type { SettingsPaneMount, SettingsPaneOptions } from "../panels/settings-pane";
34
+ import type { PublishTab } from "../publish/publish-ids";
35
+ import type { InspectorTabId } from "../shell";
36
+ import type { StylesView } from "../style/project-styles";
37
+ import type { Tab } from "../tabs/tab";
38
+ import type { WorkspaceId } from "../workspace/workspace-ids";
39
+
40
+ /** The region part the home is stamped with, under its pane: `pane.<id>/settings`. */
41
+ export const SETTINGS_REGION_PART = "settings";
42
+
43
+ /** The region of the Settings home in `paneId`. */
44
+ export function settingsRegion(paneId: string): string {
45
+ return paneRegion(paneId, SETTINGS_REGION_PART);
46
+ }
47
+
48
+ /** One pane's mount: the editor, and which tab and stage it was drawn for. */
49
+ interface PaneMount {
50
+ pane: SettingsPaneMount;
51
+ tabId: string;
52
+ wrap: HTMLElement;
53
+ }
54
+
55
+ const _panes = new Map<string, PaneMount>();
56
+
57
+ /** Panes whose next mount takes the keyboard: Settings was asked for before its home was drawn. */
58
+ const _focusOnMount = new Set<string>();
59
+
60
+ /**
61
+ * Where Back goes: the workspace entered before Settings, or Design when there was none (a session
62
+ * that was restored into Settings).
63
+ */
64
+ export function returnWorkspace(): WorkspaceId {
65
+ const previous = previousWorkspace();
66
+ return previous === null || previous === "settings" ? DEFAULT_WORKSPACE : previous;
67
+ }
68
+
69
+ /**
70
+ * Leave Settings for the workspace it was opened from.
71
+ *
72
+ * A refusal (the switch is gated on an open project) is filed in Problems under this surface's name
73
+ * instead of being dropped.
74
+ */
75
+ export async function leaveSettings(): Promise<void> {
76
+ await runActiveReported("view.setWorkspace", { workspace: returnWorkspace() }, "Settings");
77
+ }
78
+
79
+ /**
80
+ * Leave Settings for the workspace something moved to: the Moved footnote's buttons.
81
+ *
82
+ * A row that names an Inspector tab ("Head and SEO defaults → Page tab") also shows the Inspector
83
+ * on that tab, AFTER the switch, because entering a workspace applies its frame preset and would
84
+ * otherwise put the dock back as the preset has it. The tab is a document-level view, so with no
85
+ * document open there is no tab to show and asking would only file a refusal; the dock is still
86
+ * opened, which is where the tab appears once one is.
87
+ *
88
+ * A row that names a Styles view ("Colors and fonts → Styles › Variables") runs `styles.open` with
89
+ * that view instead, which is the same workspace switch followed by the view, and a row that names
90
+ * a Publish sub-tab runs `publish.open` on it.
91
+ *
92
+ * @param {WorkspaceId} workspace
93
+ * @param {InspectorTabId} [inspector]
94
+ * @param {StylesView} [view]
95
+ * @param {PublishTab} [publishTab]
96
+ * @returns {Promise<void>} Settles once every command the row runs has settled
97
+ */
98
+ export async function openMoved(
99
+ workspace: WorkspaceId,
100
+ inspector?: InspectorTabId,
101
+ view?: StylesView,
102
+ publishTab?: PublishTab,
103
+ ): Promise<void> {
104
+ if (view !== undefined) {
105
+ await runActiveReported("styles.open", { view }, "Settings");
106
+ return;
107
+ }
108
+ // Hosting is a Publish sub-tab, not the workspace's first: the button says where it lands.
109
+ if (publishTab !== undefined) {
110
+ await runActiveReported("publish.open", { tab: publishTab }, "Settings");
111
+ return;
112
+ }
113
+ await runActiveReported("view.setWorkspace", { workspace }, "Settings");
114
+ if (inspector === undefined) {
115
+ return;
116
+ }
117
+ await runActiveReported("view.setRightPanel", { open: true }, "Settings");
118
+ if (activeRegistry()?.isEnabled("view.setRightTab") === true) {
119
+ await runActiveReported("view.setRightTab", { tab: inspector }, "Settings");
120
+ }
121
+ }
122
+
123
+ /** Whether this tab's home is already mounted in this pane and still in the document. */
124
+ export function settingsWorkspaceMounted(paneId: string, tab: Tab): boolean {
125
+ const mount = _panes.get(paneId);
126
+ return (
127
+ mount !== undefined && mount.tabId === tab.id && mount.wrap.isConnected && mount.pane.attached()
128
+ );
129
+ }
130
+
131
+ /** Tear one pane's home down (mode change, tab switch, project close). Idempotent. */
132
+ export function detachSettingsWorkspace(paneId: string): void {
133
+ const mount = _panes.get(paneId);
134
+ if (!mount) {
135
+ return;
136
+ }
137
+ _panes.delete(paneId);
138
+ mount.pane.dispose();
139
+ }
140
+
141
+ /** Put the keyboard on the current section's row once the editor has drawn it. */
142
+ async function focusWhenReady(paneId: string, mount: PaneMount): Promise<void> {
143
+ await mount.pane.ready;
144
+ if (_panes.get(paneId) === mount) {
145
+ await mount.pane.focusActive();
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Give the keyboard to the Settings home in `paneId`, on the current section's row: what opening
151
+ * Settings does, so an author who asked for Advanced › Raw JSON is on Raw JSON rather than on the
152
+ * first group's tab stop. Served now when the home is standing in the pane, or by its next mount
153
+ * there.
154
+ *
155
+ * @param {string} paneId
156
+ * @returns {Promise<void>} Settles once the keyboard has moved, or once the request is queued
157
+ */
158
+ export async function focusSettingsHome(paneId: string): Promise<void> {
159
+ const mount = _panes.get(paneId);
160
+ if (mount?.wrap.isConnected === true && mount.pane.attached()) {
161
+ await focusWhenReady(paneId, mount);
162
+ return;
163
+ }
164
+ _focusOnMount.add(paneId);
165
+ }
166
+
167
+ /**
168
+ * What the home's controls do, the same in every pane: Back and the Moved buttons are workspace
169
+ * switches, window-level commands that act wherever the author is, and no fact about the pane the
170
+ * home is drawn in goes into them. Declared here rather than in {@link renderSettingsWorkspace} so
171
+ * nothing that consults the window's state is reached from a function holding a pane.
172
+ */
173
+ const HOME_ACTIONS: Omit<SettingsPaneOptions, "region"> = {
174
+ back: () => {
175
+ void leaveSettings();
176
+ },
177
+ backLabel: () => `Back to ${workspaceInfo(returnWorkspace()).title}`,
178
+ openMoved: (workspace, inspector, view, publishTab) => {
179
+ void openMoved(workspace, inspector, view, publishTab);
180
+ },
181
+ };
182
+
183
+ /**
184
+ * Draw the Settings home into a pane's stage. It reads the pane's id and its stage, and nothing
185
+ * else of the canvas surface.
186
+ *
187
+ * @param {Pick<CanvasSurface, "paneId" | "wrap">} surface
188
+ * @param {Tab} tab
189
+ */
190
+ export function renderSettingsWorkspace(
191
+ surface: Pick<CanvasSurface, "paneId" | "wrap">,
192
+ tab: Tab,
193
+ ): void {
194
+ const { paneId, wrap } = surface;
195
+ if (settingsWorkspaceMounted(paneId, tab)) {
196
+ return;
197
+ }
198
+ detachSettingsWorkspace(paneId);
199
+ const pane = mountSettingsPane(wrap, { ...HOME_ACTIONS, region: settingsRegion(paneId) });
200
+ const mount: PaneMount = { pane, tabId: tab.id, wrap };
201
+ _panes.set(paneId, mount);
202
+ if (_focusOnMount.delete(paneId)) {
203
+ void focusWhenReady(paneId, mount);
204
+ }
205
+ }
package/src/shell.ts CHANGED
@@ -45,8 +45,8 @@ import type { AnyCommand, CommandRegistry } from "./commands/registry";
45
45
  import {
46
46
  DEFAULT_WORKSPACE,
47
47
  isWorkspaceId,
48
+ SWITCHER_WORKSPACES,
48
49
  WORKSPACE_IDS,
49
- WORKSPACES,
50
50
  } from "./workspace/workspace-ids";
51
51
  import type { WorkspaceId } from "./workspace/workspace-ids";
52
52
  import { DEFAULT_PUBLISH_DESTINATION, DEFAULT_PUBLISH_TAB } from "./publish/publish-ids";
@@ -416,7 +416,7 @@ export interface WorkspacePreset {
416
416
  }
417
417
 
418
418
  /**
419
- * The five presets.
419
+ * The six presets.
420
420
  *
421
421
  * - **Design** draws the rail and both side docks: the canvas, its outline and its inspector.
422
422
  * - **Content** opens the Navigator on the Collections panel and collapses the Inspector, drawing it
@@ -427,8 +427,10 @@ export interface WorkspacePreset {
427
427
  * schemes), and the Inspector on its Style tab only while the Elements view is on screen: that
428
428
  * view selects an element default on a canvas, and the Inspector is where it is styled. Variables
429
429
  * is edited in the view itself, and Code has no canvas, so both leave the pane its width.
430
+ * - **Settings** collapses both side docks, as Media and Publish do: its home is the settings editor,
431
+ * whose section list is its own navigator.
430
432
  *
431
- * Only Design and Content draw a tab strip; the other three draw their home surface alone.
433
+ * Only Design and Content draw a tab strip; the other four draw their home surface alone.
432
434
  */
433
435
  export const WORKSPACE_PRESETS: Readonly<Record<WorkspaceId, WorkspacePreset>> = {
434
436
  content: {
@@ -459,6 +461,13 @@ export const WORKSPACE_PRESETS: Readonly<Record<WorkspaceId, WorkspacePreset>> =
459
461
  rail: false,
460
462
  tabStrip: false,
461
463
  },
464
+ settings: {
465
+ id: "settings",
466
+ inspector: { open: false },
467
+ navigator: { open: false },
468
+ rail: false,
469
+ tabStrip: false,
470
+ },
462
471
  styles: {
463
472
  id: "styles",
464
473
  inspector: { open: false, tab: "style", whileWriting: true },
@@ -833,6 +842,19 @@ let _workspaceRoot: string | null | undefined;
833
842
  */
834
843
  let _panels: Partial<Record<WorkspaceId, string>> = {};
835
844
 
845
+ /**
846
+ * The workspace on screen before the one that is, in this window, or `null` before the first
847
+ * switch. Not persisted, for the reason {@link _panels} is not. Project Settings' **Close** returns
848
+ * here (`settings/settings-workspace.ts`), because Settings is visited from somewhere rather than
849
+ * worked in, and the top bar has no tab to say where that was.
850
+ */
851
+ let _previous: WorkspaceId | null = null;
852
+
853
+ /** The workspace entered before the one on screen, or `null` when there was none this session. */
854
+ export function previousWorkspace(): WorkspaceId | null {
855
+ return _previous;
856
+ }
857
+
836
858
  /** The raw record for `root`, or `{}` for absent, corrupt or hand-edited storage. */
837
859
  function rawPersistedProject(root: string | null): JsonObject {
838
860
  if (!root) {
@@ -949,6 +971,7 @@ export function syncProjectWorkspace(root: string | null): void {
949
971
  }
950
972
  _workspaceRoot = root;
951
973
  _panels = {};
974
+ _previous = null;
952
975
  shell.workspace = persistedWorkspace(root);
953
976
  if (root === null) {
954
977
  /* No frame to apply, but the Navigator is still on screen beside the rail: a panel only the
@@ -1020,6 +1043,7 @@ export function enterWorkspace(id: WorkspaceId, deps?: WorkspaceDeps): boolean {
1020
1043
  if (!WORKSPACE_PRESETS[shell.workspace].navigator.panel) {
1021
1044
  _panels[shell.workspace] = shell.leftTab;
1022
1045
  }
1046
+ _previous = shell.workspace;
1023
1047
  shell.workspace = id;
1024
1048
  applyFrame(id, deps);
1025
1049
  persistDocks();
@@ -1580,8 +1604,9 @@ export function shellViewCommands(deps: ShellCommandDeps): AnyCommand[] {
1580
1604
  title: "Show Assistant",
1581
1605
  },
1582
1606
  // ── Workspaces ───────────────────────────────────────────────────────────
1583
- // One SETTER, and one chord record per workspace. A chord cannot carry an argument, so ⌘1–5
1584
- // Are five records that each run the setter's own path rather than five implementations of it.
1607
+ /* One SETTER, and one chord record per switcher workspace. A chord cannot carry an argument, so
1608
+ ⌘1–5 are five records that each run the setter's own path rather than five implementations
1609
+ of it. Settings has none: `settings.open` is its record, with its own chord (⌘⇧,). */
1585
1610
  {
1586
1611
  args: argsSchema({
1587
1612
  workspace: enumProperty(WORKSPACE_IDS, "Which workspace to show."),
@@ -1603,7 +1628,7 @@ export function shellViewCommands(deps: ShellCommandDeps): AnyCommand[] {
1603
1628
  },
1604
1629
  title: "Switch Workspace",
1605
1630
  },
1606
- ...WORKSPACES.map((entry, index): AnyCommand => ({
1631
+ ...SWITCHER_WORKSPACES.map((entry, index): AnyCommand => ({
1607
1632
  category: "View",
1608
1633
  id: `workspace.focus.${entry.id}`,
1609
1634
  keybinding: `mod+${index + 1}`,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://jxsuite.com/schema/v1",
3
3
  "$id": "StudioAiCredentialsForm",
4
- "$description": "The AI provider credentials form every gate embeds below the keyless offer: a key, a model, an endpoint, and the two buttons that keep or abandon them. Everything here is a projection — `ui/ai-credentials-form.ts` keeps the drafts, decides what a fetch is sent with, what Save actually persists and whether there is anything to Save or Cancel at all. This document never reads a setting and never calls the proxy.",
4
+ "$description": "The AI provider credentials form every gate embeds below the keyless offer: a provider family (only where the backend reaches more than one), a key, a model, an endpoint, and the two buttons that keep or abandon them. Everything here is a projection — `ui/ai-credentials-form.ts` keeps the drafts, decides what a fetch is sent with, what Save actually persists and whether there is anything to Save or Cancel at all. This document never reads a setting and never calls the proxy.",
5
5
  "tagName": "div",
6
6
  "attributes": {
7
7
  "part": "ai-creds-form",
@@ -32,7 +32,7 @@
32
32
  "fontWeight": "500",
33
33
  "fontSize": "var(--jx-text-sm)"
34
34
  },
35
- "& [part=\"key\"], & [part=\"model\"], & [part=\"endpoint\"]": {
35
+ "& [part=\"provider\"], & [part=\"key\"], & [part=\"model\"], & [part=\"endpoint\"]": {
36
36
  "$description": "Every control takes the column's width; the kit's fields are inline-flex by default.",
37
37
  "width": "100%"
38
38
  },
@@ -73,6 +73,54 @@
73
73
  },
74
74
  "textContent": "${state.intro}"
75
75
  },
76
+ {
77
+ "$description": "The family the key belongs to (specs/ai.md §2.6), drawn only where it is a choice: the backend's models route lists more than the OpenAI-compatible family, or the stored or drafted family is already another one, so a reader can always switch back. A backend that lists nothing reaches the OpenAI-compatible family alone and forwards every key there, so offering Claude to it would send a Claude key to OpenAI. Above the key because it decides what the key is.",
78
+ "tagName": "div",
79
+ "attributes": {
80
+ "part": "provider-slot",
81
+ "role": "none"
82
+ },
83
+ "style": {
84
+ "display": "contents"
85
+ },
86
+ "$switch": {
87
+ "$ref": "#/state/showProvider"
88
+ },
89
+ "cases": {
90
+ "true": {
91
+ "tagName": "jx-select",
92
+ "attributes": {
93
+ "part": "provider"
94
+ },
95
+ "$props": {
96
+ "label": "Provider",
97
+ "size": "sm",
98
+ "options": {
99
+ "$ref": "#/state/providerOptions"
100
+ },
101
+ "value": {
102
+ "$ref": "#/state/providerDraft"
103
+ }
104
+ },
105
+ "onchange": {
106
+ "$prototype": "Function",
107
+ "body": [
108
+ {
109
+ "operator": "call",
110
+ "target": {
111
+ "$ref": "#/state/setProvider"
112
+ },
113
+ "value": [
114
+ {
115
+ "$ref": "event#/target/value"
116
+ }
117
+ ]
118
+ }
119
+ ]
120
+ }
121
+ }
122
+ }
123
+ },
76
124
  {
77
125
  "tagName": "jx-textfield",
78
126
  "attributes": {
@@ -82,7 +130,9 @@
82
130
  "label": "AI provider key",
83
131
  "type": "password",
84
132
  "size": "sm",
85
- "placeholder": "sk-… or any compatible key",
133
+ "placeholder": {
134
+ "$ref": "#/state/keyPlaceholder"
135
+ },
86
136
  "value": {
87
137
  "$ref": "#/state/keyDraft"
88
138
  }
@@ -120,7 +170,9 @@
120
170
  "$props": {
121
171
  "label": "Model ID",
122
172
  "size": "sm",
123
- "placeholder": "Model ID (e.g. gpt-4o, claude-sonnet-4-20250514, etc.)",
173
+ "placeholder": {
174
+ "$ref": "#/state/modelPlaceholder"
175
+ },
124
176
  "allowsCustomValue": true,
125
177
  "value": {
126
178
  "$ref": "#/state/modelDraft"
@@ -231,7 +283,9 @@
231
283
  "$props": {
232
284
  "label": "Endpoint",
233
285
  "size": "sm",
234
- "placeholder": "Endpoint (optional, e.g. http://localhost:11434/v1)",
286
+ "placeholder": {
287
+ "$ref": "#/state/endpointPlaceholder"
288
+ },
235
289
  "value": {
236
290
  "$ref": "#/state/baseUrlDraft"
237
291
  }
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * The AI provider credentials form as a mounted document.
4
4
  *
5
- * `ui/ai-credentials-form.ts` is the flow — it holds the three drafts, decides which credentials a
5
+ * `ui/ai-credentials-form.ts` is the flow — it holds the drafts, decides which credentials a
6
6
  * listing is made with, what Save persists and re-reads, and whether there is anything to commit —
7
7
  * and this is the surface it draws into.
8
8
  *
@@ -37,10 +37,22 @@ registerSurface("ai-credentials-form", credentialsDoc);
37
37
  export interface AiCredentialsView {
38
38
  /** The line under the title: where the key goes, and where it does not. */
39
39
  intro: string;
40
- /** The three drafts, exactly as the flow holds them. */
40
+ /** The drafts, exactly as the flow holds them. */
41
+ providerDraft: string;
41
42
  keyDraft: string;
42
43
  modelDraft: string;
43
44
  baseUrlDraft: string;
45
+ /**
46
+ * Whether the provider picker is drawn: the backend reaches another family, or the drafted or
47
+ * stored family already is one (specs/ai.md §2.6).
48
+ */
49
+ showProvider: boolean;
50
+ /** The families the picker offers. */
51
+ providerOptions: SurfaceOption[];
52
+ /** What each field says while it is empty; they follow the drafted family. */
53
+ keyPlaceholder: string;
54
+ modelPlaceholder: string;
55
+ endpointPlaceholder: string;
44
56
  /** The catalogue the last successful listing returned; empty draws no chevron on the field. */
45
57
  models: SurfaceOption[];
46
58
  /** A listing is in flight: the button says so and refuses a second one. */
@@ -59,6 +71,7 @@ export interface AiCredentialsView {
59
71
 
60
72
  /** Everything the reader can do here. */
61
73
  export interface AiCredentialsActions {
74
+ setProvider: (value: string) => void;
62
75
  setKey: (value: string) => void;
63
76
  setModel: (value: string) => void;
64
77
  setBaseUrl: (value: string) => void;
@@ -57,7 +57,7 @@ import {
57
57
  openSettingsMenu,
58
58
  SETTINGS_MENU_PLACEMENT,
59
59
  } from "../panels/settings-menu";
60
- import { WORKSPACES } from "../workspace/workspace-ids";
60
+ import { SWITCHER_WORKSPACES } from "../workspace/workspace-ids";
61
61
  import type { WorkspaceId } from "../workspace/workspace-ids";
62
62
  import commandbarDoc from "./commandbar.json";
63
63
  import type { PresenceProjection } from "../collab/presence-chips";
@@ -584,7 +584,11 @@ function projectSegments(tab: Tab | null): SegmentProjection[] {
584
584
  }
585
585
 
586
586
  /**
587
- * `Design · Content · Media · Styles · Publish`: the five workspaces, as a tablist.
587
+ * `Design · Content · Media · Styles · Publish`: the five switcher workspaces, as a tablist.
588
+ *
589
+ * Project Settings is a workspace too, and is not here: it is reached from its links (the gear
590
+ * menu's row, ⌘⇧, and every `settings.open` deep link), so while it is on screen no tab is selected
591
+ * and any of these five leaves it (studio.md §22.2).
588
592
  *
589
593
  * Each tab RUNS `view.setWorkspace`, so the click, ⌘1–5, the palette row and a script all take the
590
594
  * same path, and the selected tab is `shell.workspace` rather than a second record of it. Publish
@@ -598,7 +602,7 @@ function projectWorkspaces(registry: CommandRegistry | null): WorkspaceTabProjec
598
602
  return [];
599
603
  }
600
604
  const dirty = shell.git.status?.files.length ?? 0;
601
- return WORKSPACES.map((entry) => {
605
+ return SWITCHER_WORKSPACES.map((entry) => {
602
606
  const badge = entry.id === "publish" && dirty > 0 ? String(dirty) : "";
603
607
  const chord = registry.keymap.formatBinding(`workspace.focus.${entry.id}`);
604
608
  const named = chord ? `${entry.title} (${chord})` : entry.title;