@jxsuite/studio 10.0.0 → 10.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunks/{2020-j56903f1.js → 2020-bx702wqa.js} +3 -3
- package/dist/chunks/adapter-pp011m5x.js +4 -0
- package/dist/chunks/add-new-flow-ecqmzz76.js +4 -0
- package/dist/chunks/cf-account-picker-c07znkqt.js +4 -0
- package/dist/chunks/cf-settings-0v2bmqy0.js +4 -0
- package/dist/chunks/components-yn0dn2pq.js +4 -0
- package/dist/chunks/content-source-g0j41yy6.js +4 -0
- package/dist/chunks/{convert-to-repeater-fhdwyg14.js → convert-to-repeater-sbjk7tgn.js} +3 -3
- package/dist/chunks/editor-xz02jeh1.js +4 -0
- package/dist/chunks/entry-commands-kmt02crp.js +4 -0
- package/dist/chunks/extension-sections-0rprf12d.js +4 -0
- package/dist/chunks/file-ops-mexj5qed.js +4 -0
- package/dist/chunks/files-b2tcppdd.js +4 -0
- package/dist/chunks/format-host-btsevv0c.js +4 -0
- package/dist/chunks/fs-events-r0apejva.js +4 -0
- package/dist/chunks/grid-open-n2a8xkht.js +4 -0
- package/dist/chunks/{grid-view-ka55z38f.js → grid-view-tghcwrrt.js} +12 -12
- package/dist/chunks/iframe-host-gx0ypyh3.js +4 -0
- package/dist/chunks/index-bs4k9tr6.js +4 -0
- package/dist/chunks/{index-qb98xhse.js → index-vr006kbt.js} +3 -3
- package/dist/chunks/{javascript-ces13wny.js → javascript-z1g3s9fd.js} +2 -2
- package/dist/chunks/{jsonMode-6md16d4v.js → jsonMode-7szwg8c4.js} +5 -5
- package/dist/chunks/jx-validate-ymf10teh.js +4 -0
- package/dist/chunks/layers-p9nfya38.js +4 -0
- package/dist/chunks/layers-panel-tt1hzhhf.js +4 -0
- package/dist/chunks/lit-html-x18n2sfz.js +4 -0
- package/dist/chunks/media-library-0n45gv8r.js +4 -0
- package/dist/chunks/media-picker-s09at95z.js +4 -0
- package/dist/chunks/{monaco-binding-p8j80s4w.js → monaco-binding-v42w9c1v.js} +3 -3
- package/dist/chunks/monaco-buffer-een2ea0v.js +4 -0
- package/dist/chunks/{monaco-setup-wasktzfg.js → monaco-setup-7bffxd6v.js} +3 -3
- package/dist/chunks/notify-qwb8radt.js +4 -0
- package/dist/chunks/parse-v77xtwv7.js +4 -0
- package/dist/chunks/platform-8jnez0wr.js +4 -0
- package/dist/chunks/publish-panel-aysnj611.js +4 -0
- package/dist/chunks/{publish-quick-vr695j26.js → publish-quick-kpbqs9mz.js} +3 -3
- package/dist/chunks/publish-workspace-nd0a8q99.js +4 -0
- package/dist/chunks/quick-search-vg0jkhk2.js +4 -0
- package/dist/chunks/references-a7v6a0e2.js +4 -0
- package/dist/chunks/right-panel-cvk1vxx5.js +4 -0
- package/dist/chunks/source-control-fsqzc6he.js +4 -0
- package/dist/chunks/studio-05jfwbmf.js +5 -0
- package/dist/chunks/{studio-hy4ye0dx.js → studio-0q5jwddh.js} +7 -7
- package/dist/chunks/{studio-jttyqrmh.js → studio-0w2dr0q8.js} +4 -4
- package/dist/chunks/{studio-xkxetm65.js → studio-1tzpds97.js} +5 -5
- package/dist/chunks/{studio-m48v13s3.js → studio-216b27sv.js} +3 -3
- package/dist/chunks/studio-2ejhqns4.js +5 -0
- package/dist/chunks/studio-2r90cts3.js +16 -0
- package/dist/chunks/{studio-h310mbqm.js → studio-399a25za.js} +4 -4
- package/dist/chunks/studio-399xknkb.js +6 -0
- package/dist/chunks/{studio-wxhy1yh7.js → studio-43y04qck.js} +4 -4
- package/dist/chunks/{studio-06j4en5h.js → studio-4g8yb6jn.js} +2 -2
- package/dist/chunks/{studio-jw0q5j6k.js → studio-4jhw38b2.js} +4 -4
- package/dist/chunks/{studio-3v5cagy9.js → studio-4ncmwqw5.js} +4 -4
- package/dist/chunks/studio-5atwjgkj.js +7 -0
- package/dist/chunks/{studio-gr36vnte.js → studio-5frqk7c0.js} +4 -4
- package/dist/chunks/{studio-9dz76yqc.js → studio-646nn2ys.js} +5 -5
- package/dist/chunks/{studio-tj2s0tks.js → studio-6cwj6xrx.js} +4 -4
- package/dist/chunks/studio-6hzdjfv0.js +5 -0
- package/dist/chunks/{studio-hcsba201.js → studio-71m92v9s.js} +7 -7
- package/dist/chunks/{studio-3c8pp7pb.js → studio-75kcdbsk.js} +4 -4
- package/dist/chunks/{studio-gwrhcrss.js → studio-78pywnp2.js} +5 -5
- package/dist/chunks/{studio-9cxmp61c.js → studio-86r8hp7r.js} +5 -5
- package/dist/chunks/studio-886t6ztg.js +5 -0
- package/dist/chunks/studio-8wnvtr4z.js +5 -0
- package/dist/chunks/studio-8z66jk4z.js +5 -0
- package/dist/chunks/{studio-ajw4k8hp.js → studio-9rbe2xpw.js} +3 -3
- package/dist/chunks/{studio-y26hx9re.js → studio-awxwjawq.js} +4 -4
- package/dist/chunks/studio-b2qpt0xy.js +14 -0
- package/dist/chunks/{studio-dx73md4n.js → studio-bvs9kpst.js} +7 -7
- package/dist/chunks/studio-c71806xz.js +7 -0
- package/dist/chunks/{studio-7dzyz9n8.js → studio-cbwks69x.js} +144 -144
- package/dist/chunks/studio-cg1bfyhf.js +9 -0
- package/dist/chunks/studio-cg710sk9.js +5 -0
- package/dist/chunks/studio-cw0wb9qk.js +7 -0
- package/dist/chunks/studio-cyheys7k.js +5 -0
- package/dist/chunks/studio-ea5m9kg5.js +5 -0
- package/dist/chunks/{studio-ry81j4q1.js → studio-ea74s8c1.js} +4 -4
- package/dist/chunks/{studio-xdpz9rpe.js → studio-efdxjwyp.js} +34 -34
- package/dist/chunks/{studio-agyvzfm4.js → studio-egmw2837.js} +4 -4
- package/dist/chunks/{studio-5tzgq6fx.js → studio-er5f383a.js} +4 -4
- package/dist/chunks/studio-f746039f.js +5 -0
- package/dist/chunks/studio-fmdymd9v.js +5 -0
- package/dist/chunks/studio-gcm12z00.js +6 -0
- package/dist/chunks/{studio-vswcpzk7.js → studio-gj98m8gy.js} +5 -5
- package/dist/chunks/{studio-yg3djpnm.js → studio-hasbgjah.js} +4 -4
- package/dist/chunks/studio-hhp4x8xf.js +6 -0
- package/dist/chunks/{studio-3w35qvay.js → studio-hz5n7x27.js} +4 -4
- package/dist/chunks/{studio-7xag4na8.js → studio-j3d2fjqz.js} +4 -4
- package/dist/chunks/studio-jeraxdgv.js +7 -0
- package/dist/chunks/studio-jvgek9xt.js +5 -0
- package/dist/chunks/studio-jws1a87n.js +5 -0
- package/dist/chunks/studio-k2sz13zg.js +5 -0
- package/dist/chunks/studio-k3m4jkdk.js +5 -0
- package/dist/chunks/{studio-r17asgs0.js → studio-kg1abnz0.js} +4 -4
- package/dist/chunks/{studio-s0fg2wav.js → studio-kg5ya5r2.js} +4 -4
- package/dist/chunks/studio-khwbgrtg.js +5 -0
- package/dist/chunks/studio-mx6wzh25.js +5 -0
- package/dist/chunks/studio-mxs3ar9t.js +5 -0
- package/dist/chunks/studio-nm4szey4.js +5 -0
- package/dist/chunks/studio-nqxqf8p2.js +4 -0
- package/dist/chunks/{studio-h5tpz5ke.js → studio-pjtntksy.js} +4 -4
- package/dist/chunks/studio-qh8berdz.js +5 -0
- package/dist/chunks/studio-qn7z4krm.js +5 -0
- package/dist/chunks/studio-qyed0ah6.js +5 -0
- package/dist/chunks/{studio-x97ay0v4.js → studio-r4bcdwhq.js} +4 -4
- package/dist/chunks/{studio-wxdcd42g.js → studio-r7pf0rkv.js} +6 -6
- package/dist/chunks/studio-rtfnhrdg.js +5 -0
- package/dist/chunks/{studio-fy7jzj9j.js → studio-rzqaz21g.js} +4 -4
- package/dist/chunks/studio-s5tzw84t.js +5 -0
- package/dist/chunks/{studio-1e8dw2w2.js → studio-sevbc3xv.js} +4 -4
- package/dist/chunks/{studio-ewarnzm4.js → studio-sxdb9h3q.js} +6 -6
- package/dist/chunks/studio-t51y8v1d.js +5 -0
- package/dist/chunks/{studio-k3bcy02e.js → studio-t98vq4r6.js} +4 -4
- package/dist/chunks/studio-th7z3fg7.js +5 -0
- package/dist/chunks/studio-tsxpqvh0.js +6 -0
- package/dist/chunks/{studio-396srjbk.js → studio-v1yascv2.js} +4 -4
- package/dist/chunks/{studio-ekgtrndg.js → studio-wc6za3zs.js} +4 -4
- package/dist/chunks/{studio-ndskgqef.js → studio-wxq6v5sv.js} +4 -4
- package/dist/chunks/{studio-4kzbrr5t.js → studio-y60sts62.js} +3 -3
- package/dist/chunks/studio-ybnwymxw.js +5 -0
- package/dist/chunks/studio-zndrqawb.js +5 -0
- package/dist/chunks/studio-zwaxn5s2.js +5 -0
- package/dist/chunks/{tsMode-h6tkz0ea.js → tsMode-f54j66ym.js} +5 -5
- package/dist/chunks/workspace-home-x1787wpv.js +4 -0
- package/dist/chunks/workspace-zpe1np43.js +4 -0
- package/dist/chunks/{ws-client-e951my95.js → ws-client-e8kj684q.js} +3 -3
- package/dist/manifest.json +76 -75
- package/dist/studio.js +83 -81
- package/package.json +4 -4
- package/src/browse/library-model.ts +56 -23
- package/src/commands/context.ts +1 -0
- package/src/editor/shortcuts.ts +3 -0
- package/src/panels/data-grid.ts +0 -4
- package/src/panels/settings-menu.ts +3 -3
- package/src/panels/settings-pane.ts +42 -56
- package/src/platform-errors.ts +17 -1
- package/src/platforms/cloud.ts +14 -0
- package/src/publish/publish-changes.ts +136 -4
- 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/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/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 +14 -0
- package/src/ui/ai-credentials-form.ts +128 -11
- package/src/ui/regions.ts +5 -5
- package/src/workspace/tab-workspace.ts +3 -2
- package/src/workspace/workspace-home.ts +18 -4
- package/src/workspace/workspace-ids.ts +40 -7
- package/dist/chunks/adapter-wc5zvg9a.js +0 -4
- package/dist/chunks/add-new-flow-snx1s4b0.js +0 -4
- package/dist/chunks/cf-account-picker-xyqw0jex.js +0 -4
- package/dist/chunks/cf-settings-hp1pjm0n.js +0 -4
- package/dist/chunks/components-nkwphrx3.js +0 -4
- package/dist/chunks/content-source-6a253wmk.js +0 -4
- package/dist/chunks/editor-wqrc68ca.js +0 -4
- package/dist/chunks/entry-commands-nef0wrmp.js +0 -4
- package/dist/chunks/extension-sections-8ez2rrk8.js +0 -4
- package/dist/chunks/file-ops-y3w71k1n.js +0 -4
- package/dist/chunks/files-de90apn9.js +0 -4
- package/dist/chunks/format-host-zfjz4rzm.js +0 -4
- package/dist/chunks/fs-events-77c0mr0x.js +0 -4
- package/dist/chunks/grid-open-4n4fjfpf.js +0 -4
- package/dist/chunks/iframe-host-na2mywz4.js +0 -4
- package/dist/chunks/index-ewmgdb7c.js +0 -4
- package/dist/chunks/jx-validate-b309c020.js +0 -4
- package/dist/chunks/layers-8rn8y6qv.js +0 -4
- package/dist/chunks/layers-panel-eyah7h9z.js +0 -4
- package/dist/chunks/lit-html-wyqw04y2.js +0 -4
- package/dist/chunks/media-library-gs3np47y.js +0 -4
- package/dist/chunks/media-picker-tt7dk4tb.js +0 -4
- package/dist/chunks/monaco-buffer-wsf6cmnq.js +0 -4
- package/dist/chunks/notify-qwst108r.js +0 -4
- package/dist/chunks/parse-n8dfxnqf.js +0 -4
- package/dist/chunks/platform-c16mqvmp.js +0 -4
- package/dist/chunks/publish-panel-d34bhvwj.js +0 -4
- package/dist/chunks/publish-workspace-2yr60mt4.js +0 -4
- package/dist/chunks/quick-search-e6xsdv0d.js +0 -4
- package/dist/chunks/references-gsj6trdv.js +0 -4
- package/dist/chunks/right-panel-33jv1knc.js +0 -4
- package/dist/chunks/source-control-bj9bejjv.js +0 -4
- package/dist/chunks/studio-03mqh8ax.js +0 -5
- package/dist/chunks/studio-0ydxv2mr.js +0 -7
- package/dist/chunks/studio-2hsqbq7b.js +0 -7
- package/dist/chunks/studio-2j86y34y.js +0 -6
- package/dist/chunks/studio-2ks6ka4g.js +0 -5
- package/dist/chunks/studio-34jrz12k.js +0 -5
- package/dist/chunks/studio-35vrtpdq.js +0 -5
- package/dist/chunks/studio-38g0jdz7.js +0 -5
- package/dist/chunks/studio-3zge48z4.js +0 -7
- package/dist/chunks/studio-45gyxpcq.js +0 -5
- package/dist/chunks/studio-5wzdy1jc.js +0 -16
- package/dist/chunks/studio-732mw0g8.js +0 -5
- package/dist/chunks/studio-771k2n12.js +0 -5
- package/dist/chunks/studio-88bmt9rh.js +0 -5
- package/dist/chunks/studio-c1pxszat.js +0 -5
- package/dist/chunks/studio-cpkamjdn.js +0 -14
- package/dist/chunks/studio-d038kqbs.js +0 -5
- package/dist/chunks/studio-d83m0wyq.js +0 -5
- package/dist/chunks/studio-dbxv7w4s.js +0 -5
- package/dist/chunks/studio-de1s2x9v.js +0 -5
- package/dist/chunks/studio-epenqmvk.js +0 -7
- package/dist/chunks/studio-et2q4s8n.js +0 -5
- package/dist/chunks/studio-h7weqf2q.js +0 -5
- package/dist/chunks/studio-hwejq03c.js +0 -5
- package/dist/chunks/studio-k3a8rp3d.js +0 -5
- package/dist/chunks/studio-p1n2xjda.js +0 -5
- package/dist/chunks/studio-pgm83a2f.js +0 -6
- package/dist/chunks/studio-pqqq6v85.js +0 -5
- package/dist/chunks/studio-psh8mwnw.js +0 -4
- package/dist/chunks/studio-rzg8hnkf.js +0 -5
- package/dist/chunks/studio-tyw7tk0x.js +0 -5
- package/dist/chunks/studio-v9ywz2se.js +0 -6
- package/dist/chunks/studio-vzgmyt0v.js +0 -5
- package/dist/chunks/studio-x08a3m0a.js +0 -5
- package/dist/chunks/studio-x3mjr5cc.js +0 -5
- package/dist/chunks/studio-xh683qzy.js +0 -5
- package/dist/chunks/studio-yx8hzqja.js +0 -5
- package/dist/chunks/studio-zg16eeq1.js +0 -5
- package/dist/chunks/studio-zgx7svjd.js +0 -9
- package/dist/chunks/studio-zkq3r91p.js +0 -5
- package/dist/chunks/workspace-home-zkkrxq3r.js +0 -4
- package/dist/chunks/workspace-tdqqjqaa.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
|
@@ -10,8 +10,16 @@
|
|
|
10
10
|
* a host that opens pull requests (`createPullRequest`); on one that does not (the dev server and
|
|
11
11
|
* the desktop app today) it is drawn disabled with that sentence rather than falling back to a
|
|
12
12
|
* branch push that opens nothing. On a host whose branches are sessions (`gitHosted`, the cloud) a
|
|
13
|
-
* branch cannot be
|
|
14
|
-
*
|
|
13
|
+
* branch cannot be checked out mid-flow, so review there commits to the session's own branch and
|
|
14
|
+
* opens the pull request from it; on the default branch, where every commit is live, the change set
|
|
15
|
+
* is committed as a branch of its own instead (`gitCommitBranch`), and the destination's note says
|
|
16
|
+
* which.
|
|
17
|
+
*
|
|
18
|
+
* **A remote that moved is not a dead end.** On such a host a commit the remote's newer changes
|
|
19
|
+
* overlap is refused with the files they share ({@link remoteConflictsOf}). The author is then
|
|
20
|
+
* offered the same branch of its own: {@link publishAsBranch} commits the change set onto the base
|
|
21
|
+
* it was edited against, which cannot conflict, and opens a pull request into the branch that
|
|
22
|
+
* moved, where GitHub reconciles the two.
|
|
15
23
|
*
|
|
16
24
|
* **Every word a control shows is decided here**, so the workspace and the dialog cannot disagree
|
|
17
25
|
* about what a button does: {@link destinationChoices}, {@link publishLabel} and
|
|
@@ -24,6 +32,8 @@
|
|
|
24
32
|
import { errorMessage } from "@jxsuite/schema/parse";
|
|
25
33
|
import { flushAllCollab } from "../collab/collab-session";
|
|
26
34
|
import { getPlatform, hasPlatform } from "../platform";
|
|
35
|
+
import { remoteConflictsOf } from "../platform-errors";
|
|
36
|
+
import { showPromptDialog } from "../ui/layers";
|
|
27
37
|
import { notify } from "../services/notify";
|
|
28
38
|
import { shell } from "../shell";
|
|
29
39
|
import { projectState } from "../store";
|
|
@@ -71,6 +81,21 @@ export function reviewBranchName(summary: string): string {
|
|
|
71
81
|
return `studio/${slug.replace(/-$/, "") || "changes"}`;
|
|
72
82
|
}
|
|
73
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Whether this host can commit a change set as a branch of its own without checking it out
|
|
86
|
+
* (`StudioPlatform.gitCommitBranch`): what lets the cloud send a review from the default branch and
|
|
87
|
+
* publish a change set the remote has conflicted with.
|
|
88
|
+
*/
|
|
89
|
+
export function platformCommitsBranches(): boolean {
|
|
90
|
+
return hasPlatform() && getPlatform().gitCommitBranch !== undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Whether review on this host commits a branch of its own rather than the session's branch. */
|
|
94
|
+
function reviewBranchesOff(): boolean {
|
|
95
|
+
const base = defaultBranch();
|
|
96
|
+
return platformHostsGit() && platformCommitsBranches() && base !== "" && currentBranch() === base;
|
|
97
|
+
}
|
|
98
|
+
|
|
74
99
|
/** One destination, as a radio card draws it. */
|
|
75
100
|
export interface DestinationChoice {
|
|
76
101
|
value: PublishDestination;
|
|
@@ -108,6 +133,9 @@ export function reviewRefusal(): string {
|
|
|
108
133
|
publish by accident. */
|
|
109
134
|
const base = defaultBranch();
|
|
110
135
|
const branch = currentBranch();
|
|
136
|
+
if (reviewBranchesOff()) {
|
|
137
|
+
return "";
|
|
138
|
+
}
|
|
111
139
|
if (!base || !branch || branch === base) {
|
|
112
140
|
return (
|
|
113
141
|
`Every commit to ${base || "the default branch"} on this host is live. Create a branch ` +
|
|
@@ -140,14 +168,17 @@ function liveNote(): string {
|
|
|
140
168
|
/** What Send for review does, said before it runs. */
|
|
141
169
|
function reviewNote(): string {
|
|
142
170
|
const branch = currentBranch() || "the current branch";
|
|
171
|
+
const summary = shell.git.commitMessage.trim();
|
|
172
|
+
const head = summary ? reviewBranchName(summary) : "a new studio/ branch";
|
|
173
|
+
if (reviewBranchesOff()) {
|
|
174
|
+
return `Saves to ${head} on GitHub and opens a pull request into ${branch}.`;
|
|
175
|
+
}
|
|
143
176
|
if (platformHostsGit()) {
|
|
144
177
|
return (
|
|
145
178
|
`Commits to ${branch} and opens a pull request from it into ` +
|
|
146
179
|
`${defaultBranch() || "the default branch"}.`
|
|
147
180
|
);
|
|
148
181
|
}
|
|
149
|
-
const summary = shell.git.commitMessage.trim();
|
|
150
|
-
const head = summary ? reviewBranchName(summary) : "a new studio/ branch";
|
|
151
182
|
return `Saves to ${head}, pushes it and opens a pull request into ${branch}.`;
|
|
152
183
|
}
|
|
153
184
|
|
|
@@ -424,6 +455,16 @@ export async function publishChanges(
|
|
|
424
455
|
await refreshGitStatus();
|
|
425
456
|
return shipped;
|
|
426
457
|
} catch (error) {
|
|
458
|
+
const conflicts = committed ? null : remoteConflictsOf(error);
|
|
459
|
+
if (conflicts && platformCommitsBranches()) {
|
|
460
|
+
shell.git.loading = false;
|
|
461
|
+
if (await publishAsBranch(message, conflicts)) {
|
|
462
|
+
return true;
|
|
463
|
+
}
|
|
464
|
+
// Declined: the refusal still stands, and it is what the author must read.
|
|
465
|
+
shell.git.error ??= errorMessage(error);
|
|
466
|
+
return false;
|
|
467
|
+
}
|
|
427
468
|
shell.git.error = errorMessage(error);
|
|
428
469
|
shell.git.loading = false;
|
|
429
470
|
// A push that failed after the commit landed must not offer the same summary again.
|
|
@@ -434,6 +475,89 @@ export async function publishChanges(
|
|
|
434
475
|
}
|
|
435
476
|
}
|
|
436
477
|
|
|
478
|
+
/** Up to three paths, then how many more: what a sentence can name of a list of files. */
|
|
479
|
+
function namePaths(paths: readonly string[]): string {
|
|
480
|
+
const shown = paths.slice(0, 3).join(", ");
|
|
481
|
+
return paths.length > 3 ? `${shown} and ${paths.length - 3} more` : shown;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Offer to publish a change set the remote has conflicted with as a branch of its own, and do it.
|
|
486
|
+
*
|
|
487
|
+
* Asked, never assumed: the author named a destination, and this is a different one. The dialog
|
|
488
|
+
* says which files collided and what will happen, and the branch name it proposes is the one Send
|
|
489
|
+
* for review would use, editable. Confirming commits the change set onto the base it was edited
|
|
490
|
+
* against (`gitCommitBranch`, which cannot conflict), opens a pull request from it into the branch
|
|
491
|
+
* that moved, and pulls that branch up to date now that the colliding changes have left it.
|
|
492
|
+
* Cancelling puts the summary back, as any publish that never committed does.
|
|
493
|
+
*
|
|
494
|
+
* @param message The summary the refused commit was made with.
|
|
495
|
+
* @param conflicts The files the remote changed that this change set changes too.
|
|
496
|
+
* @returns Whether the change set reached the new branch.
|
|
497
|
+
*/
|
|
498
|
+
export async function publishAsBranch(
|
|
499
|
+
message: string,
|
|
500
|
+
conflicts: readonly string[],
|
|
501
|
+
): Promise<boolean> {
|
|
502
|
+
const plat = getPlatform();
|
|
503
|
+
const commitBranch = plat.gitCommitBranch;
|
|
504
|
+
if (!commitBranch) {
|
|
505
|
+
return false;
|
|
506
|
+
}
|
|
507
|
+
const current = currentBranch() || "this branch";
|
|
508
|
+
const count = conflicts.length === 1 ? "1 file" : `${conflicts.length} files`;
|
|
509
|
+
const name = await showPromptDialog("Publish to a New Branch", {
|
|
510
|
+
confirmLabel: "Publish to branch",
|
|
511
|
+
message:
|
|
512
|
+
`${current} changed on GitHub after these edits began, and ${count} changed on both sides ` +
|
|
513
|
+
`(${namePaths(conflicts)}). Publish your changes to a branch of their own instead, and a ` +
|
|
514
|
+
`pull request will bring them into ${current}.`,
|
|
515
|
+
value: reviewBranchName(message),
|
|
516
|
+
validate: (value) => (value.trim() ? "" : "Enter a branch name."),
|
|
517
|
+
});
|
|
518
|
+
if (!name) {
|
|
519
|
+
restoreSummary(message);
|
|
520
|
+
return false;
|
|
521
|
+
}
|
|
522
|
+
shell.git.loading = true;
|
|
523
|
+
shell.git.error = null;
|
|
524
|
+
let committed = false;
|
|
525
|
+
try {
|
|
526
|
+
const result = await commitBranch({ branch: name, message });
|
|
527
|
+
committed = true;
|
|
528
|
+
const open = plat.createPullRequest;
|
|
529
|
+
const base = currentBranch();
|
|
530
|
+
const pr = open && base ? await open({ base, head: result.branch, title: message }) : null;
|
|
531
|
+
if (pr) {
|
|
532
|
+
shell.publish.review.number = pr.number;
|
|
533
|
+
shell.publish.review.url = pr.url;
|
|
534
|
+
}
|
|
535
|
+
notify.success(
|
|
536
|
+
pr
|
|
537
|
+
? `Published to ${result.branch}. Pull request #${pr.number} brings it into ${base}.`
|
|
538
|
+
: `Published to ${result.branch}.`,
|
|
539
|
+
{ detail: pr?.url ?? "", key: "publish.branch", source: "Publish" },
|
|
540
|
+
);
|
|
541
|
+
/* The colliding edits have left this session, so it can fast-forward to the remote now. A
|
|
542
|
+
pull that still cannot (another collaborator's edit overlaps too) is not this publish's
|
|
543
|
+
failure: the status says the branch is behind, and Pull says why. */
|
|
544
|
+
try {
|
|
545
|
+
await plat.gitPull();
|
|
546
|
+
} catch {
|
|
547
|
+
// Reported by the next Pull, where it can be acted on.
|
|
548
|
+
}
|
|
549
|
+
await refreshGitStatus();
|
|
550
|
+
return true;
|
|
551
|
+
} catch (error) {
|
|
552
|
+
shell.git.error = errorMessage(error);
|
|
553
|
+
shell.git.loading = false;
|
|
554
|
+
if (!committed) {
|
|
555
|
+
restoreSummary(message);
|
|
556
|
+
}
|
|
557
|
+
return false;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
437
561
|
/** What a pull request is opened with: its title, and the branches when the flow chose them. */
|
|
438
562
|
interface PullRequestRequest {
|
|
439
563
|
title: string;
|
|
@@ -454,6 +578,14 @@ async function commitForReview(
|
|
|
454
578
|
message: string,
|
|
455
579
|
onCommitted: () => void,
|
|
456
580
|
): Promise<PullRequestRequest> {
|
|
581
|
+
if (reviewBranchesOff() && plat.gitCommitBranch) {
|
|
582
|
+
/* Every commit to the default branch here is live, so the change set goes to a branch of its
|
|
583
|
+
own, parented on the session's base, and leaves the session: it is the pull request's now. */
|
|
584
|
+
const base = currentBranch();
|
|
585
|
+
const { branch } = await plat.gitCommitBranch({ branch: reviewBranchName(message), message });
|
|
586
|
+
onCommitted();
|
|
587
|
+
return { base, head: branch, title: message };
|
|
588
|
+
}
|
|
457
589
|
if (platformHostsGit()) {
|
|
458
590
|
// `reviewRefusal` has already refused the default branch, so this commit is not a live one.
|
|
459
591
|
await plat.gitCommit(message);
|
|
@@ -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,
|
|
@@ -60,6 +60,15 @@ function normalizeBaseUrl(raw: string): string {
|
|
|
60
60
|
return raw.trim().replace(/\/+$/, "");
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
/**
|
|
64
|
+
* The provider family, stored only when it is not the default: `"anthropic"`, or `""` for the
|
|
65
|
+
* OpenAI-compatible wire every install had before the setting existed. Anything else reads as the
|
|
66
|
+
* default, so a hand-edited value can never send a key to a family nobody chose.
|
|
67
|
+
*/
|
|
68
|
+
function normalizeProvider(raw: string): string {
|
|
69
|
+
return raw.trim().toLowerCase() === "anthropic" ? "anthropic" : "";
|
|
70
|
+
}
|
|
71
|
+
|
|
63
72
|
/** Trim. Credentials are pasted, and a paste carries whitespace. */
|
|
64
73
|
function trimmed(raw: string): string {
|
|
65
74
|
return raw.trim();
|
|
@@ -91,6 +100,16 @@ export const SETTINGS = {
|
|
|
91
100
|
scope: "user",
|
|
92
101
|
secret: true,
|
|
93
102
|
},
|
|
103
|
+
/**
|
|
104
|
+
* Which wire the key belongs to (specs/ai.md §2.6). Blank is the OpenAI-compatible default, so an
|
|
105
|
+
* install that predates the setting keeps the provider it already had.
|
|
106
|
+
*/
|
|
107
|
+
aiProvider: {
|
|
108
|
+
default: "",
|
|
109
|
+
key: "jx.ai.provider",
|
|
110
|
+
normalize: normalizeProvider,
|
|
111
|
+
scope: "user",
|
|
112
|
+
},
|
|
94
113
|
/**
|
|
95
114
|
* `"gpt-4o"` is what a sender falls back to, NOT a stored choice. A surface that writes back must
|
|
96
115
|
* read through `readStoredSetting`, or it persists a model the user never picked.
|
|
@@ -49,7 +49,8 @@ import { resolveContextPointer } from "../services/context-resolver";
|
|
|
49
49
|
import { deriveSecretEnvName } from "../services/data-service";
|
|
50
50
|
import { validateProjectConfig } from "../services/jx-validate";
|
|
51
51
|
import { notify } from "../services/notify";
|
|
52
|
-
import {
|
|
52
|
+
import { REGION_ATTR } from "../ui/regions";
|
|
53
|
+
import { settingsRegion } from "./settings-workspace";
|
|
53
54
|
import { mountContributedSurface } from "../surfaces/settings-contributed";
|
|
54
55
|
|
|
55
56
|
import type { JsonSchema, SchemaFormContext } from "../ui/schema-form";
|
|
@@ -371,15 +372,23 @@ function sectionValue(key: string): JsonObject | null {
|
|
|
371
372
|
// ─── Per-container mount ──────────────────────────────────────────────────────
|
|
372
373
|
|
|
373
374
|
/**
|
|
374
|
-
* A region under the
|
|
375
|
-
* entry rows and its entry editor are named under `
|
|
375
|
+
* A region under the Settings home, which is where a contributed section is drawn (§17.1): its
|
|
376
|
+
* entry rows and its entry editor are named under `pane.<id>/settings`, so a shot can crop one
|
|
376
377
|
* without a selector.
|
|
377
378
|
*
|
|
379
|
+
* The home is per pane, and a renderer is handed a container and nothing else, so the base is READ
|
|
380
|
+
* off the nearest region around the container, which is the home's own: the id is derived from what
|
|
381
|
+
* is on screen, as every region is (`ui/regions.ts`). A container not yet in a home names the
|
|
382
|
+
* primary pane's.
|
|
383
|
+
*
|
|
384
|
+
* @param {HTMLElement} container The element the section was rendered into
|
|
378
385
|
* @param {string} part
|
|
379
386
|
* @returns {string}
|
|
380
387
|
*/
|
|
381
|
-
function sectionRegion(part: string): string {
|
|
382
|
-
|
|
388
|
+
function sectionRegion(container: HTMLElement, part: string): string {
|
|
389
|
+
const base =
|
|
390
|
+
container.closest(`[${REGION_ATTR}]`)?.getAttribute(REGION_ATTR) ?? settingsRegion("primary");
|
|
391
|
+
return `${base}/${part}`;
|
|
383
392
|
}
|
|
384
393
|
|
|
385
394
|
/** One mounted section: its document, its two island hosts, and what it was last asked to draw. */
|
|
@@ -455,7 +464,7 @@ function project(record: SectionMount): ContributedView {
|
|
|
455
464
|
const sectionKey = contribution.key;
|
|
456
465
|
const title = contribution.title ?? sectionKey;
|
|
457
466
|
const actionsState = opts.actions ? "slot" : "none";
|
|
458
|
-
const editorRegion = sectionRegion("editor");
|
|
467
|
+
const editorRegion = sectionRegion(record.container, "editor");
|
|
459
468
|
const words = entryWords(contribution.settings.entry?.noun);
|
|
460
469
|
|
|
461
470
|
if ((contribution.settings.layout ?? "form") !== "map") {
|
|
@@ -483,7 +492,7 @@ function project(record: SectionMount): ContributedView {
|
|
|
483
492
|
editorState: editing ? "editing" : "empty",
|
|
484
493
|
entries: Object.keys(entries).map((name) => ({
|
|
485
494
|
key: name,
|
|
486
|
-
region: sectionRegion(`entry:${name}`),
|
|
495
|
+
region: sectionRegion(record.container, `entry:${name}`),
|
|
487
496
|
selected: name === selected,
|
|
488
497
|
})),
|
|
489
498
|
entryName: record.echo ?? (editing ? selected : ""),
|
|
@@ -386,9 +386,9 @@ function addOverride(container: HTMLElement, token: string, ctx: string): void {
|
|
|
386
386
|
* per token are defined.
|
|
387
387
|
*
|
|
388
388
|
* Through the command rather than by writing the registry's section: the editor is hosted outside
|
|
389
|
-
* Settings (the Styles workspace's Variables view), so selecting a section of a
|
|
390
|
-
*
|
|
391
|
-
* the editor lives in, as the Variables view's own button files it.
|
|
389
|
+
* Settings (the Styles workspace's Variables view), so selecting a section of a workspace that is
|
|
390
|
+
* not on screen would change nothing the reader can see. A refusal is filed under "Styles", the
|
|
391
|
+
* workspace the editor lives in, as the Variables view's own button files it.
|
|
392
392
|
*/
|
|
393
393
|
function openContextsSection(): void {
|
|
394
394
|
void runActiveReported("settings.open", { section: "contexts" }, "Styles");
|
|
@@ -418,7 +418,7 @@ function actions(container: HTMLElement): CssVarsActions {
|
|
|
418
418
|
* appended `'--dark': '(prefers-color-scheme: dark)'` to `$media` without ever using the word
|
|
419
419
|
* breakpoint, which made this form the fourth and least discoverable definition site for a
|
|
420
420
|
* map whose other three lived in the wizard, Overview and Properties › Media. It goes there
|
|
421
|
-
* through `settings.open`, so
|
|
421
|
+
* through `settings.open`, so Settings opens on Contexts however this editor is hosted.
|
|
422
422
|
*/
|
|
423
423
|
manageContexts: () => {
|
|
424
424
|
openContextsSection();
|
|
@@ -22,7 +22,6 @@ import { projectState } from "../store";
|
|
|
22
22
|
import { updateSiteConfig } from "../site-context";
|
|
23
23
|
import { activeRegistry } from "../commands/active-registry";
|
|
24
24
|
import { runActiveReported } from "../commands/run-reported";
|
|
25
|
-
import { closeSettingsDialog } from "./settings-dialog";
|
|
26
25
|
import { getPlatform } from "../platform";
|
|
27
26
|
import { mountOverviewSurface } from "../surfaces/settings-overview";
|
|
28
27
|
import { previewAssetSrcFor } from "../canvas/asset-refs";
|
|
@@ -265,11 +264,9 @@ function actions(container: HTMLElement): OverviewActions {
|
|
|
265
264
|
* says how to get there. This used to write `session.ui.canvasMode` here directly — a second
|
|
266
265
|
* implementation of a capability, `studio-ui-guidelines.md` §12.5's defect in miniature: the
|
|
267
266
|
* button and the command could disagree about what "open Project Styles" means, and only one of
|
|
268
|
-
* them was reachable by name.
|
|
269
|
-
* first: left up, it would cover the workspace the reader just asked for.
|
|
267
|
+
* them was reachable by name.
|
|
270
268
|
*/
|
|
271
269
|
openStyles: () => {
|
|
272
|
-
closeSettingsDialog();
|
|
273
270
|
void runActiveReported("view.setWorkspace", { workspace: "styles" }, "Settings");
|
|
274
271
|
},
|
|
275
272
|
setDescription: (value: string) => {
|