@jxsuite/studio 3.0.0 → 4.1.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/README.md +5 -5
- package/dist/chunks/{2020-xp8vnwma.js → 2020-n6tfwnda.js} +2 -2
- package/dist/chunks/Overlay.dev-etqjwyf9.js +23 -0
- package/dist/chunks/cf-account-picker-en86v7x7.js +163 -0
- package/dist/chunks/{cf-settings-zj2cj1ar.js → cf-settings-45fgyx8y.js} +6 -5
- package/dist/chunks/{components-z6cnkyy1.js → components-zmx10t62.js} +6 -5
- package/dist/chunks/content-source-48xg86wj.js +49 -0
- package/dist/chunks/{convert-to-repeater-6vvx96n2.js → convert-to-repeater-43tfn7cs.js} +33 -25
- package/dist/chunks/entry-commands-e63f1xc0.js +46 -0
- package/dist/chunks/{extension-sections-kwzvpemr.js → extension-sections-txv08xxg.js} +34 -26
- package/dist/chunks/file-ops-wbh4hhsx.js +48 -0
- package/dist/chunks/files-3kfkwhk4.js +93 -0
- package/dist/chunks/{focus-trap.esm-ggawf1hy.js → focus-trap.esm-9t2drp0g.js} +6 -5
- package/dist/chunks/{format-host-8fr3545e.js → format-host-w16q81tf.js} +3 -3
- package/dist/chunks/fs-events-hrrg328g.js +21 -0
- package/dist/chunks/{git-panel-g01d93vc.js → git-panel-532whr1y.js} +22 -16
- package/dist/chunks/{index-nj44jpzj.js → index-wcnt3r9g.js} +2 -2
- package/dist/chunks/{jx-validate-jj9rnt4n.js → jx-validate-8er6gbxy.js} +6 -4
- package/dist/chunks/layers-panel-07hm7g5j.js +67 -0
- package/dist/chunks/layers-q75tj2mf.js +45 -0
- package/dist/chunks/lit-html-j055mkh8.js +22 -0
- package/dist/chunks/monaco-buffer-dqw0xxpx.js +25 -0
- package/dist/chunks/{monaco-setup-a5wkmbgh.js → monaco-setup-vx9bhp1d.js} +4 -4
- package/dist/chunks/notify-yjm79n4s.js +30 -0
- package/dist/chunks/parse-24d0gay3.js +19 -0
- package/dist/chunks/platform-ba1j1gbh.js +16 -0
- package/dist/chunks/publish-panel-v9st96ja.js +29 -0
- package/dist/chunks/quick-search-zccjp6gv.js +66 -0
- package/dist/chunks/references-3qx8fkyp.js +29 -0
- package/dist/chunks/right-panel-vm3b6kwy.js +46 -0
- package/dist/chunks/sp-overlay.dev-qgvhnxf4.js +18 -0
- package/dist/chunks/sp-popover.dev-n2etmqdp.js +9 -0
- package/dist/chunks/sp-progress-circle.dev-hryhj0es.js +15 -0
- package/dist/chunks/sp-tooltip.dev-4sastdnw.js +15 -0
- package/dist/chunks/{sp-tray.dev-64v4pxw8.js → sp-tray.dev-tkbscm06.js} +5 -5
- package/dist/chunks/{studio-wdt9y92a.js → studio-0bq6eh7v.js} +2 -2
- package/dist/chunks/studio-1ngbejft.js +181 -0
- package/dist/chunks/studio-1wagb550.js +118 -0
- package/dist/chunks/{studio-j595e748.js → studio-2b946z26.js} +172 -95
- package/dist/chunks/{studio-pkp61mh8.js → studio-3bqpr0zh.js} +25 -17
- package/dist/chunks/{studio-k4jvjcfx.js → studio-4b5z1x43.js} +46 -12
- package/dist/chunks/{studio-ry1vz5sv.js → studio-4n2b5gzb.js} +15 -13
- package/dist/chunks/{studio-p5dja96s.js → studio-4r1pv6zf.js} +2 -2
- package/dist/chunks/{studio-kvfx9z2c.js → studio-4rzyrtjz.js} +70 -31
- package/dist/chunks/{studio-tzx3xmvx.js → studio-5fx3g9jg.js} +33 -52
- package/dist/chunks/{studio-rh1cnamv.js → studio-5myknm0a.js} +565 -135
- package/dist/chunks/studio-6yw4y3my.js +91 -0
- package/dist/chunks/{studio-0rqqhrdy.js → studio-7nte2hj4.js} +2 -2
- package/dist/chunks/studio-92ab2jqe.js +282 -0
- package/dist/chunks/{studio-6z5eyjwn.js → studio-950k0dx6.js} +72 -135
- package/dist/chunks/{studio-sas7mt3k.js → studio-9ab8gqsm.js} +2 -2
- package/dist/chunks/studio-9n4mrmnb.js +184 -0
- package/dist/chunks/{studio-hcxzyk3x.js → studio-ahn3pdgx.js} +6 -6
- package/dist/chunks/studio-b55y6ewx.js +498 -0
- package/dist/chunks/{studio-s519zc99.js → studio-bgxt93ga.js} +3 -3
- package/dist/chunks/studio-cft9r4r7.js +119 -0
- package/dist/chunks/{studio-dc8s8heb.js → studio-cmb03bg5.js} +6904 -8321
- package/dist/chunks/studio-dm19q43r.js +195 -0
- package/dist/chunks/{studio-2x9zbxmx.js → studio-ef49pnzx.js} +11 -9
- package/dist/chunks/{studio-ay8wcr2b.js → studio-eqq1w638.js} +7 -9
- package/dist/chunks/{studio-5efrtn1m.js → studio-ew0djmhg.js} +23 -19
- package/dist/chunks/{studio-tr20m8g8.js → studio-f3wqjtk5.js} +39 -3
- package/dist/chunks/{studio-xntfdy1x.js → studio-fbf92ckf.js} +36 -23
- package/dist/chunks/{studio-4q0wznzf.js → studio-gcye7qka.js} +442 -126
- package/dist/chunks/{studio-7p35254k.js → studio-gfgb8pe6.js} +36 -193
- package/dist/chunks/studio-jc994dnr.js +1066 -0
- package/dist/chunks/{studio-hj2se583.js → studio-jkftxyvz.js} +69 -3423
- package/dist/chunks/{studio-5x5774ak.js → studio-kryvsw6j.js} +3 -3
- package/dist/chunks/{studio-kfpek536.js → studio-kyh9avvv.js} +2 -2
- package/dist/chunks/{studio-twsafr1e.js → studio-m3hwn7y1.js} +3 -3
- package/dist/chunks/studio-msd847zn.js +1315 -0
- package/dist/chunks/{studio-g5gdqnyd.js → studio-mtfnaqs9.js} +19 -19
- package/dist/chunks/{studio-7ejemhej.js → studio-n6j9t10q.js} +6 -6
- package/dist/chunks/{studio-vhk5fmfv.js → studio-n8b9wqwy.js} +5 -5
- package/dist/chunks/{studio-cjm7fdjf.js → studio-p4ngc046.js} +8 -104
- package/dist/chunks/studio-q43a5y9h.js +394 -0
- package/dist/chunks/studio-q7v10cg2.js +106 -0
- package/dist/chunks/{studio-6kv3apa9.js → studio-q9jd1a60.js} +27 -11
- package/dist/chunks/studio-qcy05dp8.js +1743 -0
- package/dist/chunks/{studio-a0yp7syn.js → studio-qjy2z9bv.js} +29 -132
- package/dist/chunks/{studio-2w11grav.js → studio-qq6jym04.js} +3 -3
- package/dist/chunks/{studio-3fk1ac9z.js → studio-rdx8jcxy.js} +4 -4
- package/dist/chunks/{studio-4mqb9a0j.js → studio-rn9ta2mz.js} +4 -4
- package/dist/chunks/studio-t7qb9db8.js +665 -0
- package/dist/chunks/studio-tmc5emrw.js +8 -0
- package/dist/chunks/{studio-r5xbtctf.js → studio-trxrabqf.js} +292 -2297
- package/dist/chunks/studio-wcy8trfx.js +263 -0
- package/dist/chunks/{studio-3dkcd4c5.js → studio-x4qkdat5.js} +62 -57
- package/dist/chunks/studio-x813zp48.js +195 -0
- package/dist/chunks/{studio-qpfq7xhm.js → studio-xrapxwwc.js} +22 -285
- package/dist/chunks/{studio-xm3dbxvn.js → studio-zczps70q.js} +2 -2
- package/dist/chunks/{studio-8ezdegcx.js → studio-zqazsct2.js} +40 -36
- package/dist/chunks/workspace-fjx52ryv.js +89 -0
- package/dist/iframe-entry.js +141 -56
- package/dist/manifest.json +7 -0
- package/dist/studio.js +2605 -1950
- package/favicon.ico +0 -0
- package/index.html +1 -0
- package/package.json +20 -18
- package/src/about/about-modal.ts +4 -3
- package/src/browse/library-pane.ts +57 -3
- package/src/canvas/canvas-render.ts +23 -1
- package/src/canvas/iframe-patch.ts +47 -21
- package/src/canvas/iframe-render.ts +22 -6
- package/src/commands/app-commands.ts +4 -0
- package/src/commands/command-args.ts +29 -0
- package/src/commands/context.ts +15 -1
- package/src/commands/defaults.ts +29 -1
- package/src/commands/levels.ts +16 -0
- package/src/commands/live-context.ts +3 -0
- package/src/content/collection-match.ts +267 -0
- package/src/content/entry-commands.ts +38 -29
- package/src/content/entry-fields.ts +6 -0
- package/src/editor/shortcuts.ts +5 -0
- package/src/files/file-ops.ts +10 -42
- package/src/files/files.ts +487 -59
- package/src/files/fs-events.ts +9 -0
- package/src/files/gitignore.ts +432 -0
- package/src/files/media-paths.ts +13 -42
- package/src/files/media-usage.ts +41 -131
- package/src/files/serialize-document.ts +60 -0
- package/src/format/convert-file.ts +511 -0
- package/src/format/format-choices.ts +199 -0
- package/src/grid/grid-view.ts +16 -2
- package/src/hosting/document.ts +2 -0
- package/src/hosting/layout.ts +8 -0
- package/src/media/media-open.ts +58 -0
- package/src/media/media-pane.ts +310 -0
- package/src/new-project/import-tab.ts +357 -163
- package/src/new-project/location-fields.ts +8 -5
- package/src/new-project/new-project-modal.ts +27 -34
- package/src/packages/ensure-deps.ts +14 -7
- package/src/packages/jxsuite-update.ts +11 -11
- package/src/page-params.ts +10 -46
- package/src/panels/activity-bar.ts +45 -16
- package/src/panels/ai-chat/attached-context.ts +6 -1
- package/src/panels/ai-chat/chat-view.ts +255 -13
- package/src/panels/ai-chat/composer.ts +31 -97
- package/src/panels/ai-panel.ts +116 -2
- package/src/panels/navigator-panels.ts +7 -6
- package/src/panels/settings-menu.ts +799 -0
- package/src/panels/toolbar.ts +135 -15
- package/src/platforms/cloud.ts +261 -70
- package/src/platforms/devserver.ts +67 -33
- package/src/preview/preview-overlay.ts +226 -0
- package/src/publish/publish-panel.ts +62 -2
- package/src/services/ai-ask.ts +247 -0
- package/src/services/ai-import-tools.ts +532 -0
- package/src/services/ai-models.ts +108 -1
- package/src/services/ai-project-tools.ts +20 -26
- package/src/services/ai-system-prompt.ts +88 -2
- package/src/services/ai-turn-signal.ts +70 -0
- package/src/services/connection.ts +74 -0
- package/src/services/context-manager.ts +101 -1
- package/src/services/document-assistant.ts +45 -4
- package/src/services/import-client.ts +33 -6
- package/src/services/import-run.ts +158 -0
- package/src/services/import-seed.ts +120 -0
- package/src/services/jx-validate.ts +58 -6
- package/src/services/project-adoption.ts +71 -0
- package/src/services/references.ts +0 -0
- package/src/services/settings/definitions.ts +17 -0
- package/src/services/tool-executor.ts +45 -2
- package/src/settings/contexts-section.ts +45 -3
- package/src/settings/dependencies-editor.ts +43 -23
- package/src/settings/general-settings.ts +30 -12
- package/src/settings/preferences-accounts.ts +193 -12
- package/src/settings/preferences-dialog.ts +93 -59
- package/src/settings/preferences-sections.ts +57 -0
- package/src/settings/settings-document.ts +65 -9
- package/src/studio.ts +33 -6
- package/src/tabs/transact.ts +42 -19
- package/src/types.ts +129 -7
- package/src/ui/ai-managed-connect.ts +62 -13
- package/src/ui/ai-model-picker.ts +189 -0
- package/src/ui/cf-account-picker.ts +200 -0
- package/src/ui/choice-field.ts +72 -0
- package/src/ui/layers.ts +83 -5
- package/src/utils/studio-utils.ts +22 -52
- package/styles/forced-colors.css +7 -0
- package/styles/overlays.css +256 -0
- package/styles/panels.css +13 -1
- package/styles/shell.css +147 -0
- package/dist/chunks/Overlay.dev-d2ajy6sj.js +0 -23
- package/dist/chunks/content-source-7fy9qrm0.js +0 -40
- package/dist/chunks/files-xcybanxe.js +0 -70
- package/dist/chunks/layers-panel-27essgkc.js +0 -60
- package/dist/chunks/publish-panel-09sy2bab.js +0 -22
- package/dist/chunks/quick-search-sefb61v8.js +0 -55
- package/dist/chunks/right-panel-9r32jd5j.js +0 -39
- package/dist/chunks/sp-overlay.dev-s18hymn9.js +0 -18
- package/dist/chunks/sp-popover.dev-dj9x7e7a.js +0 -9
- package/dist/chunks/sp-progress-circle.dev-0zxcyz3r.js +0 -15
- package/dist/chunks/sp-tooltip.dev-5c6d1kjz.js +0 -15
- package/dist/chunks/studio-bw5dz6km.js +0 -608
- package/dist/chunks/studio-c31yb4t6.js +0 -55
- package/src/canvas/site-style-css.ts +0 -117
package/src/types.ts
CHANGED
|
@@ -14,7 +14,9 @@ import type {
|
|
|
14
14
|
import type {
|
|
15
15
|
AppInfo,
|
|
16
16
|
AssetCapabilities,
|
|
17
|
+
CfAccountSummary,
|
|
17
18
|
CfConnection,
|
|
19
|
+
CfConnectOutcome,
|
|
18
20
|
CodeServiceResult,
|
|
19
21
|
ComponentMeta,
|
|
20
22
|
DataConnectionsResponse,
|
|
@@ -32,10 +34,12 @@ import type {
|
|
|
32
34
|
GitLogEntry,
|
|
33
35
|
GitStatusResult,
|
|
34
36
|
ImportProgressEvent,
|
|
37
|
+
ImportReadyEvent,
|
|
35
38
|
ImportSiteOptions,
|
|
36
|
-
|
|
39
|
+
ImportSiteSummary,
|
|
37
40
|
PackageInfo,
|
|
38
41
|
PackageOpResult,
|
|
42
|
+
PackageVersionInfo,
|
|
39
43
|
ProjectListEntry,
|
|
40
44
|
ProjectSchemasResponse,
|
|
41
45
|
RecentProjectEntry,
|
|
@@ -52,7 +56,9 @@ export type {
|
|
|
52
56
|
AiModelInfo,
|
|
53
57
|
AiModelsResponse,
|
|
54
58
|
AppInfo,
|
|
59
|
+
CfAccountSummary,
|
|
55
60
|
CfConnection,
|
|
61
|
+
CfConnectOutcome,
|
|
56
62
|
CodeServiceResult,
|
|
57
63
|
ComponentMeta,
|
|
58
64
|
ComponentSlotMeta,
|
|
@@ -79,12 +85,15 @@ export type {
|
|
|
79
85
|
GitFileStatus,
|
|
80
86
|
GitLogEntry,
|
|
81
87
|
GitStatusResult,
|
|
88
|
+
ImportBreakpointPolicy,
|
|
82
89
|
ImportProgressEvent,
|
|
90
|
+
ImportReadyEvent,
|
|
83
91
|
ImportSiteOptions,
|
|
92
|
+
ImportSiteSummary,
|
|
84
93
|
JsonValue,
|
|
85
|
-
OutdatedInfo,
|
|
86
94
|
PackageInfo,
|
|
87
95
|
PackageOpResult,
|
|
96
|
+
PackageVersionInfo,
|
|
88
97
|
ProjectListEntry,
|
|
89
98
|
ProjectSchemasResponse,
|
|
90
99
|
PullRequestInfo,
|
|
@@ -149,6 +158,20 @@ export interface SiteBuildResult {
|
|
|
149
158
|
routes: number;
|
|
150
159
|
files: number;
|
|
151
160
|
errors: string[];
|
|
161
|
+
/**
|
|
162
|
+
* How the backend produced what {@link url} serves.
|
|
163
|
+
*
|
|
164
|
+
* `"built"` is compiler output — what the published site will literally be. `"live"` is the
|
|
165
|
+
* working tree rendered as a site: the same pages at the same routes, assembled by the runtime in
|
|
166
|
+
* the reader's browser rather than prerendered, because the backend cannot run a build at all.
|
|
167
|
+
*
|
|
168
|
+
* The distinction is reported rather than hidden because the two differ in ways a reader would
|
|
169
|
+
* otherwise be misled by — a live preview has no prerendered HTML, no optimized images, no
|
|
170
|
+
* islands and no server-timing results — and because a live preview has something a build does
|
|
171
|
+
* not: it shows the tree as it stands, unsaved edits included. Absent means `"built"`, so a
|
|
172
|
+
* backend that predates this keeps its meaning.
|
|
173
|
+
*/
|
|
174
|
+
mode?: "built" | "live";
|
|
152
175
|
/**
|
|
153
176
|
* Origin the built site is browsable at, e.g. `http://127.0.0.1:41234`.
|
|
154
177
|
*
|
|
@@ -160,6 +183,24 @@ export interface SiteBuildResult {
|
|
|
160
183
|
url?: string;
|
|
161
184
|
}
|
|
162
185
|
|
|
186
|
+
/**
|
|
187
|
+
* What a live site preview reports back.
|
|
188
|
+
*
|
|
189
|
+
* Everything {@link SiteBuildResult} carries, plus the one answer only a live preview can give:
|
|
190
|
+
* whether the reader already had a tab on this project.
|
|
191
|
+
*/
|
|
192
|
+
export interface SitePreviewResult extends SiteBuildResult {
|
|
193
|
+
/**
|
|
194
|
+
* A tab already holding this project's preview took the route, so open nothing.
|
|
195
|
+
*
|
|
196
|
+
* The caller MUST honour it. One tab per project is the contract, and a caller that opens a
|
|
197
|
+
* second one anyway gives the author two tabs on the same project — which is precisely what
|
|
198
|
+
* retargeting the first one exists to prevent. `false` means the backend has no live client, or
|
|
199
|
+
* had one that did not answer, and the caller should open a tab itself.
|
|
200
|
+
*/
|
|
201
|
+
reused?: boolean;
|
|
202
|
+
}
|
|
203
|
+
|
|
163
204
|
export interface StudioPlatform {
|
|
164
205
|
id: string;
|
|
165
206
|
projectRoot: string;
|
|
@@ -266,6 +307,15 @@ export interface StudioPlatform {
|
|
|
266
307
|
*/
|
|
267
308
|
uploadFile: (path: string, data: string | File | Blob | ArrayBuffer) => Promise<UploadResult>;
|
|
268
309
|
deleteFile: (path: string) => Promise<void>;
|
|
310
|
+
/**
|
|
311
|
+
* Move a file and rewrite the project's references to it.
|
|
312
|
+
*
|
|
313
|
+
* `from` and `to` are project-relative, and so is every path in the report — including
|
|
314
|
+
* `references.files[].path` and the `errors[].path` naming a document the refactor could not
|
|
315
|
+
* write. An adapter over a backend with a different path space translates the REQUEST or the
|
|
316
|
+
* REPLY, never both: which one needs translating is a property of that backend's route, and doing
|
|
317
|
+
* both silently assumes it echoes its own input space (specs/desktop.md §3.1).
|
|
318
|
+
*/
|
|
269
319
|
renameFile: (from: string, to: string) => Promise<RenameResult>;
|
|
270
320
|
/**
|
|
271
321
|
* Where a file or a component tag is used across the project — the read side of the same walker
|
|
@@ -279,6 +329,9 @@ export interface StudioPlatform {
|
|
|
279
329
|
*
|
|
280
330
|
* At least one of `path` / `tagName` must be given. A `path` naming a component document
|
|
281
331
|
* contributes its own root tag, so one call answers "as a file AND as an element" together.
|
|
332
|
+
*
|
|
333
|
+
* `path` is project-relative going in, and every path coming back is too. A backend that cannot
|
|
334
|
+
* place the target inside the active project MUST fail rather than answer zero.
|
|
282
335
|
*/
|
|
283
336
|
findReferences?: (target: { path?: string; tagName?: string }) => Promise<ReferencesResult>;
|
|
284
337
|
createDirectory: (path: string) => Promise<void>;
|
|
@@ -298,8 +351,12 @@ export interface StudioPlatform {
|
|
|
298
351
|
installDependencies?: () => Promise<PackageOpResult>;
|
|
299
352
|
/** Whether the project has uninstalled dependencies (node_modules missing). */
|
|
300
353
|
dependenciesNeedInstall?: () => Promise<boolean>;
|
|
301
|
-
/**
|
|
302
|
-
|
|
354
|
+
/**
|
|
355
|
+
* The newest version published on the registry for each dependency, whether or not it is behind.
|
|
356
|
+
* Optional: a platform that cannot reach a registry omits it, and Studio shows no Latest column
|
|
357
|
+
* values rather than inventing them.
|
|
358
|
+
*/
|
|
359
|
+
packageVersions?: () => Promise<PackageVersionInfo[]>;
|
|
303
360
|
/**
|
|
304
361
|
* Rewrite the version range of each named package (preserving its dependencies/devDependencies
|
|
305
362
|
* placement) and run `bun install`. Used by the @jxsuite bump and per-dependency updates.
|
|
@@ -389,6 +446,31 @@ export interface StudioPlatform {
|
|
|
389
446
|
* build happened to leave — which for most projects is nothing at all.
|
|
390
447
|
*/
|
|
391
448
|
buildSite?: () => Promise<SiteBuildResult>;
|
|
449
|
+
/**
|
|
450
|
+
* Preview the site LIVE at `route`, with no build on the path.
|
|
451
|
+
*
|
|
452
|
+
* The sibling of {@link buildSite} and what `View: Open in Browser` reaches first. Nothing is
|
|
453
|
+
* compiled: the backend composes the working tree on demand and the runtime assembles each page
|
|
454
|
+
* in the reader's browser, so what opens carries edits nobody has saved. `buildSite` keeps its
|
|
455
|
+
* own meaning and its own command, because "what does this page look like at its real URL?" and
|
|
456
|
+
* "does my site build?" are different questions and only one of them wants to wait.
|
|
457
|
+
*
|
|
458
|
+
* Optional, and the fallback is `buildSite`: a backend that predates this keeps working, and a
|
|
459
|
+
* backend that can do neither says so rather than opening an origin where half the site is the
|
|
460
|
+
* wrong file.
|
|
461
|
+
*/
|
|
462
|
+
previewSite?: (opts: { route: string }) => Promise<SitePreviewResult>;
|
|
463
|
+
/**
|
|
464
|
+
* Publish the bytes a save WOULD write for one document, so a live preview shows the canvas
|
|
465
|
+
* rather than the disk.
|
|
466
|
+
*
|
|
467
|
+
* Bytes rather than a document, and the same bytes `writeFile` would receive: that is what makes
|
|
468
|
+
* "what the reader sees" and "what saving would produce" one answer instead of two that drift. A
|
|
469
|
+
* backend holds them in memory and writes them nowhere.
|
|
470
|
+
*/
|
|
471
|
+
setPreviewOverlay?: (path: string, contents: string) => Promise<void>;
|
|
472
|
+
/** Drop one document's unsaved bytes, or every one of this project's when no path is given. */
|
|
473
|
+
clearPreviewOverlay?: (path?: string) => Promise<void>;
|
|
392
474
|
gitAddRemote: (name: string, url: string) => Promise<void>;
|
|
393
475
|
/**
|
|
394
476
|
* How the New Project modal collects a destination, and which `CreateProjectDestination` variant
|
|
@@ -429,12 +511,19 @@ export interface StudioPlatform {
|
|
|
429
511
|
* AI-guided import of an existing site into a new project. Runs in the backend (headless Chrome +
|
|
430
512
|
* fs), streaming progress until the project is written. Absent on platforms without a backend
|
|
431
513
|
* import pipeline — the New Project modal hides its Import tab then.
|
|
514
|
+
*
|
|
515
|
+
* `onReady` fires the moment the destination holds an openable `project.json` — seconds in, where
|
|
516
|
+
* the resolved promise is minutes away on a real crawl. It is what lets the caller OPEN the
|
|
517
|
+
* project and let its file tree fill up while the run continues, instead of showing a welcome
|
|
518
|
+
* screen throughout. Optional on both sides: a backend that never sends the line simply never
|
|
519
|
+
* calls it, and the caller opens the project when the run ends, as it always did.
|
|
432
520
|
*/
|
|
433
521
|
importSite?: (
|
|
434
522
|
opts: ImportSiteOptions,
|
|
435
523
|
onProgress: (evt: ImportProgressEvent) => void,
|
|
436
524
|
signal?: AbortSignal,
|
|
437
|
-
|
|
525
|
+
onReady?: (evt: ImportReadyEvent) => void,
|
|
526
|
+
) => Promise<{ root: string; config: ProjectConfig; result?: ImportSiteSummary }>;
|
|
438
527
|
/**
|
|
439
528
|
* Open a native directory picker and return the chosen absolute path (null when cancelled). Backs
|
|
440
529
|
* the New Project modal's **Browse…** button on `createDestination: "path"` platforms. Desktop
|
|
@@ -527,14 +616,34 @@ export interface StudioPlatform {
|
|
|
527
616
|
head?: string;
|
|
528
617
|
base?: string;
|
|
529
618
|
}) => Promise<{ url: string; number: number }>;
|
|
530
|
-
/**
|
|
619
|
+
/**
|
|
620
|
+
* Current Cloudflare connection state, when the platform can broker one. See {@link CfConnection}
|
|
621
|
+
* for what null, `{connected: false}` and `needsReconnect` each mean — they are three different
|
|
622
|
+
* states and the UI says a different sentence for each.
|
|
623
|
+
*/
|
|
531
624
|
cfConnection?: () => Promise<CfConnection | null>;
|
|
532
625
|
/**
|
|
533
626
|
* Interactively connect a Cloudflare account (hosted OAuth on the cloud platform). Local
|
|
534
627
|
* platforms omit it — the publish UI collects an API token instead and verifies via
|
|
535
628
|
* cfConnection.
|
|
629
|
+
*
|
|
630
|
+
* Resolves a {@link CfConnectOutcome} rather than a connection, because "connected" is only one of
|
|
631
|
+
* four endings: a blocked popup navigates the whole page (`redirect`), a closed popup is a
|
|
632
|
+
* cancellation, and a passed deadline is a timeout. Rejects only when the flow actually failed —
|
|
633
|
+
* a relayed OAuth error, or a success the platform could not confirm. Resolves null only where
|
|
634
|
+
* there is no DOM to open a popup in.
|
|
635
|
+
*/
|
|
636
|
+
cfConnect?: () => Promise<CfConnectOutcome | null>;
|
|
637
|
+
/**
|
|
638
|
+
* Every Cloudflare account the connected grant can reach. Backs the account picker a
|
|
639
|
+
* multi-account user needs before publishing: the broker stores one account id per connection,
|
|
640
|
+
* and until one is chosen every Cloudflare-backed call answers `cf_account_required`.
|
|
536
641
|
*/
|
|
537
|
-
|
|
642
|
+
cfAccounts?: () => Promise<CfAccountSummary[]>;
|
|
643
|
+
/** Store the chosen account on the brokered connection (the picker's commit). */
|
|
644
|
+
cfSelectAccount?: (account: { id: string; name?: string }) => Promise<void>;
|
|
645
|
+
/** Forget the brokered connection and revoke its tokens upstream. */
|
|
646
|
+
cfDisconnect?: () => Promise<void>;
|
|
538
647
|
/**
|
|
539
648
|
* Allowlisted Cloudflare API passthrough (accounts, Pages projects and deployments). The backend
|
|
540
649
|
* injects credentials — an OAuth token on the cloud platform, the user's pasted API token locally
|
|
@@ -568,6 +677,19 @@ export interface StudioPlatform {
|
|
|
568
677
|
* showing a stale value until its next boot rather than losing anything.
|
|
569
678
|
*/
|
|
570
679
|
subscribeSettings?: (handler: (settings: Record<string, string>) => void) => () => void;
|
|
680
|
+
/**
|
|
681
|
+
* Watch the backend connection, on platforms whose transport can lose one.
|
|
682
|
+
*
|
|
683
|
+
* Only a socket-based launcher implements this: the dev server talks over `fetch` and has nothing
|
|
684
|
+
* to lose, and the electrobun launcher's RPC times a request out rather than dropping it. The
|
|
685
|
+
* chromium launcher holds one long-lived WebSocket, and when it died every call after it hung
|
|
686
|
+
* with no error anywhere — so the transport reports the state, and the studio says it out loud.
|
|
687
|
+
*
|
|
688
|
+
* The handler is called on each transition, never repeatedly for one state.
|
|
689
|
+
*/
|
|
690
|
+
subscribeConnection?: (
|
|
691
|
+
handler: (state: { online: boolean; reason?: string }) => void,
|
|
692
|
+
) => () => void;
|
|
571
693
|
}
|
|
572
694
|
|
|
573
695
|
// ─── Studio Types ───────────────────────────────────────────────────────────
|
|
@@ -25,7 +25,9 @@ import {
|
|
|
25
25
|
isManagedProxy,
|
|
26
26
|
isProxyConfigured,
|
|
27
27
|
proxyStateCode,
|
|
28
|
+
resetModelCache,
|
|
28
29
|
} from "../services/ai-models";
|
|
30
|
+
import type { CfConnectOutcome } from "../types";
|
|
29
31
|
|
|
30
32
|
export interface ManagedConnectOptions {
|
|
31
33
|
/** Host re-render scheduler — called on connect start/finish and when the probe settles. */
|
|
@@ -52,20 +54,73 @@ export function createManagedConnect(opts: ManagedConnectOptions): ManagedConnec
|
|
|
52
54
|
let connectError = "";
|
|
53
55
|
|
|
54
56
|
/*
|
|
55
|
-
* Offer the keyless path when the proxy
|
|
56
|
-
*
|
|
57
|
-
*
|
|
57
|
+
* Offer the keyless path when the proxy is managed and the platform can run the hosted OAuth flow
|
|
58
|
+
* (the PAL seam — desktop shells can implement cfConnect later). Guarded on hasPlatform() because
|
|
59
|
+
* gates can render from modules that load before registration.
|
|
60
|
+
*
|
|
61
|
+
* `configured` alone does NOT withdraw the offer any more. A backend can answer `configured: true`
|
|
62
|
+
* over a grant it cannot actually use, and hiding the CTA there left the user with a broken
|
|
63
|
+
* assistant and no button at all — the one state in which reconnecting is the whole fix. So the
|
|
64
|
+
* lapsed code overrides `configured`, while `cf_upstream_error` (also `configured: true`, and
|
|
65
|
+
* carrying no reconnect code) still hides it: an unreachable Cloudflare is not fixed by an OAuth
|
|
66
|
+
* round trip.
|
|
58
67
|
*/
|
|
59
68
|
function canOffer(): boolean {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
69
|
+
if (!isManagedProxy() || !hasPlatform() || !getPlatform().cfConnect) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
return !isProxyConfigured() || proxyStateCode() === "cf_reconnect_required";
|
|
63
73
|
}
|
|
64
74
|
|
|
65
75
|
function ensureProbe() {
|
|
66
76
|
ensureProxyProbe(opts.requestRender);
|
|
67
77
|
}
|
|
68
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Act on how the flow ended.
|
|
81
|
+
*
|
|
82
|
+
* Every branch here exists because the old code had ONE: any truthy result was success, so a
|
|
83
|
+
* connect that landed on the same lapsed row re-probed, got the same `cf_reconnect_required`
|
|
84
|
+
* back, and re-rendered a byte-identical CTA. The user clicked Reconnect and observably nothing
|
|
85
|
+
* happened — which is the half of the outage that made the other half impossible to diagnose.
|
|
86
|
+
*
|
|
87
|
+
* @param {CfConnectOutcome | null} outcome - Null where the platform had no DOM to open a popup
|
|
88
|
+
* in.
|
|
89
|
+
*/
|
|
90
|
+
async function settle(outcome: CfConnectOutcome | null): Promise<void> {
|
|
91
|
+
/* A blocked popup turns into a full-page redirect, and null is a host with nowhere to open one:
|
|
92
|
+
in both the document is on its way out, so an apology would be the last thing painted before
|
|
93
|
+
it goes. Cancellation is silent for the opposite reason — the user already knows. */
|
|
94
|
+
if (!outcome || outcome.status === "redirect" || outcome.status === "canceled") {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (outcome.status === "timeout") {
|
|
98
|
+
connectError =
|
|
99
|
+
"The Cloudflare window didn't finish. Sign in there, then reconnect — nothing was changed.";
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (!outcome.connection.accountId) {
|
|
103
|
+
/* Lazily, because the picker pulls the dialog layer in and this module is imported by every
|
|
104
|
+
credentials gate — including ones that render before layers are bound. */
|
|
105
|
+
const { openCfAccountPicker } = await import("./cf-account-picker");
|
|
106
|
+
if (!(await openCfAccountPicker())) {
|
|
107
|
+
connectError =
|
|
108
|
+
"Cloudflare is connected, but no account is chosen yet — pick one to finish setting up the assistant.";
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// Re-probe: /models flips to configured once the connection lands, opening the gate.
|
|
113
|
+
resetModelCache();
|
|
114
|
+
await fetchAvailableModels({ force: true });
|
|
115
|
+
/* And then CHECK, because a connect the backend does not honour must not look like one that
|
|
116
|
+
worked. This is the only place that can tell the difference. */
|
|
117
|
+
if (!isProxyConfigured() || proxyStateCode() === "cf_reconnect_required") {
|
|
118
|
+
connectError =
|
|
119
|
+
"Cloudflare connected, but the assistant backend still reports the connection as unusable. " +
|
|
120
|
+
"Try reconnecting, or use your own API key below.";
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
69
124
|
async function connect() {
|
|
70
125
|
if (busy) {
|
|
71
126
|
return;
|
|
@@ -74,13 +129,7 @@ export function createManagedConnect(opts: ManagedConnectOptions): ManagedConnec
|
|
|
74
129
|
connectError = "";
|
|
75
130
|
opts.requestRender();
|
|
76
131
|
try {
|
|
77
|
-
|
|
78
|
-
if (connection) {
|
|
79
|
-
// Re-probe: /models flips to configured once the connection lands, opening the gate.
|
|
80
|
-
await fetchAvailableModels({ force: true });
|
|
81
|
-
} else {
|
|
82
|
-
connectError = "Cloudflare connection was not completed.";
|
|
83
|
-
}
|
|
132
|
+
await settle((await getPlatform().cfConnect?.()) ?? null);
|
|
84
133
|
} catch (error) {
|
|
85
134
|
connectError = error instanceof Error ? error.message : String(error);
|
|
86
135
|
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/**
|
|
3
|
+
* Ai-model-picker.ts — the provider's model catalogue as one `sp-picker`, for every surface that
|
|
4
|
+
* chooses a model.
|
|
5
|
+
*
|
|
6
|
+
* It was private to `panels/ai-chat/composer.ts`, which was correct while the chat composer was the
|
|
7
|
+
* only place a model could be chosen. The New Project **Import** source now chooses one too — and
|
|
8
|
+
* that run spends more tokens than any single chat turn — so the choice needed a second host. A
|
|
9
|
+
* copy would have been two implementations of the invariant below, which is exactly the kind that
|
|
10
|
+
* only breaks in the copy nobody looked at.
|
|
11
|
+
*
|
|
12
|
+
* **The list is never held privately.** It is read from `cachedModels(credentials)` on every
|
|
13
|
+
* render, so a list can only ever be shown for the credentials it was listed under: change the key
|
|
14
|
+
* and the catalogue becomes UNAVAILABLE rather than stale. Holding it in a closure is what once let
|
|
15
|
+
* the picker offer one provider's models while another was configured.
|
|
16
|
+
*
|
|
17
|
+
* A closure factory (precedent: `createComposer`, `createAiCredentialsForm`) so two hosts never
|
|
18
|
+
* share a loading flag or a failed-fetch record.
|
|
19
|
+
*
|
|
20
|
+
* @license MIT
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { html, nothing } from "lit-html";
|
|
24
|
+
import { live } from "lit-html/directives/live.js";
|
|
25
|
+
import {
|
|
26
|
+
aiConnection,
|
|
27
|
+
cachedModels,
|
|
28
|
+
fetchAvailableModels,
|
|
29
|
+
modelToolSupport,
|
|
30
|
+
preferredModel,
|
|
31
|
+
} from "../services/ai-models";
|
|
32
|
+
import { setModel } from "../services/ai-settings";
|
|
33
|
+
|
|
34
|
+
import type { TemplateResult } from "lit-html";
|
|
35
|
+
import type { AiCredentials } from "../services/ai-models";
|
|
36
|
+
|
|
37
|
+
/** The `sp-menu-item` value that means "try listing again" rather than "select this model". */
|
|
38
|
+
export const RETRY_MODELS = "__retry_models__";
|
|
39
|
+
|
|
40
|
+
/** The `sp-menu-item` value of the disabled placeholder shown during the first fetch. */
|
|
41
|
+
export const LOADING_MODELS = "__loading__";
|
|
42
|
+
|
|
43
|
+
/** What a listed model's row says when the backend reported it cannot call tools. */
|
|
44
|
+
export const NO_TOOLS_SUFFIX = " — no tools";
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A model's row label: its name, plus a note when it is known to be chat-only.
|
|
48
|
+
*
|
|
49
|
+
* Labelled, never filtered or disabled. Choosing a chat-only model to ask questions of is a
|
|
50
|
+
* legitimate thing to do, and a picker that hides half a managed catalogue would be reporting a
|
|
51
|
+
* capability gap as an outage. Only `toolSupport === false` is labelled — `undefined` is a BYOK
|
|
52
|
+
* provider saying nothing, and a suffix there would be an invention.
|
|
53
|
+
*/
|
|
54
|
+
function itemLabel(model: { id: string; name: string }): string {
|
|
55
|
+
return modelToolSupport(model.id) === false ? `${model.name}${NO_TOOLS_SUFFIX}` : model.name;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface ModelPickerOptions {
|
|
59
|
+
/** Host re-render scheduler — called whenever a fetch settles. */
|
|
60
|
+
requestRender: () => void;
|
|
61
|
+
/**
|
|
62
|
+
* The model to show as selected. Defaults to the stored/proxy-preferred one, which is what the
|
|
63
|
+
* chat composer wants; the Import form passes its own draft, which is not a stored setting until
|
|
64
|
+
* the run starts.
|
|
65
|
+
*/
|
|
66
|
+
getModel?: () => string;
|
|
67
|
+
/**
|
|
68
|
+
* Where a choice goes. Defaults to `setModel` (the roaming application preference). The Import
|
|
69
|
+
* form passes its own setter, because choosing a model for one import must not silently retarget
|
|
70
|
+
* the assistant.
|
|
71
|
+
*/
|
|
72
|
+
onChange?: (id: string) => void;
|
|
73
|
+
/** Extra class on the `sp-picker`, so a host can size it without touching this module. */
|
|
74
|
+
className?: string;
|
|
75
|
+
/** Picker size; `"s"` (the composer's) by default. */
|
|
76
|
+
size?: "s" | "m";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface ModelPicker {
|
|
80
|
+
render: () => TemplateResult;
|
|
81
|
+
/** Whether a fetch is in flight — for a host that wants to disable a submit button. */
|
|
82
|
+
isLoading: () => boolean;
|
|
83
|
+
/** The last fetch's failure, or `""`. */
|
|
84
|
+
error: () => string;
|
|
85
|
+
/**
|
|
86
|
+
* Whether the selected model is known NOT to support tools — for a host that wants to say so.
|
|
87
|
+
*
|
|
88
|
+
* `false` while the backend has no opinion, so a host reads it as "warn" rather than as "block".
|
|
89
|
+
*/
|
|
90
|
+
selectedLacksTools: () => boolean;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Create a model picker bound to a host's render scheduler.
|
|
95
|
+
*
|
|
96
|
+
* @param {ModelPickerOptions} opts
|
|
97
|
+
* @returns {ModelPicker}
|
|
98
|
+
*/
|
|
99
|
+
export function createModelPicker(opts: ModelPickerOptions): ModelPicker {
|
|
100
|
+
const getModel = opts.getModel ?? preferredModel;
|
|
101
|
+
const onChange = opts.onChange ?? setModel;
|
|
102
|
+
|
|
103
|
+
let loading = false;
|
|
104
|
+
let failure = "";
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The credentials the last fetch was made FOR, so a failure is not retried on every render.
|
|
108
|
+
*
|
|
109
|
+
* The list itself is deliberately not held here — see the module docblock.
|
|
110
|
+
*/
|
|
111
|
+
let attempted: AiCredentials | null = null;
|
|
112
|
+
|
|
113
|
+
function sameConnection(a: AiCredentials | null, b: AiCredentials): boolean {
|
|
114
|
+
return a !== null && a.apiKey === b.apiKey && a.baseUrl === b.baseUrl;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function ensureModels(force = false) {
|
|
118
|
+
const credentials = aiConnection();
|
|
119
|
+
const settled = cachedModels(credentials) !== null || sameConnection(attempted, credentials);
|
|
120
|
+
if (loading || (settled && !force)) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
loading = true;
|
|
124
|
+
failure = "";
|
|
125
|
+
attempted = credentials;
|
|
126
|
+
fetchAvailableModels({ credentials, force })
|
|
127
|
+
.catch((error: unknown) => {
|
|
128
|
+
failure = (error as Error).message || "Failed to fetch models";
|
|
129
|
+
})
|
|
130
|
+
.finally(() => {
|
|
131
|
+
loading = false;
|
|
132
|
+
opts.requestRender();
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function onPickerChange(e: Event) {
|
|
137
|
+
const { value } = e.target as HTMLInputElement;
|
|
138
|
+
if (value === RETRY_MODELS) {
|
|
139
|
+
attempted = null;
|
|
140
|
+
ensureModels(true);
|
|
141
|
+
// Re-render so the picker snaps back to the current model instead of showing "Retry".
|
|
142
|
+
opts.requestRender();
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (value && value !== LOADING_MODELS) {
|
|
146
|
+
onChange(value);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function render(): TemplateResult {
|
|
151
|
+
ensureModels();
|
|
152
|
+
const current = getModel();
|
|
153
|
+
const listed = cachedModels(aiConnection()) ?? [];
|
|
154
|
+
/* A catalogue that does not contain the current model still has to show it selected — an
|
|
155
|
+
unlisted id is the normal case for a self-hosted or newly released model. */
|
|
156
|
+
const items = listed.some((m) => m.id === current)
|
|
157
|
+
? listed
|
|
158
|
+
: [{ id: current, name: current }, ...listed];
|
|
159
|
+
return html`
|
|
160
|
+
<sp-picker
|
|
161
|
+
size=${opts.size ?? "s"}
|
|
162
|
+
quiet
|
|
163
|
+
class=${opts.className ?? "ai-model-picker"}
|
|
164
|
+
title=${failure ? `Couldn't load models: ${failure}` : "Model"}
|
|
165
|
+
.value=${live(current)}
|
|
166
|
+
@change=${onPickerChange}
|
|
167
|
+
>
|
|
168
|
+
${items.map((m) => html`<sp-menu-item value=${m.id}>${itemLabel(m)}</sp-menu-item>`)}
|
|
169
|
+
${
|
|
170
|
+
loading
|
|
171
|
+
? html`<sp-menu-item disabled value=${LOADING_MODELS}>Loading models…</sp-menu-item>`
|
|
172
|
+
: nothing
|
|
173
|
+
}
|
|
174
|
+
${
|
|
175
|
+
failure
|
|
176
|
+
? html`<sp-menu-item value=${RETRY_MODELS}>Retry loading models</sp-menu-item>`
|
|
177
|
+
: nothing
|
|
178
|
+
}
|
|
179
|
+
</sp-picker>
|
|
180
|
+
`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
error: () => failure,
|
|
185
|
+
isLoading: () => loading,
|
|
186
|
+
render,
|
|
187
|
+
selectedLacksTools: () => modelToolSupport(getModel()) === false,
|
|
188
|
+
};
|
|
189
|
+
}
|