@jxsuite/studio 10.1.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 (239) hide show
  1. package/dist/chunks/{2020-mkb0qa6p.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-f92ey3kp.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-r22jsqht.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-pxwwf00y.js → index-vr006kbt.js} +3 -3
  21. package/dist/chunks/{javascript-pzqryby6.js → javascript-z1g3s9fd.js} +2 -2
  22. package/dist/chunks/{jsonMode-tcnyxyap.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-bqpge64f.js → monaco-binding-v42w9c1v.js} +3 -3
  30. package/dist/chunks/monaco-buffer-een2ea0v.js +4 -0
  31. package/dist/chunks/{monaco-setup-1mkab762.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-m9vkhahq.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-qyb5bebs.js → studio-0q5jwddh.js} +7 -7
  44. package/dist/chunks/{studio-cvz0e00f.js → studio-0w2dr0q8.js} +4 -4
  45. package/dist/chunks/{studio-eesyjg7h.js → studio-1tzpds97.js} +5 -5
  46. package/dist/chunks/{studio-a3s6frde.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-dy2xtt7a.js → studio-399a25za.js} +4 -4
  50. package/dist/chunks/studio-399xknkb.js +6 -0
  51. package/dist/chunks/{studio-jh6t5qz4.js → studio-43y04qck.js} +4 -4
  52. package/dist/chunks/{studio-skynbac3.js → studio-4g8yb6jn.js} +2 -2
  53. package/dist/chunks/{studio-jw0q5j6k.js → studio-4jhw38b2.js} +4 -4
  54. package/dist/chunks/{studio-4smcnv33.js → studio-4ncmwqw5.js} +4 -4
  55. package/dist/chunks/studio-5atwjgkj.js +7 -0
  56. package/dist/chunks/{studio-vcz63n42.js → studio-5frqk7c0.js} +4 -4
  57. package/dist/chunks/{studio-hsr3k06v.js → studio-646nn2ys.js} +5 -5
  58. package/dist/chunks/{studio-ch8gt8vt.js → studio-6cwj6xrx.js} +4 -4
  59. package/dist/chunks/studio-6hzdjfv0.js +5 -0
  60. package/dist/chunks/{studio-9cp96v90.js → studio-71m92v9s.js} +7 -7
  61. package/dist/chunks/{studio-brdp85q7.js → studio-75kcdbsk.js} +4 -4
  62. package/dist/chunks/{studio-6vwvshw8.js → studio-78pywnp2.js} +5 -5
  63. package/dist/chunks/{studio-ggm7zf70.js → studio-86r8hp7r.js} +5 -5
  64. package/dist/chunks/studio-886t6ztg.js +5 -0
  65. package/dist/chunks/{studio-5dmgmyw4.js → studio-8wnvtr4z.js} +4 -4
  66. package/dist/chunks/studio-8z66jk4z.js +5 -0
  67. package/dist/chunks/{studio-x2metr13.js → studio-9rbe2xpw.js} +3 -3
  68. package/dist/chunks/{studio-8qejv8yf.js → studio-awxwjawq.js} +4 -4
  69. package/dist/chunks/studio-b2qpt0xy.js +14 -0
  70. package/dist/chunks/{studio-9rcbvksg.js → studio-bvs9kpst.js} +7 -7
  71. package/dist/chunks/studio-c71806xz.js +7 -0
  72. package/dist/chunks/{studio-vjhej0w5.js → studio-cbwks69x.js} +140 -140
  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-4nkm4404.js → studio-ea74s8c1.js} +4 -4
  79. package/dist/chunks/{studio-37twdaf7.js → studio-efdxjwyp.js} +34 -34
  80. package/dist/chunks/{studio-pmzg17fj.js → studio-egmw2837.js} +4 -4
  81. package/dist/chunks/{studio-8jb31dcf.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-t3j8htp7.js → studio-gj98m8gy.js} +5 -5
  86. package/dist/chunks/{studio-cgb97rwv.js → studio-hasbgjah.js} +4 -4
  87. package/dist/chunks/studio-hhp4x8xf.js +6 -0
  88. package/dist/chunks/{studio-848rk4p1.js → studio-hz5n7x27.js} +4 -4
  89. package/dist/chunks/{studio-0g2sm01j.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-7vb997ff.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-07k0geap.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-5e7p1vgd.js → studio-r4bcdwhq.js} +4 -4
  107. package/dist/chunks/{studio-mwnmzx7g.js → studio-r7pf0rkv.js} +6 -6
  108. package/dist/chunks/studio-rtfnhrdg.js +5 -0
  109. package/dist/chunks/{studio-zacvfm31.js → studio-rzqaz21g.js} +4 -4
  110. package/dist/chunks/studio-s5tzw84t.js +5 -0
  111. package/dist/chunks/{studio-nf0pqpbj.js → studio-sevbc3xv.js} +4 -4
  112. package/dist/chunks/{studio-va0e63sk.js → studio-sxdb9h3q.js} +6 -6
  113. package/dist/chunks/studio-t51y8v1d.js +5 -0
  114. package/dist/chunks/{studio-pd9dsqpq.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-7823f0g8.js → studio-v1yascv2.js} +4 -4
  118. package/dist/chunks/{studio-tpy3rdxn.js → studio-wc6za3zs.js} +4 -4
  119. package/dist/chunks/{studio-5m0w815d.js → studio-wxq6v5sv.js} +4 -4
  120. package/dist/chunks/{studio-ahf3cspq.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-6vwhy6ty.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-zcxkfgmf.js → ws-client-e8kj684q.js} +3 -3
  128. package/dist/manifest.json +76 -75
  129. package/dist/studio.js +81 -82
  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/services/ai-import-tools.ts +6 -3
  138. package/src/services/ai-models.ts +65 -11
  139. package/src/services/ai-settings.ts +45 -5
  140. package/src/services/document-assistant.ts +5 -2
  141. package/src/services/settings/definitions.ts +19 -0
  142. package/src/settings/contributed-section.ts +16 -7
  143. package/src/settings/css-vars-editor.ts +4 -4
  144. package/src/settings/general-settings.ts +1 -4
  145. package/src/settings/preferences-accounts.ts +2 -2
  146. package/src/settings/preferences-dialog.ts +4 -2
  147. package/src/settings/project-sections.ts +1 -1
  148. package/src/settings/section-registry.ts +5 -4
  149. package/src/settings/settings-document.ts +33 -24
  150. package/src/settings/settings-workspace.ts +205 -0
  151. package/src/shell.ts +31 -6
  152. package/src/surfaces/ai-credentials-form.json +59 -5
  153. package/src/surfaces/ai-credentials-form.ts +15 -2
  154. package/src/surfaces/commandbar.ts +7 -3
  155. package/src/surfaces/settings-contributed.json +4 -4
  156. package/src/surfaces/settings-contributed.ts +2 -2
  157. package/src/surfaces/settings-pane.json +262 -180
  158. package/src/surfaces/settings-pane.ts +63 -14
  159. package/src/ui/ai-credentials-form.ts +128 -11
  160. package/src/ui/regions.ts +5 -5
  161. package/src/workspace/tab-workspace.ts +3 -2
  162. package/src/workspace/workspace-home.ts +18 -4
  163. package/src/workspace/workspace-ids.ts +40 -7
  164. package/dist/chunks/adapter-xcpsmecx.js +0 -4
  165. package/dist/chunks/add-new-flow-ptmhmzr9.js +0 -4
  166. package/dist/chunks/cf-account-picker-z1sgwpwd.js +0 -4
  167. package/dist/chunks/cf-settings-xqvhxxe9.js +0 -4
  168. package/dist/chunks/components-whsc81a7.js +0 -4
  169. package/dist/chunks/content-source-jwtxg78w.js +0 -4
  170. package/dist/chunks/editor-zdqdpq1x.js +0 -4
  171. package/dist/chunks/entry-commands-w8p2fbpt.js +0 -4
  172. package/dist/chunks/extension-sections-y2p1te2r.js +0 -4
  173. package/dist/chunks/file-ops-13ab4swj.js +0 -4
  174. package/dist/chunks/files-zbc8p6v1.js +0 -4
  175. package/dist/chunks/format-host-zjsbktbx.js +0 -4
  176. package/dist/chunks/fs-events-t6x0hch6.js +0 -4
  177. package/dist/chunks/grid-open-fyzdwpkh.js +0 -4
  178. package/dist/chunks/iframe-host-kbyqc7pt.js +0 -4
  179. package/dist/chunks/index-6nvj89vb.js +0 -4
  180. package/dist/chunks/jx-validate-5sp8a4rm.js +0 -4
  181. package/dist/chunks/layers-panel-qrxj9vr2.js +0 -4
  182. package/dist/chunks/layers-tpzayggx.js +0 -4
  183. package/dist/chunks/lit-html-x6nrxsx3.js +0 -4
  184. package/dist/chunks/media-library-ywppnjrz.js +0 -4
  185. package/dist/chunks/media-picker-etxgc8qq.js +0 -4
  186. package/dist/chunks/monaco-buffer-qrnqhk0b.js +0 -4
  187. package/dist/chunks/notify-kcv38wwe.js +0 -4
  188. package/dist/chunks/parse-df2kkmhc.js +0 -4
  189. package/dist/chunks/platform-dtsysmyd.js +0 -4
  190. package/dist/chunks/publish-panel-evb1rcxt.js +0 -4
  191. package/dist/chunks/publish-workspace-ktk9h536.js +0 -4
  192. package/dist/chunks/quick-search-2f47fxcw.js +0 -4
  193. package/dist/chunks/references-5w6ajjv7.js +0 -4
  194. package/dist/chunks/right-panel-kz0a6byf.js +0 -4
  195. package/dist/chunks/source-control-1phx90zk.js +0 -4
  196. package/dist/chunks/studio-0hr000ba.js +0 -5
  197. package/dist/chunks/studio-1wyp7dt5.js +0 -5
  198. package/dist/chunks/studio-20ap8dds.js +0 -5
  199. package/dist/chunks/studio-291gqh98.js +0 -5
  200. package/dist/chunks/studio-4zqfw5ha.js +0 -5
  201. package/dist/chunks/studio-56egy6vf.js +0 -5
  202. package/dist/chunks/studio-695k95bw.js +0 -5
  203. package/dist/chunks/studio-732mw0g8.js +0 -5
  204. package/dist/chunks/studio-76t560kn.js +0 -6
  205. package/dist/chunks/studio-8486jn4r.js +0 -5
  206. package/dist/chunks/studio-8x8616xd.js +0 -5
  207. package/dist/chunks/studio-bkmv1h3e.js +0 -9
  208. package/dist/chunks/studio-cqf1k4qk.js +0 -5
  209. package/dist/chunks/studio-dbgv4fv9.js +0 -16
  210. package/dist/chunks/studio-et6t8g4m.js +0 -4
  211. package/dist/chunks/studio-f2j9et3h.js +0 -14
  212. package/dist/chunks/studio-fep7hr66.js +0 -7
  213. package/dist/chunks/studio-fyf7j57j.js +0 -5
  214. package/dist/chunks/studio-g5prntab.js +0 -5
  215. package/dist/chunks/studio-ga77mf07.js +0 -5
  216. package/dist/chunks/studio-h7gy12h2.js +0 -5
  217. package/dist/chunks/studio-j933k8nr.js +0 -5
  218. package/dist/chunks/studio-jgbvqcgh.js +0 -5
  219. package/dist/chunks/studio-k4xc5g6h.js +0 -5
  220. package/dist/chunks/studio-p0nf2e1f.js +0 -6
  221. package/dist/chunks/studio-pzfa2vmb.js +0 -5
  222. package/dist/chunks/studio-rf8d6p9f.js +0 -7
  223. package/dist/chunks/studio-rt498t2v.js +0 -5
  224. package/dist/chunks/studio-sd07xva5.js +0 -6
  225. package/dist/chunks/studio-thj81e98.js +0 -7
  226. package/dist/chunks/studio-tyw7tk0x.js +0 -5
  227. package/dist/chunks/studio-vffwe9k7.js +0 -5
  228. package/dist/chunks/studio-vxjap704.js +0 -5
  229. package/dist/chunks/studio-w67vfgy5.js +0 -5
  230. package/dist/chunks/studio-xebavr9s.js +0 -5
  231. package/dist/chunks/studio-yg5n49za.js +0 -5
  232. package/dist/chunks/studio-yhz5b313.js +0 -5
  233. package/dist/chunks/studio-zsjpsw08.js +0 -5
  234. package/dist/chunks/studio-zxm98z2m.js +0 -7
  235. package/dist/chunks/workspace-d65bz1qk.js +0 -4
  236. package/dist/chunks/workspace-home-vbg9b3jh.js +0 -4
  237. package/src/settings/settings-dialog.ts +0 -192
  238. package/src/surfaces/settings-dialog.json +0 -51
  239. package/src/surfaces/settings-dialog.ts +0 -169
