@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/studio",
3
- "version": "10.1.0",
3
+ "version": "10.2.0",
4
4
  "description": "Jx Studio — visual builder for Jx documents",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -60,12 +60,12 @@
60
60
  "dependencies": {
61
61
  "@atlaskit/pragmatic-drag-and-drop": "^4.0.0",
62
62
  "@atlaskit/pragmatic-drag-and-drop-hitbox": "^3.0.0",
63
- "@jxsuite/ai": "^0.41.2",
63
+ "@jxsuite/ai": "^0.42.0",
64
64
  "@jxsuite/collab": "^0.11.4",
65
- "@jxsuite/create": "^1.5.1",
65
+ "@jxsuite/create": "^1.5.2",
66
66
  "@jxsuite/formulas": "^0.0.26",
67
67
  "@jxsuite/markup": "^1.0.2",
68
- "@jxsuite/protocol": "^6.1.0",
68
+ "@jxsuite/protocol": "^6.2.0",
69
69
  "@jxsuite/runtime": "^6.0.1",
70
70
  "@jxsuite/schema": "^5.0.0",
71
71
  "@jxsuite/site": "^2.2.2",
@@ -200,44 +200,64 @@ export function entryCollectionOf(path: string, pageExtensions?: readonly string
200
200
  */
201
201
  const SKIP_DIRS = new Set(["node_modules", ".git", ".jx", ".cache"]);
202
202
 
203
+ /** What every branch of one scan shares. */
204
+ interface ScanRun {
205
+ platform: Pick<StudioPlatform, "listDirectory">;
206
+ out: LibraryFile[];
207
+ failures: ScanFailure[];
208
+ i18n: ResolvedI18n | null;
209
+ pageExtensions: readonly string[] | undefined;
210
+ /**
211
+ * Every directory some branch has claimed, the roots included. A collection may live in an
212
+ * ANCESTOR of the project (a site inside the vault it publishes declares `source: "../.."`), and
213
+ * that walk reaches the project's own directories under the same project-relative paths the roots
214
+ * walk: every page was listed twice. A directory is walked by whichever branch claims it first.
215
+ */
216
+ claimed: Set<string>;
217
+ }
218
+
219
+ /**
220
+ * The project root as a listing names it from outside: `""` (relative to itself) or `"."`. A walk
221
+ * from an ancestor stops there; the project's own directories are roots of the scan already, and
222
+ * the rest of the root (`dist/`, `scripts/`) holds no collection's entries.
223
+ */
224
+ function isProjectRoot(path: string): boolean {
225
+ const normalized = normalizeDir(path);
226
+ return normalized === "" || normalized === ".";
227
+ }
228
+
203
229
  /**
204
230
  * Walk one directory tree, collecting files and recording — never swallowing — what it could not
205
231
  * read. A failure at depth is reported against the directory that failed, not the root, so the
206
232
  * Problem names the path the author can act on.
207
233
  */
208
- async function walk(
209
- dir: string,
210
- platform: Pick<StudioPlatform, "listDirectory">,
211
- out: LibraryFile[],
212
- failures: ScanFailure[],
213
- i18n: ResolvedI18n | null,
214
- optional = false,
215
- pageExtensions?: readonly string[],
216
- ): Promise<void> {
234
+ async function walk(dir: string, run: ScanRun, optional = false): Promise<void> {
217
235
  let entries: DirEntry[];
218
236
  try {
219
237
  // Noted: the Library walks every media directory, so it is where most versions are learned.
220
- entries = await notedListing(() => platform.listDirectory(dir));
238
+ entries = await notedListing(() => run.platform.listDirectory(dir));
221
239
  } catch (error) {
222
240
  // A declared collection's folder that does not exist yet is an empty collection, not a fault.
223
241
  if (!optional) {
224
- failures.push({ dir, error: errorMessage(error) });
242
+ run.failures.push({ dir, error: errorMessage(error) });
225
243
  }
226
244
  return;
227
245
  }
228
246
  const subdirectories: string[] = [];
229
247
  for (const entry of entries) {
230
248
  if (entry.type === "directory") {
231
- if (!SKIP_DIRS.has(entry.name)) {
249
+ const key = normalizeDir(entry.path);
250
+ if (!SKIP_DIRS.has(entry.name) && !isProjectRoot(key) && !run.claimed.has(key)) {
251
+ run.claimed.add(key);
232
252
  subdirectories.push(entry.path);
233
253
  }
234
254
  continue;
235
255
  }
236
256
  const ext = extensionOf(entry.name);
237
- out.push({
238
- collection: entryCollectionOf(entry.path, pageExtensions),
257
+ run.out.push({
258
+ collection: entryCollectionOf(entry.path, run.pageExtensions),
239
259
  ext,
240
- locale: servedLocaleOfPath(entry.path, i18n) ?? undefined,
260
+ locale: servedLocaleOfPath(entry.path, run.i18n) ?? undefined,
241
261
  media: MEDIA_EXTENSIONS.has(ext),
242
262
  modified: entry.modified,
243
263
  name: entry.name,
@@ -248,9 +268,7 @@ async function walk(
248
268
  }
249
269
  // Siblings in parallel, depth serialized per branch: the platform counts every call as in-flight,
250
270
  // So `probe.idle()` already knows the scan is running without the Library declaring a source.
251
- await Promise.all(
252
- subdirectories.map((sub) => walk(sub, platform, out, failures, i18n, false, pageExtensions)),
253
- );
271
+ await Promise.all(subdirectories.map((sub) => walk(sub, run)));
254
272
  }
255
273
 
256
274
  /**
@@ -265,16 +283,31 @@ export async function scanLibrary(
265
283
  optional: ReadonlySet<string> = new Set(),
266
284
  pageExtensions?: readonly string[],
267
285
  ): Promise<LibraryScan> {
268
- const files: LibraryFile[] = [];
269
- const failures: ScanFailure[] = [];
270
286
  // Resolved ONCE for the whole walk. `resolveI18n` canonicalizes every declared tag through
271
287
  // `Intl.Locale`, and a project with 300 pages would otherwise pay for that per file.
272
288
  const { i18n } = resolveI18n(projectState?.projectConfig ?? {});
289
+ const run: ScanRun = {
290
+ claimed: new Set(),
291
+ failures: [],
292
+ i18n,
293
+ out: [],
294
+ pageExtensions,
295
+ platform,
296
+ };
297
+ /* Every root is claimed before any walk descends: `map` runs to the end synchronously, and a walk
298
+ claims its subdirectories only after its own listing has been awaited. A root named twice is
299
+ walked once. */
273
300
  await Promise.all(
274
- dirs.map((dir) =>
275
- walk(dir, platform, files, failures, i18n, optional.has(dir), pageExtensions),
276
- ),
301
+ dirs.map((dir) => {
302
+ const key = normalizeDir(dir);
303
+ if (run.claimed.has(key)) {
304
+ return Promise.resolve();
305
+ }
306
+ run.claimed.add(key);
307
+ return walk(dir, run, optional.has(dir));
308
+ }),
277
309
  );
310
+ const { failures, out: files } = run;
278
311
  files.sort((a, b) => a.path.localeCompare(b.path));
279
312
  return { failures, files };
280
313
  }
@@ -107,6 +107,7 @@ const EDITOR_KIND_BY_MODE = new Map<string, EditorKind>([
107
107
  ["content-home", "home"],
108
108
  ["media-home", "home"],
109
109
  ["publish-home", "home"],
110
+ ["settings-home", "home"],
110
111
  /* The Media viewer — `media/media-pane.ts`. An image, a video, a font specimen: files the studio
111
112
  can SHOW but has no document model for, which is why clicking one used to end in "No format
112
113
  class imported for …", advice that means nothing about a PNG. */
@@ -1010,6 +1010,9 @@ const SELF_SCROLLING_MODES: ReadonlySet<string> = new Set([
1010
1010
  "media-home",
1011
1011
  "preview",
1012
1012
  "publish-home",
1013
+ /* Project Settings' home — `settings/settings-workspace.ts`. Two columns, each scrolling its own
1014
+ list, with nothing to zoom. */
1015
+ "settings-home",
1013
1016
  /* The Styles workspace's Variables view — `style/variables-view.ts`. An `overflow-y: auto`
1014
1017
  column over the stage; without it here the wheel panned an artboard that does not exist. */
1015
1018
  "tokens",
@@ -23,7 +23,6 @@ import { mountDataActionsSurface } from "../surfaces/data-actions";
23
23
  import { mountPushPlanSurface } from "../surfaces/push-plan";
24
24
  import { dataSurfaceAvailable, pushSchema, testConnection } from "../services/data-service";
25
25
  import { openGridSourcePicker } from "../grid/grid-open";
26
- import { closeSettingsDialog } from "../settings/settings-dialog";
27
26
  import type { DataActionsSurfaceHandle, DataActionsView } from "../surfaces/data-actions";
28
27
  import type { DialogSurfaceHandle } from "../surfaces/dialog";
29
28
  import type { PushPlanSurfaceHandle, PushPlanView } from "../surfaces/push-plan";
@@ -278,9 +277,6 @@ function paintSectionActions(
278
277
  host,
279
278
  mountDataActionsSurface(host, projectActions(sectionKey, selected), {
280
279
  openGrid: () => {
281
- /* The actions row is drawn in the settings dialog, and a grid opens as a TAB: left up, the
282
- dialog would cover the very tab the reader asked for (§17.1). */
283
- closeSettingsDialog();
284
280
  void openGridSourcePicker();
285
281
  },
286
282
  push: () => {
@@ -15,15 +15,15 @@
15
15
  * a record joins the menu by declaring the placement and leaves by not declaring it. The submenus
16
16
  * are the enumeration of each parent command's own `section` ARGUMENT, read from that argument's
17
17
  * own definition site — which is state the command already validates, not a second vocabulary
18
- * (`studio-ui-guidelines.md` §12.5). Project Settings' submenu is GROUPED as its dialog's nav is
18
+ * (`studio-ui-guidelines.md` §12.5). Project Settings' submenu is GROUPED as its own nav is
19
19
  * (General, Integrations, Advanced), with a divider where the group changes.
20
20
  *
21
21
  * **A parent row runs its own command AND owns a submenu.** The APG menu pattern does not describe
22
22
  * that and Spectrum's stock submenu forbade it outright; the kit's `jx-menu-item` makes it the rule
23
23
  * (`ui.md` §5.1), so the deviation `specs/studio-ui-guidelines.md` §8.4 records is now what the
24
24
  * element does rather than what this file hand-rolls around it. Nothing is unreachable by keyboard:
25
- * Enter runs the row, ArrowRight reaches every child, and every child is also in the settings
26
- * dialog's own nav.
25
+ * Enter runs the row, ArrowRight reaches every child, and every child is also in Project Settings'
26
+ * own nav.
27
27
  *
28
28
  * The menu is the `menu` surface (`surfaces/menu.json`, through `surfaces/menu.ts`). What is left
29
29
  * here is the projection — which records, which sections, what each row runs — and where the panel
@@ -1,26 +1,25 @@
1
1
  /// <reference lib="dom" />
2
2
  /**
3
3
  * The Project Settings editor's flow: the grouped section list, the section body, and the Moved
4
- * footnote, drawn inside the settings dialog (§17.1).
4
+ * footnote, drawn as the Settings workspace's home (§17.1, §22.9).
5
5
  *
6
6
  * Two columns. The list is the section registry in its three groups (General, Integrations,
7
- * Advanced), so an extension's section joins the dialog by registering and leaves by going away;
8
- * the body is whichever section is current, drawn by that section's own renderer.
7
+ * Advanced), so an extension's section joins Settings by registering and leaves by going away; the
8
+ * body is whichever section is current, drawn by that section's own renderer.
9
9
  *
10
10
  * **This file is the FLOW; the markup is a document.** `surfaces/settings-pane.json` owns the two
11
11
  * columns, the lists' ARIA and keyboard, and every value in their style;
12
12
  * `surfaces/settings-pane.ts` mounts it (studio-ui-guidelines.md §6, §9.3), and
13
- * `settings/settings-dialog.ts` owns the dialog it is mounted into. What stays here is the part
13
+ * `settings/settings-workspace.ts` owns the pane it is mounted into. What stays here is the part
14
14
  * that is a decision: which sections there are, which one is current, which renderer gets the body,
15
15
  * and what a renderer's failure says.
16
16
  *
17
17
  * **It imports the registry, never the sections.** The section RENDERERS are registered by
18
18
  * `settings/settings-document.ts`, which the app loads when it registers its commands.
19
19
  *
20
- * Nothing here is stamped with a region of its own. The dialog carries `overlay.dialog:settings`,
21
- * and the entry rows and entry editor of a map-layout section are stamped by
22
- * `settings/contributed-section.ts` under it, as `overlay.dialog:settings/entry:<key>` and
23
- * `overlay.dialog:settings/editor`.
20
+ * The home carries the region its host hands in, `pane.<id>/settings`, and the entry rows and entry
21
+ * editor of a map-layout section are stamped by `settings/contributed-section.ts` under it, as
22
+ * `pane.<id>/settings/entry:<key>` and `pane.<id>/settings/editor`.
24
23
  *
25
24
  * @docs studio/projects/settings
26
25
  */
@@ -41,6 +40,9 @@ import type { WorkspaceId } from "../workspace/workspace-ids";
41
40
  import type { StylesView } from "../style/project-styles";
42
41
  import type { PublishTab } from "../publish/publish-ids";
43
42
 
43
+ /** The home's heading: the workspace's own title, so the two cannot drift apart. */
44
+ const HEADING = workspaceInfo("settings").title;
45
+
44
46
  /** What the body says when the section that is current is registered by nobody. */
45
47
  const NO_SECTIONS = "No settings sections.";
46
48
 
@@ -80,9 +82,15 @@ const MOVED: readonly {
80
82
 
81
83
  /** What the mounted editor is told to do by things only its host can do. */
82
84
  export interface SettingsPaneOptions {
85
+ /** The region the home is stamped with: `pane.<id>/settings`. */
86
+ region: string;
87
+ /** Where the Back control goes, as its text says it ("Back to Design"). Read on every draw. */
88
+ backLabel: () => string;
89
+ /** The Back control: leave Settings for the workspace it was opened from. */
90
+ back: () => void;
83
91
  /**
84
- * A Moved footnote's button: close the dialog and go to the named workspace, and to the named
85
- * Inspector tab or Styles view there when the row names one.
92
+ * A Moved footnote's button: go to the named workspace, and to the named Inspector tab or Styles
93
+ * view there when the row names one.
86
94
  */
87
95
  openMoved: (
88
96
  workspace: WorkspaceId,
@@ -96,15 +104,10 @@ export interface SettingsPaneOptions {
96
104
  export interface SettingsPaneMount {
97
105
  /** Settles once the columns are mounted and the current section has been drawn once. */
98
106
  ready: Promise<void>;
99
- /** Put the keyboard on the current section's row. */
100
- focusActive: () => void;
101
- /**
102
- * Stop following the section registry while the dialog is closed. The editor stays built; a
103
- * section registering or a project closing in the meantime is caught up on by {@link resume}.
104
- */
105
- suspend: () => void;
106
- /** Follow the registry again and redraw the current section: the dialog is being shown again. */
107
- resume: () => void;
107
+ /** Put the keyboard on the current section's row. Settles once it has moved. */
108
+ focusActive: () => Promise<void>;
109
+ /** Whether the home still stands in the stage it was mounted into. */
110
+ attached: () => boolean;
108
111
  /** Take the editor down and unsubscribe. Idempotent. */
109
112
  dispose: () => void;
110
113
  }
@@ -122,14 +125,16 @@ function movedRow(value: string): (typeof MOVED)[number] | null {
122
125
  /**
123
126
  * Everything the document draws, decided here so the document decides nothing.
124
127
  *
128
+ * @param {SettingsPaneOptions} options The host's region and Back control
125
129
  * @param {string} failure The current section's failure sentence, or the empty string
126
130
  * @returns {SettingsPaneValues}
127
131
  */
128
- function values(failure: string): SettingsPaneValues {
132
+ function values(options: SettingsPaneOptions, failure: string): SettingsPaneValues {
129
133
  const active = settingsDocumentSection();
130
134
  const grouped = groupedSettingsSections();
131
135
  return {
132
136
  active,
137
+ backLabel: options.backLabel(),
133
138
  activeTabId: tabIdFor(active),
134
139
  empty: NO_SECTIONS,
135
140
  failure,
@@ -146,6 +151,7 @@ function values(failure: string): SettingsPaneValues {
146
151
  titleId: `settings-group-${group.id}`,
147
152
  })),
148
153
  hasSection: settingsSection(active) !== undefined,
154
+ heading: HEADING,
149
155
  // The section's own sentence when it has one, its group's otherwise.
150
156
  intro:
151
157
  settingsSection(active)?.intro ||
@@ -161,11 +167,12 @@ function values(failure: string): SettingsPaneValues {
161
167
  workspace: row.workspace,
162
168
  };
163
169
  }),
170
+ region: options.region,
164
171
  };
165
172
  }
166
173
 
167
174
  /**
168
- * Mount the editor into `host`, the settings dialog's pane slot, and keep it current.
175
+ * Mount the editor into `host`, a pane's stage, and keep it current until it is disposed.
169
176
  *
170
177
  * It subscribes to the section registry and to the chosen section, so an extension's section
171
178
  * appearing a tick later redraws the nav, and a deep link to an entry inside the section already on
@@ -198,7 +205,10 @@ export function mountSettingsPane(
198
205
  /** Counts renders, so a late rejection from a superseded one cannot overwrite a newer answer. */
199
206
  let generation = 0;
200
207
 
201
- const surface: SettingsPaneHandle = mountSettingsPaneSurface(host, values(failure), {
208
+ const surface: SettingsPaneHandle = mountSettingsPaneSurface(host, values(options, failure), {
209
+ back: () => {
210
+ options.back();
211
+ },
202
212
  openMoved: (workspace: string) => {
203
213
  const target = movedRow(workspace);
204
214
  if (target !== null) {
@@ -217,7 +227,7 @@ export function mountSettingsPane(
217
227
  }
218
228
  const label = settingsSection(key)?.label ?? key;
219
229
  failure = `Could not show ${label}: ${reason}`;
220
- surface.update(values(failure));
230
+ surface.update(values(options, failure));
221
231
  }
222
232
 
223
233
  /**
@@ -237,7 +247,7 @@ export function mountSettingsPane(
237
247
  rendered = null;
238
248
  }
239
249
  if (!island || (!force && rendered === active)) {
240
- surface.update(values(failure));
250
+ surface.update(values(options, failure));
241
251
  return;
242
252
  }
243
253
  /* Read BEFORE the write: whether the section is changing is what decides the host, and
@@ -245,10 +255,10 @@ export function mountSettingsPane(
245
255
  const changed = rendered !== active;
246
256
  rendered = active;
247
257
  failure = "";
248
- surface.update(values(failure));
258
+ surface.update(values(options, failure));
249
259
  const section = settingsSection(active);
250
260
  if (!section) {
251
- /* Every section unregistered at once: a project closing while the dialog is open. The
261
+ /* Every section unregistered at once: a project closing while Settings is on screen. The
252
262
  document says so itself (`hasSection`), and the island is emptied so a renderer's leftovers
253
263
  do not sit under the sentence explaining that there is nothing to draw. */
254
264
  sectionHost = null;
@@ -276,12 +286,9 @@ export function mountSettingsPane(
276
286
  }
277
287
  }
278
288
 
279
- const follow = (): (() => void) =>
280
- onSettingsDocumentChanged(() => {
281
- draw(true);
282
- });
283
- /** The registry subscription, or null while the dialog is closed. */
284
- let off: (() => void) | null = follow();
289
+ const off = onSettingsDocumentChanged(() => {
290
+ draw(true);
291
+ });
285
292
  /* The island does not exist until the mount lands, so the first pass that reaches a section
286
293
  renderer is this one. */
287
294
  const ready = surface.ready.then(() => {
@@ -289,37 +296,16 @@ export function mountSettingsPane(
289
296
  });
290
297
 
291
298
  return {
299
+ attached: () => surface.attached(),
292
300
  dispose() {
293
301
  if (disposed) {
294
302
  return;
295
303
  }
296
304
  disposed = true;
297
- off?.();
298
- off = null;
305
+ off();
299
306
  surface.dispose();
300
307
  },
301
- focusActive: () => {
302
- surface.focusActive();
303
- },
308
+ focusActive: () => surface.focusActive(),
304
309
  ready,
305
- resume() {
306
- if (disposed || off !== null) {
307
- return;
308
- }
309
- off = follow();
310
- /* Redrawn rather than trusted: anything may have moved while it was closed (a section
311
- registered, the project closed or changed, the configuration edited in Styles). Into a
312
- FRESH host, as every open drew into when each one built its own frame: several sections
313
- keep their form state (a half-typed entry, a failed save's error) in a WeakMap keyed on
314
- the host they were given, and an open is a new visit, so none of that may come back from
315
- the last one, or from another project's. Only a redraw WHILE open keeps the host, which is
316
- what keeps the caret in the field being typed in. */
317
- sectionHost = null;
318
- draw(true);
319
- },
320
- suspend() {
321
- off?.();
322
- off = null;
323
- },
324
310
  };
325
311
  }
@@ -27,7 +27,7 @@ import { IMPORT_MAX_PAGES } from "@jxsuite/protocol";
27
27
  import { isBoolean, isNumber, isString } from "@jxsuite/schema/guards";
28
28
  import { getPlatform } from "../platform";
29
29
  import { workspace } from "../workspace/workspace";
30
- import { getBaseUrl, getOpenAiKey } from "./ai-settings";
30
+ import { getAiProvider, getBaseUrl, getOpenAiCompatKey } from "./ai-settings";
31
31
  import { preferredModel } from "./ai-models";
32
32
  import { MAX_IMPORT_DEPTH, clearPendingImportBrief, pendingImportBrief } from "./import-seed";
33
33
  import type { ImportBrief } from "./import-seed";
@@ -431,8 +431,11 @@ export function registerImportTools(
431
431
  };
432
432
  callSignal.addEventListener("abort", stopRun, { once: true });
433
433
 
434
- const apiKey = getOpenAiKey();
435
- const baseUrl = getBaseUrl();
434
+ /* The importer's component naming speaks only the OpenAI-compatible wire, so a Claude key
435
+ and its endpoint are withheld rather than sent to an OpenAI client; the import then names
436
+ components heuristically, as it does with no key at all. */
437
+ const apiKey = getOpenAiCompatKey();
438
+ const baseUrl = getAiProvider() === "openai-compat" ? getBaseUrl() : "";
436
439
  const model = brief?.model || preferredModel();
437
440
 
438
441
  const breakpoints = breakpointPolicyFor(
@@ -10,12 +10,13 @@
10
10
  * @license MIT
11
11
  */
12
12
 
13
- import type { AiCredentials } from "@jxsuite/ai/streaming-client";
13
+ import { AI_PROVIDER_HEADER } from "@jxsuite/ai/streaming-client";
14
+ import type { AiCredentials, AiProviderFamily } from "@jxsuite/ai/streaming-client";
14
15
  import type { AiModelsResponse, OfferedAction } from "@jxsuite/protocol";
15
16
  import { isBoolean, isNumber, isString } from "@jxsuite/schema/guards";
16
17
  import { getPlatform } from "../platform";
17
18
  import { offeredActions } from "../platform-errors";
18
- import { getBaseUrl, getOpenAiKey, hasOpenAiKey, storedModel } from "./ai-settings";
19
+ import { getAiProvider, getBaseUrl, getOpenAiKey, hasOpenAiKey, storedModel } from "./ai-settings";
19
20
  import { SETTINGS } from "./settings/definitions";
20
21
  import { onSettingsChanged } from "./settings/kernel";
21
22
 
@@ -42,19 +43,32 @@ export interface AiModel {
42
43
  */
43
44
  export type { AiCredentials } from "@jxsuite/ai/streaming-client";
44
45
 
46
+ /**
47
+ * The credentials a model list belongs to, and the family they belong to. `provider` absent is the
48
+ * OpenAI-compatible default: the same key and endpoint list different catalogues under different
49
+ * families, so the family is part of what a list was listed under.
50
+ */
51
+ export interface AiConnection extends AiCredentials {
52
+ provider?: AiProviderFamily;
53
+ }
54
+
55
+ /** The model a Claude sender falls back to when neither the user nor the backend chose one. */
56
+ export const ANTHROPIC_DEFAULT_MODEL = "claude-opus-5-5";
57
+
45
58
  /** The credentials the app is configured with right now. */
46
- export function aiConnection(): AiCredentials {
47
- return { apiKey: getOpenAiKey(), baseUrl: getBaseUrl() };
59
+ export function aiConnection(): AiConnection {
60
+ return { apiKey: getOpenAiKey(), baseUrl: getBaseUrl(), provider: getAiProvider() };
48
61
  }
49
62
 
50
63
  /**
51
- * An opaque identity for a credential pair.
64
+ * An opaque identity for a connection.
52
65
  *
53
66
  * NUL-joined rather than concatenated: a key ending in the base URL's first characters would
54
67
  * otherwise be indistinguishable from a shorter key and a longer URL.
55
68
  */
56
- function fingerprint(credentials: AiCredentials): string {
57
- return `${credentials.apiKey}\u0000${credentials.baseUrl}`;
69
+ function fingerprint(credentials: AiConnection): string {
70
+ const family = credentials.provider ?? "openai-compat";
71
+ return `${credentials.apiKey}\u0000${credentials.baseUrl}\u0000${family}`;
58
72
  }
59
73
 
60
74
  let cache: AiModel[] | null = null;
@@ -77,6 +91,8 @@ let proxyCode: AiModelsResponse["code"];
77
91
  let proxyModelsError = "";
78
92
  /** What an `action_required` probe offered; null for every other answer. */
79
93
  let proxyAction: ProxyActionOffer | null = null;
94
+ /** The families the last probe said the chat route reaches; the default alone until one says more. */
95
+ let proxyProviders: AiProviderFamily[] = ["openai-compat"];
80
96
 
81
97
  /** What a managed backend says the assistant needs first, in the backend's own words. */
82
98
  export interface ProxyActionOffer {
@@ -126,6 +142,7 @@ export function resetModelCache() {
126
142
  proxyCode = undefined;
127
143
  proxyModelsError = "";
128
144
  proxyAction = null;
145
+ proxyProviders = ["openai-compat"];
129
146
  /* The probe's result IS the flags above. Clearing them while keeping the settled promise
130
147
  would strand every gate on a permanent "unconfigured, unmanaged" reading — ensureProxyProbe
131
148
  would no-op forever and the managed option would vanish until a full reload. */
@@ -137,6 +154,7 @@ export function resetModelCache() {
137
154
  const CONNECTION_KEYS = new Set<string>([
138
155
  SETTINGS.aiOpenAiKey.key,
139
156
  SETTINGS.aiBaseUrl.key,
157
+ SETTINGS.aiProvider.key,
140
158
  SETTINGS.cfToken.key,
141
159
  SETTINGS.cfAccountId.key,
142
160
  ]);
@@ -151,7 +169,7 @@ onSettingsChanged((keys) => {
151
169
  * The cached list, if it was listed under `credentials`. Null when it was not, or when there is
152
170
  * none — a caller must not show one provider's catalogue for another's key.
153
171
  */
154
- export function cachedModels(credentials: AiCredentials): AiModel[] | null {
172
+ export function cachedModels(credentials: AiConnection): AiModel[] | null {
155
173
  return cache && cacheKey === fingerprint(credentials) ? cache : null;
156
174
  }
157
175
 
@@ -294,6 +312,18 @@ export function proxyActionOffer(): ProxyActionOffer | null {
294
312
  return proxyAction;
295
313
  }
296
314
 
315
+ /**
316
+ * The provider families the backend's chat route reaches with a caller's key, as the last probe
317
+ * reported them. A backend that says nothing reaches the OpenAI-compatible family alone, and a
318
+ * surface **MUST NOT** offer it another: such a backend forwards every key to an OpenAI-compatible
319
+ * endpoint (specs/ai.md §2.6).
320
+ *
321
+ * @returns {readonly AiProviderFamily[]}
322
+ */
323
+ export function backendProviders(): readonly AiProviderFamily[] {
324
+ return proxyProviders;
325
+ }
326
+
297
327
  /** The proxy's preferred model id ("" when it does not declare one). */
298
328
  export function getProxyDefaultModel(): string {
299
329
  return proxyDefaultModel;
@@ -307,13 +337,15 @@ export function getProxyDefaultModel(): string {
307
337
  * that value was fetched, stored and never read — so a user who had chosen nothing got the
308
338
  * hardcoded `"gpt-4o"`, which Workers AI does not serve. Asking the backend what it prefers before
309
339
  * falling back to a name invented here is the difference between working and a 404 on the first
310
- * message.
340
+ * message. The last term follows the family for the same reason: Anthropic serves no `gpt-4o`.
311
341
  *
312
342
  * It lives here rather than in `ai-settings.ts` because only this module knows what the proxy said,
313
343
  * and `ai-settings` must not import it — this module already imports `ai-settings`.
314
344
  */
315
345
  export function preferredModel(): string {
316
- return storedModel() || getProxyDefaultModel() || SETTINGS.aiModel.default;
346
+ const fallback =
347
+ getAiProvider() === "anthropic" ? ANTHROPIC_DEFAULT_MODEL : SETTINGS.aiModel.default;
348
+ return storedModel() || getProxyDefaultModel() || fallback;
317
349
  }
318
350
 
319
351
  /**
@@ -345,6 +377,23 @@ export function siblingRoute(chatUrl: string, segment: string): string {
345
377
  return /^https?:/i.test(chatUrl) ? url.href : `${url.pathname}${url.search}`;
346
378
  }
347
379
 
380
+ /**
381
+ * The families a probe listed, keeping only the ones this client can speak. The answer is the
382
+ * backend's JSON, so an entry its type calls a family may be any string; a probe that lists none is
383
+ * a backend that predates the member, which reaches the default family alone.
384
+ *
385
+ * @param {readonly string[] | undefined} listed - The probe's `providers`, as sent
386
+ * @returns {AiProviderFamily[]}
387
+ */
388
+ function providersOf(listed: readonly string[] | undefined): AiProviderFamily[] {
389
+ if (listed === undefined) {
390
+ return ["openai-compat"];
391
+ }
392
+ return listed.filter((family): family is AiProviderFamily =>
393
+ ["anthropic", "openai-compat"].includes(family),
394
+ );
395
+ }
396
+
348
397
  /**
349
398
  * Fetch the models available through the AI proxy. Returns the cached list when present unless
350
399
  * `force` is set; throws on HTTP/network failure (callers surface the error).
@@ -354,7 +403,7 @@ export function siblingRoute(chatUrl: string, segment: string): string {
354
403
  * @returns {Promise<AiModel[]>}
355
404
  */
356
405
  export async function fetchAvailableModels(
357
- opts: { credentials?: AiCredentials; force?: boolean } = {},
406
+ opts: { credentials?: AiConnection; force?: boolean } = {},
358
407
  ): Promise<AiModel[]> {
359
408
  /* Resolve the credentials BEFORE consulting the cache. Checking the cache first is what let a
360
409
  list fetched under one provider be handed to a caller asking about another. */
@@ -374,6 +423,10 @@ export async function fetchAvailableModels(
374
423
  if (credentials.baseUrl) {
375
424
  headers["X-Api-Base-URL"] = credentials.baseUrl;
376
425
  }
426
+ // The default family says nothing, as the chat request does (`createProxyStreamingClient`).
427
+ if (credentials.provider === "anthropic") {
428
+ headers[AI_PROVIDER_HEADER] = credentials.provider;
429
+ }
377
430
 
378
431
  const resp = await fetch(modelsUrl, { headers });
379
432
  if (!resp.ok) {
@@ -390,6 +443,7 @@ export async function fetchAvailableModels(
390
443
  proxyCode = data.code;
391
444
  proxyModelsError = data.upstreamError !== undefined ? (data.upstreamMessage ?? "") : "";
392
445
  proxyAction = null;
446
+ proxyProviders = providersOf(data.providers);
393
447
  if (data.code === "action_required") {
394
448
  const offer: ProxyActionOffer = { actions: offeredActions(data.actions) };
395
449
  if (isString(data.detail) && data.detail) {