@jxsuite/studio 10.1.0 → 10.3.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.
- package/dist/chunks/{2020-mkb0qa6p.js → 2020-906vx2dq.js} +3 -3
- package/dist/chunks/adapter-hnvad61x.js +4 -0
- package/dist/chunks/add-new-flow-3tkn72ey.js +4 -0
- package/dist/chunks/cf-account-picker-xb8vvt2h.js +4 -0
- package/dist/chunks/cf-settings-mkbcz4z2.js +4 -0
- package/dist/chunks/components-w5zqwva1.js +4 -0
- package/dist/chunks/content-source-1vzxq1ba.js +4 -0
- package/dist/chunks/convert-to-repeater-gwzh9gar.js +4 -0
- package/dist/chunks/editor-wvf5e6j9.js +4 -0
- package/dist/chunks/entry-commands-mx2whrjx.js +4 -0
- package/dist/chunks/extension-sections-338ee35t.js +4 -0
- package/dist/chunks/file-ops-9bxvk59j.js +4 -0
- package/dist/chunks/files-t07yqqej.js +4 -0
- package/dist/chunks/format-host-5ja5gxyk.js +4 -0
- package/dist/chunks/fs-events-3424ezp3.js +4 -0
- package/dist/chunks/grid-open-1mpq9mzb.js +4 -0
- package/dist/chunks/{grid-view-r22jsqht.js → grid-view-w9nfy203.js} +15 -15
- package/dist/chunks/icons-gb967qc8.js +4 -0
- package/dist/chunks/iframe-host-wsg0bn73.js +4 -0
- package/dist/chunks/image-bake-4afr4y6a.js +4 -0
- package/dist/chunks/image-editor-x7t8jbjn.js +4 -0
- package/dist/chunks/index-f9qr7kzh.js +4 -0
- package/dist/chunks/{index-pxwwf00y.js → index-yjrfeks8.js} +3 -3
- package/dist/chunks/{javascript-pzqryby6.js → javascript-hy2mws8v.js} +3 -3
- package/dist/chunks/{jsonMode-tcnyxyap.js → jsonMode-vj30te2h.js} +6 -6
- package/dist/chunks/jx-validate-ttr5ewf0.js +4 -0
- package/dist/chunks/layers-m49nmwvx.js +4 -0
- package/dist/chunks/layers-panel-hvy2e549.js +4 -0
- package/dist/chunks/lit-html-k2mb9k0r.js +4 -0
- package/dist/chunks/media-library-ae67q4at.js +4 -0
- package/dist/chunks/media-open-yp9jm834.js +4 -0
- package/dist/chunks/media-picker-s41ngdfr.js +4 -0
- package/dist/chunks/{monaco-binding-bqpge64f.js → monaco-binding-a51py33c.js} +3 -3
- package/dist/chunks/monaco-buffer-pa0g6ax7.js +4 -0
- package/dist/chunks/{monaco-setup-1mkab762.js → monaco-setup-g9kterhx.js} +3 -3
- package/dist/chunks/notify-bwehrp7q.js +4 -0
- package/dist/chunks/parse-hee7ne9b.js +4 -0
- package/dist/chunks/platform-kv0k65cy.js +4 -0
- package/dist/chunks/publish-panel-gzfxcm0t.js +4 -0
- package/dist/chunks/publish-quick-w3nxd7q4.js +4 -0
- package/dist/chunks/publish-workspace-dppd82tc.js +4 -0
- package/dist/chunks/quick-search-tdxaam55.js +4 -0
- package/dist/chunks/references-vqyr5s9x.js +4 -0
- package/dist/chunks/right-panel-0yrw4cgz.js +4 -0
- package/dist/chunks/source-control-6fk72me2.js +4 -0
- package/dist/chunks/{studio-0g2sm01j.js → studio-0939t1q1.js} +4 -4
- package/dist/chunks/studio-0gvvqj3h.js +5 -0
- package/dist/chunks/studio-0khd8xtx.js +4 -0
- package/dist/chunks/{studio-rf8d6p9f.js → studio-0w3541dg.js} +6 -6
- package/dist/chunks/{studio-9b7177jq.js → studio-0x6f3nk9.js} +4 -4
- package/dist/chunks/studio-1410q5dw.js +5 -0
- package/dist/chunks/{studio-8qejv8yf.js → studio-15hwqgd8.js} +4 -4
- package/dist/chunks/studio-1ks5d9y4.js +5 -0
- package/dist/chunks/{studio-881z1xrx.js → studio-2ggfzrrd.js} +4 -4
- package/dist/chunks/{studio-jw0q5j6k.js → studio-321ym1tt.js} +4 -4
- package/dist/chunks/studio-3c10v2q1.js +5 -0
- package/dist/chunks/{studio-9rcbvksg.js → studio-3na1r48j.js} +8 -8
- package/dist/chunks/{studio-ggm7zf70.js → studio-3q7mhbgx.js} +5 -5
- package/dist/chunks/studio-3qr08sxq.js +4 -0
- package/dist/chunks/studio-3xk9zdgb.js +5 -0
- package/dist/chunks/studio-423tfdte.js +5 -0
- package/dist/chunks/{studio-brdp85q7.js → studio-45mj4jqe.js} +4 -4
- package/dist/chunks/studio-4f7thq0p.js +5 -0
- package/dist/chunks/{studio-w67vfgy5.js → studio-4fnb5648.js} +4 -4
- package/dist/chunks/studio-4zd6an2b.js +5 -0
- package/dist/chunks/studio-53wy9ek3.js +5 -0
- package/dist/chunks/studio-5n193zqd.js +5 -0
- package/dist/chunks/{studio-x2metr13.js → studio-5tet77de.js} +3 -3
- package/dist/chunks/studio-7rrdbvex.js +5 -0
- package/dist/chunks/studio-8kx6m0rg.js +5 -0
- package/dist/chunks/{studio-va0e63sk.js → studio-90zkgne5.js} +7 -7
- package/dist/chunks/studio-9c6cbq2c.js +4 -0
- package/dist/chunks/studio-9dq7rwc7.js +5 -0
- package/dist/chunks/studio-9qx1ywbv.js +9 -0
- package/dist/chunks/studio-a65kbs2e.js +9 -0
- package/dist/chunks/studio-aam8zt8z.js +5 -0
- package/dist/chunks/{studio-8jb31dcf.js → studio-abh9kfdh.js} +4 -4
- package/dist/chunks/studio-b4e0ftkt.js +5 -0
- package/dist/chunks/studio-b68k0z00.js +5 -0
- package/dist/chunks/{studio-dy2xtt7a.js → studio-bct64zmj.js} +4 -4
- package/dist/chunks/studio-bz0b5brj.js +6 -0
- package/dist/chunks/studio-cfnn70h1.js +5 -0
- package/dist/chunks/studio-daksw8kh.js +5 -0
- package/dist/chunks/{studio-mwnmzx7g.js → studio-e7vjyp04.js} +7 -7
- package/dist/chunks/studio-ea6r1zyr.js +5 -0
- package/dist/chunks/studio-egnjpyde.js +9 -0
- package/dist/chunks/studio-ejrypvmq.js +5 -0
- package/dist/chunks/studio-eqcddvc2.js +928 -0
- package/dist/chunks/studio-f8tyeyzt.js +5 -0
- package/dist/chunks/studio-fj6c905s.js +5 -0
- package/dist/chunks/{studio-hsr3k06v.js → studio-fkrr7kgg.js} +5 -5
- package/dist/chunks/studio-fspe7m9t.js +5 -0
- package/dist/chunks/{studio-4zqfw5ha.js → studio-g0hxtcga.js} +4 -4
- package/dist/chunks/studio-g9brbewz.js +5 -0
- package/dist/chunks/studio-ggx3j7nd.js +5 -0
- package/dist/chunks/{studio-jh6t5qz4.js → studio-hfxttkyr.js} +4 -4
- package/dist/chunks/studio-hh394phe.js +5 -0
- package/dist/chunks/{studio-07k0geap.js → studio-j0zf5x2x.js} +4 -4
- package/dist/chunks/studio-jeb7hth3.js +5 -0
- package/dist/chunks/studio-k0xzrse2.js +64 -0
- package/dist/chunks/studio-k1xqrgm5.js +5 -0
- package/dist/chunks/studio-k2evngrj.js +7 -0
- package/dist/chunks/studio-k3we3kqb.js +5 -0
- package/dist/chunks/{studio-nf0pqpbj.js → studio-n9qba0gx.js} +4 -4
- package/dist/chunks/studio-nf8esxxc.js +5 -0
- package/dist/chunks/studio-nn4f9m9c.js +5 -0
- package/dist/chunks/studio-nskxe9q1.js +6 -0
- package/dist/chunks/{studio-4nkm4404.js → studio-nvcn2ykc.js} +4 -4
- package/dist/chunks/studio-nwynxqtb.js +5 -0
- package/dist/chunks/{studio-f2j9et3h.js → studio-nzemfbkk.js} +8 -8
- package/dist/chunks/studio-p2a03pgm.js +5 -0
- package/dist/chunks/{studio-skynbac3.js → studio-pm17wdc4.js} +4 -4
- package/dist/chunks/{studio-9cp96v90.js → studio-q80fg1d6.js} +7 -7
- package/dist/chunks/studio-r050sez0.js +6 -0
- package/dist/chunks/{studio-sd07xva5.js → studio-s7t4vp1d.js} +5 -5
- package/dist/chunks/studio-s9fng7m8.js +7 -0
- package/dist/chunks/studio-sce30r7b.js +4 -0
- package/dist/chunks/{studio-zxm98z2m.js → studio-stahswgb.js} +6 -6
- package/dist/chunks/studio-swedpd82.js +5 -0
- package/dist/chunks/studio-thz5t120.js +5 -0
- package/dist/chunks/studio-tpxcryf2.js +5 -0
- package/dist/chunks/{studio-tpy3rdxn.js → studio-v5wktj6f.js} +4 -4
- package/dist/chunks/{studio-7vb997ff.js → studio-vpevkfxw.js} +4 -4
- package/dist/chunks/studio-waekn290.js +5 -0
- package/dist/chunks/studio-wfct4h6n.js +5 -0
- package/dist/chunks/studio-wgc54sxf.js +5 -0
- package/dist/chunks/studio-wqfaqw4m.js +5 -0
- package/dist/chunks/studio-wse1rs8t.js +7 -0
- package/dist/chunks/studio-x0gsfr1d.js +16 -0
- package/dist/chunks/studio-xpx91fby.js +6 -0
- package/dist/chunks/studio-y1b86ast.js +5 -0
- package/dist/chunks/studio-yrcrncev.js +5 -0
- package/dist/chunks/{studio-zacvfm31.js → studio-yzpskb0j.js} +4 -4
- package/dist/chunks/studio-zfdbmhp3.js +5 -0
- package/dist/chunks/{tsMode-6vwhy6ty.js → tsMode-jg1dtphj.js} +7 -7
- package/dist/chunks/workspace-9btkc8tk.js +4 -0
- package/dist/chunks/workspace-home-qcc4z5ag.js +4 -0
- package/dist/chunks/{ws-client-zcxkfgmf.js → ws-client-arz2qsy9.js} +3 -3
- package/dist/iframe-entry.js +13 -13
- package/dist/manifest.json +83 -77
- package/dist/studio.js +74 -75
- package/package.json +5 -5
- package/src/browse/library-model.ts +56 -23
- package/src/browse/library-pane.ts +44 -29
- package/src/browse/library-preview.ts +6 -2
- package/src/canvas/asset-refs.ts +13 -4
- package/src/canvas/asset-resolve.ts +54 -11
- package/src/canvas/canvas-render.ts +25 -0
- package/src/canvas/iframe-entry.ts +5 -1
- package/src/canvas/iframe-host.ts +16 -6
- package/src/canvas/iframe-protocol.ts +9 -1
- package/src/commands/context.ts +6 -0
- package/src/commands/live-context.ts +5 -2
- package/src/editor/shortcuts.ts +6 -0
- package/src/files/asset-versions.ts +108 -2
- package/src/files/file-ops.ts +11 -1
- package/src/files/files.ts +111 -48
- package/src/files/media-paths.ts +12 -4
- package/src/files/media-upload.ts +195 -21
- package/src/files/new-file-name.ts +39 -1
- package/src/media/image-bake.ts +164 -0
- package/src/media/image-editor.ts +846 -0
- package/src/media/image-format.ts +262 -0
- package/src/media/image-recipe.ts +651 -0
- package/src/media/media-commands.ts +65 -1
- package/src/media/media-library-model.ts +1 -1
- package/src/media/media-library.ts +175 -15
- package/src/media/media-open.ts +156 -3
- package/src/media/media-pane.ts +8 -4
- package/src/panels/data-grid.ts +0 -4
- package/src/panels/pane-context.ts +6 -1
- package/src/panels/settings-menu.ts +3 -3
- package/src/panels/settings-pane.ts +42 -56
- package/src/panels/tab-strip.ts +12 -3
- package/src/services/ai-import-tools.ts +6 -3
- package/src/services/ai-models.ts +65 -11
- package/src/services/ai-settings.ts +45 -5
- package/src/services/document-assistant.ts +5 -2
- package/src/services/settings/definitions.ts +19 -0
- package/src/settings/contributed-section.ts +16 -7
- package/src/settings/css-vars-editor.ts +4 -4
- package/src/settings/general-settings.ts +1 -4
- package/src/settings/preferences-accounts.ts +2 -2
- package/src/settings/preferences-dialog.ts +4 -2
- package/src/settings/project-sections.ts +1 -1
- package/src/settings/section-registry.ts +5 -4
- package/src/settings/settings-document.ts +33 -24
- package/src/settings/settings-workspace.ts +205 -0
- package/src/shell.ts +31 -6
- package/src/studio.ts +13 -2
- package/src/surfaces/ai-credentials-form.json +59 -5
- package/src/surfaces/ai-credentials-form.ts +15 -2
- package/src/surfaces/commandbar.ts +7 -3
- package/src/surfaces/image-editor.json +1390 -0
- package/src/surfaces/image-editor.ts +272 -0
- package/src/surfaces/media-library.json +129 -1
- package/src/surfaces/media-library.ts +17 -2
- package/src/surfaces/settings-contributed.json +4 -4
- package/src/surfaces/settings-contributed.ts +2 -2
- package/src/surfaces/settings-pane.json +262 -180
- package/src/surfaces/settings-pane.ts +63 -14
- package/src/types.ts +2 -1
- package/src/ui/ai-credentials-form.ts +128 -11
- package/src/ui/regions.ts +5 -5
- package/src/workspace/tab-workspace.ts +5 -3
- package/src/workspace/workspace-home.ts +18 -4
- package/src/workspace/workspace-ids.ts +40 -7
- package/src/workspace/workspace-switch.ts +3 -1
- package/src/workspace/workspace.ts +22 -1
- package/dist/chunks/adapter-xcpsmecx.js +0 -4
- package/dist/chunks/add-new-flow-ptmhmzr9.js +0 -4
- package/dist/chunks/cf-account-picker-z1sgwpwd.js +0 -4
- package/dist/chunks/cf-settings-xqvhxxe9.js +0 -4
- package/dist/chunks/components-whsc81a7.js +0 -4
- package/dist/chunks/content-source-jwtxg78w.js +0 -4
- package/dist/chunks/convert-to-repeater-f92ey3kp.js +0 -4
- package/dist/chunks/editor-zdqdpq1x.js +0 -4
- package/dist/chunks/entry-commands-w8p2fbpt.js +0 -4
- package/dist/chunks/extension-sections-y2p1te2r.js +0 -4
- package/dist/chunks/file-ops-13ab4swj.js +0 -4
- package/dist/chunks/files-zbc8p6v1.js +0 -4
- package/dist/chunks/format-host-zjsbktbx.js +0 -4
- package/dist/chunks/fs-events-t6x0hch6.js +0 -4
- package/dist/chunks/grid-open-fyzdwpkh.js +0 -4
- package/dist/chunks/icons-853n6qt3.js +0 -4
- package/dist/chunks/iframe-host-kbyqc7pt.js +0 -4
- package/dist/chunks/index-6nvj89vb.js +0 -4
- package/dist/chunks/jx-validate-5sp8a4rm.js +0 -4
- package/dist/chunks/layers-panel-qrxj9vr2.js +0 -4
- package/dist/chunks/layers-tpzayggx.js +0 -4
- package/dist/chunks/lit-html-x6nrxsx3.js +0 -4
- package/dist/chunks/media-library-ywppnjrz.js +0 -4
- package/dist/chunks/media-picker-etxgc8qq.js +0 -4
- package/dist/chunks/monaco-buffer-qrnqhk0b.js +0 -4
- package/dist/chunks/notify-kcv38wwe.js +0 -4
- package/dist/chunks/parse-df2kkmhc.js +0 -4
- package/dist/chunks/platform-dtsysmyd.js +0 -4
- package/dist/chunks/publish-panel-evb1rcxt.js +0 -4
- package/dist/chunks/publish-quick-m9vkhahq.js +0 -4
- package/dist/chunks/publish-workspace-ktk9h536.js +0 -4
- package/dist/chunks/quick-search-2f47fxcw.js +0 -4
- package/dist/chunks/references-5w6ajjv7.js +0 -4
- package/dist/chunks/right-panel-kz0a6byf.js +0 -4
- package/dist/chunks/source-control-1phx90zk.js +0 -4
- package/dist/chunks/studio-0hr000ba.js +0 -5
- package/dist/chunks/studio-1wyp7dt5.js +0 -5
- package/dist/chunks/studio-20ap8dds.js +0 -5
- package/dist/chunks/studio-291gqh98.js +0 -5
- package/dist/chunks/studio-37twdaf7.js +0 -64
- package/dist/chunks/studio-4smcnv33.js +0 -5
- package/dist/chunks/studio-56egy6vf.js +0 -5
- package/dist/chunks/studio-5dmgmyw4.js +0 -5
- package/dist/chunks/studio-5e7p1vgd.js +0 -5
- package/dist/chunks/studio-5m0w815d.js +0 -5
- package/dist/chunks/studio-695k95bw.js +0 -5
- package/dist/chunks/studio-6vwvshw8.js +0 -6
- package/dist/chunks/studio-732mw0g8.js +0 -5
- package/dist/chunks/studio-76t560kn.js +0 -6
- package/dist/chunks/studio-7823f0g8.js +0 -5
- package/dist/chunks/studio-8486jn4r.js +0 -5
- package/dist/chunks/studio-848rk4p1.js +0 -5
- package/dist/chunks/studio-8x8616xd.js +0 -5
- package/dist/chunks/studio-a3s6frde.js +0 -4
- package/dist/chunks/studio-ahf3cspq.js +0 -4
- package/dist/chunks/studio-bkmv1h3e.js +0 -9
- package/dist/chunks/studio-cgb97rwv.js +0 -5
- package/dist/chunks/studio-ch8gt8vt.js +0 -5
- package/dist/chunks/studio-cqf1k4qk.js +0 -5
- package/dist/chunks/studio-cvz0e00f.js +0 -5
- package/dist/chunks/studio-dbgv4fv9.js +0 -16
- package/dist/chunks/studio-eesyjg7h.js +0 -9
- package/dist/chunks/studio-et6t8g4m.js +0 -4
- package/dist/chunks/studio-fep7hr66.js +0 -7
- package/dist/chunks/studio-fyf7j57j.js +0 -5
- package/dist/chunks/studio-g5prntab.js +0 -5
- package/dist/chunks/studio-ga77mf07.js +0 -5
- package/dist/chunks/studio-h7gy12h2.js +0 -5
- package/dist/chunks/studio-j933k8nr.js +0 -5
- package/dist/chunks/studio-jgbvqcgh.js +0 -5
- package/dist/chunks/studio-k4xc5g6h.js +0 -5
- package/dist/chunks/studio-p0nf2e1f.js +0 -6
- package/dist/chunks/studio-pd9dsqpq.js +0 -5
- package/dist/chunks/studio-pmzg17fj.js +0 -5
- package/dist/chunks/studio-pzfa2vmb.js +0 -5
- package/dist/chunks/studio-qyb5bebs.js +0 -9
- package/dist/chunks/studio-rt498t2v.js +0 -5
- package/dist/chunks/studio-s0fg2wav.js +0 -5
- package/dist/chunks/studio-t3j8htp7.js +0 -7
- package/dist/chunks/studio-thj81e98.js +0 -7
- package/dist/chunks/studio-tyw7tk0x.js +0 -5
- package/dist/chunks/studio-vcz63n42.js +0 -5
- package/dist/chunks/studio-vffwe9k7.js +0 -5
- package/dist/chunks/studio-vjhej0w5.js +0 -928
- package/dist/chunks/studio-vxjap704.js +0 -5
- package/dist/chunks/studio-xebavr9s.js +0 -5
- package/dist/chunks/studio-yg5n49za.js +0 -5
- package/dist/chunks/studio-yhz5b313.js +0 -5
- package/dist/chunks/studio-zsjpsw08.js +0 -5
- package/dist/chunks/workspace-d65bz1qk.js +0 -4
- package/dist/chunks/workspace-home-vbg9b3jh.js +0 -4
- package/src/settings/settings-dialog.ts +0 -192
- package/src/surfaces/settings-dialog.json +0 -51
- package/src/surfaces/settings-dialog.ts +0 -169
package/src/panels/data-grid.ts
CHANGED
|
@@ -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: () => {
|
|
@@ -800,7 +800,12 @@ function editorAxis(tab: Tab): Partial<PaneContextView> {
|
|
|
800
800
|
const current = editorKindOf(tab);
|
|
801
801
|
const kinds = IMPLICIT_EDITOR_KINDS.has(current)
|
|
802
802
|
? [current]
|
|
803
|
-
: editorKindsOf(tab).filter(
|
|
803
|
+
: editorKindsOf(tab).filter(
|
|
804
|
+
(kind) =>
|
|
805
|
+
!IMPLICIT_EDITOR_KINDS.has(kind) &&
|
|
806
|
+
// A collaborator who cannot write is not offered the editor that exists to write (§9.5).
|
|
807
|
+
(kind !== "image" || kind === current || !collabState(tab).readOnly),
|
|
808
|
+
);
|
|
804
809
|
if (kinds.length < 2) {
|
|
805
810
|
// One kind is not a choice. Rendering it as a segmented group would be a control that cannot move —
|
|
806
811
|
// Which is the defect this axis exists to remove, in miniature.
|
|
@@ -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
|
|
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
|
|
26
|
-
*
|
|
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
|
|
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
|
|
8
|
-
*
|
|
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-
|
|
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
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* `settings/
|
|
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:
|
|
85
|
-
*
|
|
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
|
-
|
|
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`,
|
|
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
|
|
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
|
|
280
|
-
|
|
281
|
-
|
|
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
|
}
|
package/src/panels/tab-strip.ts
CHANGED
|
@@ -83,6 +83,7 @@ import { openMenu } from "../surfaces/menu";
|
|
|
83
83
|
import type { MenuHandle, MenuRowProjection } from "../surfaces/menu";
|
|
84
84
|
import { saveFile } from "../files/file-ops";
|
|
85
85
|
import { collabReadOnly } from "../collab/collab-session";
|
|
86
|
+
import { imageEditorDirty } from "../media/image-editor";
|
|
86
87
|
import { collabState } from "../collab/collab-state";
|
|
87
88
|
import { elementAtPoint, rectOf } from "../utils/geometry";
|
|
88
89
|
import { resolveRegion } from "../ui/regions";
|
|
@@ -255,6 +256,8 @@ export function mount(host: HTMLElement) {
|
|
|
255
256
|
// A tab's workspace follows its editor (`project.json` is Styles' in its stylebook only).
|
|
256
257
|
void tab.session.ui.canvasMode;
|
|
257
258
|
void tab.doc.dirty;
|
|
259
|
+
// An image edit is the tab's unsaved work too (§9.5), held beside the document, not in it.
|
|
260
|
+
void imageEditorDirty(tab);
|
|
258
261
|
void tab.documentPath;
|
|
259
262
|
void tab.pinned;
|
|
260
263
|
void tab.preview;
|
|
@@ -689,7 +692,7 @@ function chipView(
|
|
|
689
692
|
const pill = entryDraftPill(tab);
|
|
690
693
|
return [
|
|
691
694
|
{
|
|
692
|
-
dirty: tab.doc.dirty,
|
|
695
|
+
dirty: tab.doc.dirty || imageEditorDirty(tab),
|
|
693
696
|
draft: pill?.draft ?? false,
|
|
694
697
|
index,
|
|
695
698
|
key: id,
|
|
@@ -1336,11 +1339,17 @@ export function tabLabel(tab: Tab): string {
|
|
|
1336
1339
|
* buffer rather than resyncing over a half-typed heading), and that text exists nowhere else.
|
|
1337
1340
|
* {@link tabBufferUnsaved} is that residue, and it is the user's own typing by construction.
|
|
1338
1341
|
*
|
|
1342
|
+
* **And an image tab's unsaved work is not in its document at all.** A media tab's document is a
|
|
1343
|
+
* stub; an Image editor edit (§9.5) is a recipe held beside it, so it is a third source of dirt,
|
|
1344
|
+
* and it takes the residue's side of the peer-count gate for the residue's reason: a recipe has
|
|
1345
|
+
* never been published, mirrored or written, so no peer focused on the path holds a copy of it.
|
|
1346
|
+
*
|
|
1339
1347
|
* @param {Tab} tab
|
|
1340
1348
|
*/
|
|
1341
1349
|
export function shouldWarnOnClose(tab: Tab): boolean {
|
|
1342
1350
|
const residue = tabBufferUnsaved(tab);
|
|
1343
|
-
|
|
1351
|
+
const image = imageEditorDirty(tab);
|
|
1352
|
+
if (!tab.doc.dirty && !residue && !image) {
|
|
1344
1353
|
return false;
|
|
1345
1354
|
}
|
|
1346
1355
|
const state = collabState(tab);
|
|
@@ -1361,7 +1370,7 @@ export function shouldWarnOnClose(tab: Tab): boolean {
|
|
|
1361
1370
|
lock holder is by definition a peer focused on this path, so the busier the room got, the
|
|
1362
1371
|
quieter the close became. Asked before the peer count rather than after, because the peer count
|
|
1363
1372
|
has no jurisdiction over text the room has never been told about. */
|
|
1364
|
-
if (residue) {
|
|
1373
|
+
if (residue || image) {
|
|
1365
1374
|
return true;
|
|
1366
1375
|
}
|
|
1367
1376
|
const peersHere = state.peers.filter((p) => p.state.focusedPath === tab.documentPath);
|
|
@@ -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,
|
|
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
|
-
|
|
435
|
-
|
|
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
|
|
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():
|
|
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
|
|
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:
|
|
57
|
-
|
|
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:
|
|
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
|
-
|
|
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?:
|
|
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) {
|
|
@@ -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
|
-
/**
|
|
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
|
|
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
|
|
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
|
|
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,
|