@@ -19,6 +19,7 @@
19
19
  * @license MIT
20
20
  */
21
21
 
22
+ import type { AiProviderFamily } from "@jxsuite/ai/streaming-client";
22
23
  import { SETTINGS } from "./settings/definitions";
23
24
  import type { SettingDefinition } from "./settings/definitions";
24
25
  import {
@@ -58,6 +59,29 @@ export function getBaseUrl() {
58
59
  return readStoredSetting(SETTINGS.aiBaseUrl);
59
60
  }
60
61
 
62
+ /**
63
+ * The wire the stored key belongs to (specs/ai.md §2.6): `"anthropic"` for a Claude key, and the
64
+ * OpenAI-compatible default for anything else, including nothing stored.
65
+ *
66
+ * @returns {AiProviderFamily}
67
+ */
68
+ export function getAiProvider(): AiProviderFamily {
69
+ return readStoredSetting(SETTINGS.aiProvider) === "anthropic" ? "anthropic" : "openai-compat";
70
+ }
71
+
72
+ /**
73
+ * The stored key when it is an OpenAI-compatible one, and `""` when it belongs to another family.
74
+ *
75
+ * For the paths that only speak the OpenAI-compatible wire, such as the site importer's component
76
+ * naming: handing them a Claude key would send it to an OpenAI endpoint, which is the one thing a
77
+ * key must never do.
78
+ *
79
+ * @returns {string}
80
+ */
81
+ export function getOpenAiCompatKey(): string {
82
+ return getAiProvider() === "openai-compat" ? getOpenAiKey() : "";
83
+ }
84
+
61
85
  // ─── Model selection ────────────────────────────────────────────────────────
62
86
 
63
87
  /**
@@ -84,11 +108,25 @@ export function setModel(modelId: string) {
84
108
  setSetting(SETTINGS.aiModel, modelId || "");
85
109
  }
86
110
 
87
- /** The provider's three values together — what a credentials form drafts and what Save stores. */
111
+ /**
112
+ * The provider's values together: what a credentials form drafts and what Save stores. `provider`
113
+ * absent is the OpenAI-compatible default, which is what every caller before it meant.
114
+ */
88
115
  export interface AiProvider {
89
116
  apiKey: string;
90
117
  baseUrl: string;
91
118
  model: string;
119
+ provider?: AiProviderFamily;
120
+ }
121
+
122
+ /**
123
+ * A family as the store holds it: `"anthropic"`, or blank for the default.
124
+ *
125
+ * @param {AiProviderFamily | undefined} family
126
+ * @returns {string}
127
+ */
128
+ function storedFamily(family: AiProviderFamily | undefined): string {
129
+ return family === "anthropic" ? "anthropic" : "";
92
130
  }
93
131
 
94
132
  /**
@@ -105,6 +143,7 @@ export function saveAiProvider(provider: AiProvider) {
105
143
  [SETTINGS.aiOpenAiKey, provider.apiKey],
106
144
  [SETTINGS.aiBaseUrl, provider.baseUrl],
107
145
  [SETTINGS.aiModel, provider.model],
146
+ [SETTINGS.aiProvider, storedFamily(provider.provider)],
108
147
  ]);
109
148
  }
110
149
 
@@ -127,13 +166,14 @@ export function saveAiProvider(provider: AiProvider) {
127
166
  * sentence above always claimed to be making.
128
167
  *
129
168
  * @param {AiProvider} provider The drafts.
130
- * @returns {boolean} `true` when at least one of the three would move.
169
+ * @returns {boolean} `true` when at least one of the values would move.
131
170
  */
132
171
  export function aiProviderDiffers(provider: AiProvider): boolean {
133
172
  return (
134
173
  !storedEquals(SETTINGS.aiOpenAiKey, provider.apiKey, getOpenAiKey()) ||
135
174
  !storedEquals(SETTINGS.aiBaseUrl, provider.baseUrl, getBaseUrl()) ||
136
- !storedEquals(SETTINGS.aiModel, provider.model, storedModel())
175
+ !storedEquals(SETTINGS.aiModel, provider.model, storedModel()) ||
176
+ storedFamily(provider.provider) !== storedFamily(getAiProvider())
137
177
  );
138
178
  }
139
179
 
@@ -153,11 +193,11 @@ export function storedEquals(
153
193
  }
154
194
 
155
195
  /**
156
- * Forget the provider entirely — key, endpoint and model.
196
+ * Forget the provider entirely — key, endpoint, model and family.
157
197
  *
158
198
  * The one deletion path, and the one the Accounts list's Disconnect uses. Deliberately all three
159
199
  * together: a key with an orphaned endpoint is a state no surface knows how to describe.
160
200
  */
161
201
  export function clearAiProvider() {
162
- clearSettings([SETTINGS.aiOpenAiKey, SETTINGS.aiBaseUrl, SETTINGS.aiModel]);
202
+ clearSettings([SETTINGS.aiOpenAiKey, SETTINGS.aiBaseUrl, SETTINGS.aiModel, SETTINGS.aiProvider]);
163
203
  }
@@ -34,7 +34,7 @@ import { abortImportRun, resetImportRuns } from "./import-run";
34
34
  import { runAgentLoop } from "./tool-executor";
35
35
  import { buildSystemPrompt } from "./ai-system-prompt";
36
36
  import type { ExtensionCatalogSummary } from "./ai-system-prompt";
37
- import { getBaseUrl, getOpenAiKey } from "./ai-settings";
37
+ import { getAiProvider, getBaseUrl, getOpenAiKey } from "./ai-settings";
38
38
  import { preferredModel } from "./ai-models";
39
39
  import { pruneOrphanToolMessages, trimContext } from "./context-manager";
40
40
  import { renderCheck } from "./render-critic";
@@ -329,8 +329,11 @@ export function createDocumentAssistant() {
329
329
  model: chatState.model,
330
330
  // Sent as X-Api-Key; the proxy falls back to the server's OPENAI_API_KEY when empty.
331
331
  apiKey: getOpenAiKey() || undefined,
332
- // Optional OpenAI-compatible endpoint override; empty uses the proxy default.
332
+ // Optional endpoint override; empty uses the proxy default for the family.
333
333
  baseUrl: getBaseUrl() || undefined,
334
+ /* The family the key belongs to (specs/ai.md §2.6), named only when it is not the default,
335
+ so an OpenAI-compatible send is the one it always was. */
336
+ provider: getAiProvider() === "anthropic" ? "anthropic" : undefined,
334
337
  /* The persisted conversation, so the backend can keep its rounds on one warm prompt cache.
335
338
  Sent to the backend only, which hashes it before any provider sees it (specs/ai.md §2.5). */
336
339
  sessionId,
@@ -60,6 +60,15 @@ function normalizeBaseUrl(raw: string): string {
60
60
  return raw.trim().replace(/\/+$/, "");
61
61
  }
62
62
 
63
+ /**
64
+ * The provider family, stored only when it is not the default: `"anthropic"`, or `""` for the
65
+ * OpenAI-compatible wire every install had before the setting existed. Anything else reads as the
66
+ * default, so a hand-edited value can never send a key to a family nobody chose.
67
+ */
68
+ function normalizeProvider(raw: string): string {
69
+ return raw.trim().toLowerCase() === "anthropic" ? "anthropic" : "";
70
+ }
71
+
63
72
  /** Trim. Credentials are pasted, and a paste carries whitespace. */
64
73
  function trimmed(raw: string): string {
65
74
  return raw.trim();
@@ -91,6 +100,16 @@ export const SETTINGS = {
91
100
  scope: "user",
92
101
  secret: true,
93
102
  },
103
+ /**
104
+ * Which wire the key belongs to (specs/ai.md §2.6). Blank is the OpenAI-compatible default, so an
105
+ * install that predates the setting keeps the provider it already had.
106
+ */
107
+ aiProvider: {
108
+ default: "",
109
+ key: "jx.ai.provider",
110
+ normalize: normalizeProvider,
111
+ scope: "user",
112
+ },
94
113
  /**
95
114
  * `"gpt-4o"` is what a sender falls back to, NOT a stored choice. A surface that writes back must
96
115
  * read through `readStoredSetting`, or it persists a model the user never picked.
@@ -49,7 +49,8 @@ import { resolveContextPointer } from "../services/context-resolver";
49
49
  import { deriveSecretEnvName } from "../services/data-service";
50
50
  import { validateProjectConfig } from "../services/jx-validate";
51
51
  import { notify } from "../services/notify";
52
- import { SETTINGS_DIALOG_REGION } from "../surfaces/settings-dialog";
52
+ import { REGION_ATTR } from "../ui/regions";
53
+ import { settingsRegion } from "./settings-workspace";
53
54
  import { mountContributedSurface } from "../surfaces/settings-contributed";
54
55
 
55
56
  import type { JsonSchema, SchemaFormContext } from "../ui/schema-form";
@@ -371,15 +372,23 @@ function sectionValue(key: string): JsonObject | null {
371
372
  // ─── Per-container mount ──────────────────────────────────────────────────────
372
373
 
373
374
  /**
374
- * A region under the settings dialog, which is where a contributed section is drawn (§17.1): its
375
- * entry rows and its entry editor are named under `overlay.dialog:settings`, so a shot can crop one
375
+ * A region under the Settings home, which is where a contributed section is drawn (§17.1): its
376
+ * entry rows and its entry editor are named under `pane.<id>/settings`, so a shot can crop one
376
377
  * without a selector.
377
378
  *
379
+ * The home is per pane, and a renderer is handed a container and nothing else, so the base is READ
380
+ * off the nearest region around the container, which is the home's own: the id is derived from what
381
+ * is on screen, as every region is (`ui/regions.ts`). A container not yet in a home names the
382
+ * primary pane's.
383
+ *
384
+ * @param {HTMLElement} container The element the section was rendered into
378
385
  * @param {string} part
379
386
  * @returns {string}
380
387
  */
381
- function sectionRegion(part: string): string {
382
- return `${SETTINGS_DIALOG_REGION}/${part}`;
388
+ function sectionRegion(container: HTMLElement, part: string): string {
389
+ const base =
390
+ container.closest(`[${REGION_ATTR}]`)?.getAttribute(REGION_ATTR) ?? settingsRegion("primary");
391
+ return `${base}/${part}`;
383
392
  }
384
393
 
385
394
  /** One mounted section: its document, its two island hosts, and what it was last asked to draw. */
@@ -455,7 +464,7 @@ function project(record: SectionMount): ContributedView {
455
464
  const sectionKey = contribution.key;
456
465
  const title = contribution.title ?? sectionKey;
457
466
  const actionsState = opts.actions ? "slot" : "none";
458
- const editorRegion = sectionRegion("editor");
467
+ const editorRegion = sectionRegion(record.container, "editor");
459
468
  const words = entryWords(contribution.settings.entry?.noun);
460
469
 
461
470
  if ((contribution.settings.layout ?? "form") !== "map") {
@@ -483,7 +492,7 @@ function project(record: SectionMount): ContributedView {
483
492
  editorState: editing ? "editing" : "empty",
484
493
  entries: Object.keys(entries).map((name) => ({
485
494
  key: name,
486
- region: sectionRegion(`entry:${name}`),
495
+ region: sectionRegion(record.container, `entry:${name}`),
487
496
  selected: name === selected,
488
497
  })),
489
498
  entryName: record.echo ?? (editing ? selected : ""),
@@ -386,9 +386,9 @@ function addOverride(container: HTMLElement, token: string, ctx: string): void {
386
386
  * per token are defined.
387
387
  *
388
388
  * Through the command rather than by writing the registry's section: the editor is hosted outside
389
- * Settings (the Styles workspace's Variables view), so selecting a section of a dialog that is not
390
- * open would change nothing the reader can see. A refusal is filed under "Styles", the workspace
391
- * the editor lives in, as the Variables view's own button files it.
389
+ * Settings (the Styles workspace's Variables view), so selecting a section of a workspace that is
390
+ * not on screen would change nothing the reader can see. A refusal is filed under "Styles", the
391
+ * workspace the editor lives in, as the Variables view's own button files it.
392
392
  */
393
393
  function openContextsSection(): void {
394
394
  void runActiveReported("settings.open", { section: "contexts" }, "Styles");
@@ -418,7 +418,7 @@ function actions(container: HTMLElement): CssVarsActions {
418
418
  * appended `'--dark': '(prefers-color-scheme: dark)'` to `$media` without ever using the word
419
419
  * breakpoint, which made this form the fourth and least discoverable definition site for a
420
420
  * map whose other three lived in the wizard, Overview and Properties › Media. It goes there
421
- * through `settings.open`, so the dialog opens on Contexts however this editor is hosted.
421
+ * through `settings.open`, so Settings opens on Contexts however this editor is hosted.
422
422
  */
423
423
  manageContexts: () => {
424
424
  openContextsSection();
@@ -22,7 +22,6 @@ import { projectState } from "../store";
22
22
  import { updateSiteConfig } from "../site-context";
23
23
  import { activeRegistry } from "../commands/active-registry";
24
24
  import { runActiveReported } from "../commands/run-reported";
25
- import { closeSettingsDialog } from "./settings-dialog";
26
25
  import { getPlatform } from "../platform";
27
26
  import { mountOverviewSurface } from "../surfaces/settings-overview";
28
27
  import { previewAssetSrcFor } from "../canvas/asset-refs";
@@ -265,11 +264,9 @@ function actions(container: HTMLElement): OverviewActions {
265
264
  * says how to get there. This used to write `session.ui.canvasMode` here directly — a second
266
265
  * implementation of a capability, `studio-ui-guidelines.md` §12.5's defect in miniature: the
267
266
  * button and the command could disagree about what "open Project Styles" means, and only one of
268
- * them was reachable by name. Overview is drawn in the settings dialog, so the dialog closes
269
- * first: left up, it would cover the workspace the reader just asked for.
267
+ * them was reachable by name.
270
268
  */
271
269
  openStyles: () => {
272
- closeSettingsDialog();
273
270
  void runActiveReported("view.setWorkspace", { workspace: "styles" }, "Settings");
274
271
  },
275
272
  setDescription: (value: string) => {
@@ -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
+ }