@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
|
@@ -28,6 +28,7 @@ import type {
|
|
|
28
28
|
ExtensionsInfo,
|
|
29
29
|
FsEvent,
|
|
30
30
|
ImportProgressEvent,
|
|
31
|
+
ImportReadyEvent,
|
|
31
32
|
ImportSiteOptions,
|
|
32
33
|
ProjectSchemasResponse,
|
|
33
34
|
ReferencesResult,
|
|
@@ -35,6 +36,7 @@ import type {
|
|
|
35
36
|
SecretsSetRequest,
|
|
36
37
|
SecretsSetResponse,
|
|
37
38
|
SiteBuildResult,
|
|
39
|
+
SitePreviewResult,
|
|
38
40
|
StarterInfo,
|
|
39
41
|
} from "../types";
|
|
40
42
|
import { problemDetail, problemMessage } from "@jxsuite/protocol";
|
|
@@ -62,11 +64,26 @@ interface SiteEntry {
|
|
|
62
64
|
*
|
|
63
65
|
* The adapter is stateless apart from `_projectRoot`, which tracks the server-relative project
|
|
64
66
|
* directory (e.g. "examples/site-demo"). All paths passed INTO PAL methods are project-relative;
|
|
65
|
-
* the adapter prefixes them with `_projectRoot` before hitting the server
|
|
66
|
-
*
|
|
67
|
+
* the adapter prefixes them with `_projectRoot` before hitting the server.
|
|
68
|
+
*
|
|
69
|
+
* **Coming back, the space depends on the route, and the routes do not all agree** (the contract is
|
|
70
|
+
* `specs/server.md` §4.1). `/__studio/files` echoes what it was asked — server-root-relative — so
|
|
71
|
+
* its replies are stripped. The refactor routes (`file/rename`, `references`) answer in the ACTIVE
|
|
72
|
+
* PROJECT's space, because that is the space their sweep runs in, so their replies are returned
|
|
73
|
+
* untouched. Stripping one of those is not a harmless no-op: it would eat the first segment of any
|
|
74
|
+
* path that begins with the project root's own name.
|
|
67
75
|
*/
|
|
68
76
|
export function createDevServerPlatform() {
|
|
69
77
|
let _projectRoot = "";
|
|
78
|
+
/**
|
|
79
|
+
* The in-flight `activate()` for the current root, so a query whose answer depends on the
|
|
80
|
+
* server's project binding can await it.
|
|
81
|
+
*
|
|
82
|
+
* `set projectRoot` fires activation fire-and-forget, and `/__studio/references` is the one route
|
|
83
|
+
* with no `dir` parameter — its sweep root comes solely from the server's activated project. A
|
|
84
|
+
* query landing inside that window would sweep the server's own root instead (issue 239).
|
|
85
|
+
*/
|
|
86
|
+
let _activation: Promise<unknown> | null = null;
|
|
70
87
|
/**
|
|
71
88
|
* Lazy /__studio/collab capability probe (null = not asked yet). Its result is the subprotocol
|
|
72
89
|
* negotiation, not a boolean: the probe is the only place the client can learn what envelope the
|
|
@@ -126,7 +143,7 @@ export function createDevServerPlatform() {
|
|
|
126
143
|
// Fire-and-forget: callers that need the binding in place before reading await activate()
|
|
127
144
|
// Themselves. The rejection is reported rather than dropped — an unactivated root leaves
|
|
128
145
|
// Every rootless endpoint (git especially) operating on the server's OWN root.
|
|
129
|
-
|
|
146
|
+
_activation = this.activate(_projectRoot).catch((error: unknown) => {
|
|
130
147
|
console.error("Project activation failed:", error);
|
|
131
148
|
});
|
|
132
149
|
}
|
|
@@ -335,8 +352,9 @@ export function createDevServerPlatform() {
|
|
|
335
352
|
opts: ImportSiteOptions,
|
|
336
353
|
onProgress: (evt: ImportProgressEvent) => void,
|
|
337
354
|
signal?: AbortSignal,
|
|
355
|
+
onReady?: (evt: ImportReadyEvent) => void,
|
|
338
356
|
) {
|
|
339
|
-
return await streamImport("/__studio/import-site", opts, onProgress, signal);
|
|
357
|
+
return await streamImport("/__studio/import-site", opts, onProgress, signal, onReady);
|
|
340
358
|
},
|
|
341
359
|
|
|
342
360
|
// ─── File operations ──────────────────────────────────────────────────
|
|
@@ -421,21 +439,11 @@ export function createDevServerPlatform() {
|
|
|
421
439
|
if (!res.ok) {
|
|
422
440
|
throw new Error(`Failed to rename: ${from} → ${to}`);
|
|
423
441
|
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
if (typeof report.to === "string") {
|
|
430
|
-
report.to = stripRoot(report.to);
|
|
431
|
-
}
|
|
432
|
-
for (const f of report.references?.files ?? []) {
|
|
433
|
-
f.path = stripRoot(f.path);
|
|
434
|
-
}
|
|
435
|
-
for (const e of report.errors ?? []) {
|
|
436
|
-
e.path = stripRoot(e.path);
|
|
437
|
-
}
|
|
438
|
-
return report;
|
|
442
|
+
/* No strip on the way back: the route takes its paths in server space and answers in the
|
|
443
|
+
project's (specs/server.md §4.1). Stripping an already-project-relative path is not merely
|
|
444
|
+
redundant — it corrupts any path whose first segment happens to match the project root's
|
|
445
|
+
own name, which a server-root-relative root makes reachable. */
|
|
446
|
+
return readJson<RenameResult>(res);
|
|
439
447
|
},
|
|
440
448
|
|
|
441
449
|
/**
|
|
@@ -446,6 +454,7 @@ export function createDevServerPlatform() {
|
|
|
446
454
|
* @param {{ path?: string; tagName?: string }} target
|
|
447
455
|
*/
|
|
448
456
|
async findReferences(target: { path?: string; tagName?: string }): Promise<ReferencesResult> {
|
|
457
|
+
await _activation;
|
|
449
458
|
const params = new URLSearchParams();
|
|
450
459
|
if (target.path) {
|
|
451
460
|
params.set("path", serverPath(target.path));
|
|
@@ -457,18 +466,8 @@ export function createDevServerPlatform() {
|
|
|
457
466
|
if (!res.ok) {
|
|
458
467
|
throw new Error(`Failed to find references: ${target.path ?? target.tagName ?? ""}`);
|
|
459
468
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
if (typeof result.path === "string") {
|
|
463
|
-
result.path = stripRoot(result.path);
|
|
464
|
-
}
|
|
465
|
-
for (const f of result.files ?? []) {
|
|
466
|
-
f.path = stripRoot(f.path);
|
|
467
|
-
}
|
|
468
|
-
for (const e of result.errors ?? []) {
|
|
469
|
-
e.path = stripRoot(e.path);
|
|
470
|
-
}
|
|
471
|
-
return result;
|
|
469
|
+
// Project-relative on the way out, exactly as `renameFile` above receives it.
|
|
470
|
+
return readJson<ReferencesResult>(res);
|
|
472
471
|
},
|
|
473
472
|
|
|
474
473
|
/**
|
|
@@ -627,8 +626,8 @@ export function createDevServerPlatform() {
|
|
|
627
626
|
return Boolean(data.needsInstall);
|
|
628
627
|
},
|
|
629
628
|
|
|
630
|
-
async
|
|
631
|
-
const res = await fetch("/__studio/packages/
|
|
629
|
+
async packageVersions() {
|
|
630
|
+
const res = await fetch("/__studio/packages/versions");
|
|
632
631
|
if (!res.ok) {
|
|
633
632
|
return [];
|
|
634
633
|
}
|
|
@@ -1149,6 +1148,41 @@ export function createDevServerPlatform() {
|
|
|
1149
1148
|
return (await res.json()) as SiteBuildResult;
|
|
1150
1149
|
},
|
|
1151
1150
|
|
|
1151
|
+
/**
|
|
1152
|
+
* Preview the working tree live, and point this project's open tab at a route.
|
|
1153
|
+
*
|
|
1154
|
+
* The dev server holds the same live origin the desktop launchers do, so the browser Studio
|
|
1155
|
+
* gets the same preview: composed on demand, unsaved edits included.
|
|
1156
|
+
*/
|
|
1157
|
+
async previewSite(opts: { route: string }) {
|
|
1158
|
+
const res = await fetch("/__studio/preview", {
|
|
1159
|
+
body: JSON.stringify(opts),
|
|
1160
|
+
headers: { "Content-Type": "application/json" },
|
|
1161
|
+
method: "POST",
|
|
1162
|
+
});
|
|
1163
|
+
if (!res.ok) {
|
|
1164
|
+
const body = await readJson<ErrorBody>(res);
|
|
1165
|
+
throw new Error(problemDetail(body) ?? "The site could not be previewed.");
|
|
1166
|
+
}
|
|
1167
|
+
return (await res.json()) as SitePreviewResult;
|
|
1168
|
+
},
|
|
1169
|
+
|
|
1170
|
+
async setPreviewOverlay(path: string, contents: string) {
|
|
1171
|
+
await fetch("/__studio/preview/overlay", {
|
|
1172
|
+
body: JSON.stringify({ contents, path }),
|
|
1173
|
+
headers: { "Content-Type": "application/json" },
|
|
1174
|
+
method: "POST",
|
|
1175
|
+
});
|
|
1176
|
+
},
|
|
1177
|
+
|
|
1178
|
+
async clearPreviewOverlay(path?: string) {
|
|
1179
|
+
await fetch("/__studio/preview/overlay", {
|
|
1180
|
+
body: JSON.stringify(path === undefined ? {} : { path }),
|
|
1181
|
+
headers: { "Content-Type": "application/json" },
|
|
1182
|
+
method: "DELETE",
|
|
1183
|
+
});
|
|
1184
|
+
},
|
|
1185
|
+
|
|
1152
1186
|
/**
|
|
1153
1187
|
* @param {string} name
|
|
1154
1188
|
* @param {string} url
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Publishing what the author is looking at, so a live preview shows the canvas and not the disk.
|
|
3
|
+
*
|
|
4
|
+
* A live preview composes a page from the project's working TREE, and the tree is what has been
|
|
5
|
+
* saved. The document the author is actually editing lives in a tab's reactive memory and nowhere
|
|
6
|
+
* else, so without this the one thing that makes a live preview worth having — seeing an edit
|
|
7
|
+
* before it is written — would be exactly the thing it could not show.
|
|
8
|
+
*
|
|
9
|
+
* What travels is BYTES, and specifically {@link serializeDocument}'s: the same function `saveFile`
|
|
10
|
+
* writes through, grid controllers, format round-trips, frontmatter and all. That is what makes
|
|
11
|
+
* "what the reader sees" and "what saving would produce" one answer rather than two that drift. A
|
|
12
|
+
* document object would bypass the format layer, and a `.md` page's bytes are not
|
|
13
|
+
* `JSON.stringify(doc)`.
|
|
14
|
+
*
|
|
15
|
+
* **Why an effect and not `transactDoc`.** `transact.ts` has exactly one observer slot and the
|
|
16
|
+
* collab layer owns it. More importantly a transaction hook would MISS three of the most common
|
|
17
|
+
* ways a document changes: a Monaco source commit, a content-entry field edit, and every grid edit
|
|
18
|
+
* route around `transactDoc` entirely. "I edited it in Source and the preview did not change" is
|
|
19
|
+
* the bug this shape avoids.
|
|
20
|
+
*
|
|
21
|
+
* **Why one effect over the whole tab set rather than one per tab.** Reading the map tracks
|
|
22
|
+
* additions, so a tab opened later needs no registration and a tab closed needs no teardown. And
|
|
23
|
+
* the publish is a DIFF against what was last published, which means save, close, discard and undo
|
|
24
|
+
* back to clean all resolve through the same line instead of four hooks that can each be
|
|
25
|
+
* forgotten.
|
|
26
|
+
*
|
|
27
|
+
* **Armed, not always on.** Serializing dirty documents on every edit is not free, and before the
|
|
28
|
+
* author has ever opened a preview there is nothing to publish to. Nothing happens until a preview
|
|
29
|
+
* origin exists.
|
|
30
|
+
*
|
|
31
|
+
* @docs studio/interface
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { effect, effectScope } from "../reactivity";
|
|
35
|
+
import { getPlatform, hasPlatform } from "../platform";
|
|
36
|
+
import { serializeDocument } from "../files/serialize-document";
|
|
37
|
+
import { workspace } from "../workspace/workspace";
|
|
38
|
+
import type { Tab } from "../tabs/tab";
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Trailing debounce on a publish.
|
|
42
|
+
*
|
|
43
|
+
* No leading edge: a leading publish would send the first keystroke of a burst and the last, which
|
|
44
|
+
* is two reloads for one thought. The max wait is what keeps a continuous drag from starving the
|
|
45
|
+
* trailing timer forever.
|
|
46
|
+
*/
|
|
47
|
+
const PUBLISH_DEBOUNCE_MS = 200;
|
|
48
|
+
const PUBLISH_MAX_WAIT_MS = 500;
|
|
49
|
+
|
|
50
|
+
/** Bytes one document may publish before it is left showing its saved state instead. */
|
|
51
|
+
const MAX_ENTRY_BYTES = 2 * 1024 * 1024;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Path to the bytes last published for it, so identical bytes cost nothing.
|
|
55
|
+
*
|
|
56
|
+
* The TEXT rather than a digest of it. An edit that lands back on the same bytes — a character
|
|
57
|
+
* typed and deleted — must not cost the reader a reload, and comparing what was sent is exact where
|
|
58
|
+
* a hash would be a collision away from being wrong for no saving anyone can measure. Memory is
|
|
59
|
+
* bounded by the same per-entry cap the publish is.
|
|
60
|
+
*/
|
|
61
|
+
const published = new Map<string, string>();
|
|
62
|
+
|
|
63
|
+
let armed = false;
|
|
64
|
+
let scope: ReturnType<typeof effectScope> | null = null;
|
|
65
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
66
|
+
let burstStartedAt: number | null = null;
|
|
67
|
+
/** The publish in flight, so a flush awaits it rather than racing it. */
|
|
68
|
+
let inFlight: Promise<void> | null = null;
|
|
69
|
+
|
|
70
|
+
/** The documents whose unsaved bytes a preview needs, in the order they were opened. */
|
|
71
|
+
function dirtyTabs(): Tab[] {
|
|
72
|
+
const tabs: Tab[] = [];
|
|
73
|
+
for (const tab of workspace.tabs.values()) {
|
|
74
|
+
/* Dirty only. A clean tab's bytes are on disk by definition, and publishing them would create
|
|
75
|
+
a second source of truth for identical content. */
|
|
76
|
+
if (tab.doc.dirty && tab.documentPath) {
|
|
77
|
+
tabs.push(tab);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return tabs;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Publish what changed and retract what no longer applies.
|
|
85
|
+
*
|
|
86
|
+
* A diff rather than a stream of events: whatever reason a document has for no longer being unsaved
|
|
87
|
+
* — saved, closed, discarded, undone back to its file — it is simply absent from this pass, and the
|
|
88
|
+
* retraction follows without anywhere else having to remember to ask for it.
|
|
89
|
+
*/
|
|
90
|
+
async function publish(): Promise<void> {
|
|
91
|
+
/* Not `armed` too: `flushPreviewOverlay` refuses when disarmed and `disarmPreviewOverlay` clears
|
|
92
|
+
the pending timer, so there is no path into here without it. A host with no platform at all is
|
|
93
|
+
real though — `getPlatform()` throws rather than answering null. */
|
|
94
|
+
if (!hasPlatform()) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const platform = getPlatform();
|
|
98
|
+
if (!platform.setPreviewOverlay || !platform.clearPreviewOverlay) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const live = new Set<string>();
|
|
102
|
+
for (const tab of dirtyTabs()) {
|
|
103
|
+
const path = tab.documentPath!.replace(/^\.\//, "");
|
|
104
|
+
let text: string;
|
|
105
|
+
try {
|
|
106
|
+
text = await serializeDocument(tab);
|
|
107
|
+
} catch {
|
|
108
|
+
/* A half-typed document that will not serialize is not a reason to stop previewing the layout
|
|
109
|
+
beside it. Skipped, and the next edit that does serialize publishes it. */
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (text.length > MAX_ENTRY_BYTES) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
live.add(path);
|
|
116
|
+
if (published.get(path) === text) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
published.set(path, text);
|
|
120
|
+
await platform.setPreviewOverlay(path, text);
|
|
121
|
+
}
|
|
122
|
+
for (const path of published.keys()) {
|
|
123
|
+
if (!live.has(path)) {
|
|
124
|
+
published.delete(path);
|
|
125
|
+
await platform.clearPreviewOverlay(path);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Run a publish, coalescing with any already scheduled. */
|
|
131
|
+
function schedule(): void {
|
|
132
|
+
if (!armed) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const now = Date.now();
|
|
136
|
+
burstStartedAt ??= now;
|
|
137
|
+
if (timer) {
|
|
138
|
+
clearTimeout(timer);
|
|
139
|
+
}
|
|
140
|
+
const waited = now - burstStartedAt;
|
|
141
|
+
const delay = Math.max(0, Math.min(PUBLISH_DEBOUNCE_MS, PUBLISH_MAX_WAIT_MS - waited));
|
|
142
|
+
timer = setTimeout(() => {
|
|
143
|
+
timer = null;
|
|
144
|
+
burstStartedAt = null;
|
|
145
|
+
inFlight = publish().finally(() => {
|
|
146
|
+
inFlight = null;
|
|
147
|
+
});
|
|
148
|
+
}, delay);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Note an edit the reactive effect cannot see.
|
|
153
|
+
*
|
|
154
|
+
* `transactDoc` replaces the document's ROOT REFERENCE on every transaction, and a Monaco source
|
|
155
|
+
* commit assigns a fresh one, so a shallow read catches both. A content-entry field edit writes a
|
|
156
|
+
* key in place on an already-dirty tab, which changes nothing the effect reads. This is the seam
|
|
157
|
+
* for exactly that: an explicit call from the few places that mutate without swapping.
|
|
158
|
+
*/
|
|
159
|
+
export function notePreviewOverlayEdit(): void {
|
|
160
|
+
schedule();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Start publishing, because a preview origin now exists to publish to.
|
|
165
|
+
*
|
|
166
|
+
* Idempotent: `View: Open in Browser` calls it on every invocation and only the first does work.
|
|
167
|
+
*/
|
|
168
|
+
export function armPreviewOverlay(): void {
|
|
169
|
+
if (armed) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
armed = true;
|
|
173
|
+
scope = effectScope();
|
|
174
|
+
scope.run(() => {
|
|
175
|
+
effect(() => {
|
|
176
|
+
for (const tab of workspace.tabs.values()) {
|
|
177
|
+
/* Two reads, both shallow. `document` is a fresh reference on every transaction — see
|
|
178
|
+
`transactDoc`, which assigns one specifically so effects re-run — so this costs a
|
|
179
|
+
property read rather than a walk of the document. */
|
|
180
|
+
void tab.doc.dirty;
|
|
181
|
+
void tab.doc.document;
|
|
182
|
+
}
|
|
183
|
+
schedule();
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Stop publishing and forget what was published, without retracting it.
|
|
190
|
+
*
|
|
191
|
+
* Called when the window changes project. The published map is keyed by project-relative path, so
|
|
192
|
+
* carrying it across a switch would let one project's `pages/index.json` be read as the answer for
|
|
193
|
+
* another's — and the backend lets go of the old root's overlay on its own re-root anyway.
|
|
194
|
+
*/
|
|
195
|
+
export function disarmPreviewOverlay(): void {
|
|
196
|
+
armed = false;
|
|
197
|
+
if (timer) {
|
|
198
|
+
clearTimeout(timer);
|
|
199
|
+
timer = null;
|
|
200
|
+
}
|
|
201
|
+
burstStartedAt = null;
|
|
202
|
+
scope?.stop();
|
|
203
|
+
scope = null;
|
|
204
|
+
published.clear();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Publish now and wait for it.
|
|
209
|
+
*
|
|
210
|
+
* Two callers, and both are moments where the debounce is exactly wrong: on the way to opening a
|
|
211
|
+
* browser tab, so the page the author is about to look at already carries their last keystroke; and
|
|
212
|
+
* after a save, so the retraction rides with the write instead of arriving a debounce later and
|
|
213
|
+
* costing a second reload.
|
|
214
|
+
*/
|
|
215
|
+
export async function flushPreviewOverlay(): Promise<void> {
|
|
216
|
+
if (!armed) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (timer) {
|
|
220
|
+
clearTimeout(timer);
|
|
221
|
+
timer = null;
|
|
222
|
+
burstStartedAt = null;
|
|
223
|
+
}
|
|
224
|
+
await inFlight;
|
|
225
|
+
await publish();
|
|
226
|
+
}
|
|
@@ -26,6 +26,7 @@ import { activeRegistry } from "../commands/active-registry";
|
|
|
26
26
|
import { currentDeploy, noteDeployment } from "./deploy-checklist";
|
|
27
27
|
import { getPlatform } from "../platform";
|
|
28
28
|
import { getCfToken, setCfToken } from "../services/cf-settings";
|
|
29
|
+
import { resetModelCache } from "../services/ai-models";
|
|
29
30
|
import { projectState } from "../store";
|
|
30
31
|
import type { CfConnection } from "../types";
|
|
31
32
|
import { openModal } from "../ui/layers";
|
|
@@ -76,7 +77,13 @@ async function loadConnection(): Promise<void> {
|
|
|
76
77
|
render();
|
|
77
78
|
try {
|
|
78
79
|
_connection = (await getPlatform().cfConnection?.()) ?? null;
|
|
79
|
-
|
|
80
|
+
/*
|
|
81
|
+
* A lapsed brokered connection is `connected: true`, and asking Cloudflare anything through one
|
|
82
|
+
* is a guaranteed 401. Every such call landed in the catch below, which nulled the connection —
|
|
83
|
+
* so the panel showed the FIRST-TIME "Connect Cloudflare" invitation next to a raw
|
|
84
|
+
* "Cloudflare API: …" string, and never the one sentence that was true: it expired.
|
|
85
|
+
*/
|
|
86
|
+
if (_connection?.connected && !_connection.needsReconnect) {
|
|
80
87
|
_accounts = await listAccounts();
|
|
81
88
|
_form.accountId = _connection.accountId ?? _accounts[0]?.id ?? "";
|
|
82
89
|
const deploy = currentDeploy();
|
|
@@ -122,15 +129,37 @@ function openAccounts(): void {
|
|
|
122
129
|
void activeRegistry()?.run("app.preferences", { section: "accounts" });
|
|
123
130
|
}
|
|
124
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Run the hosted OAuth flow and act on how it ended.
|
|
134
|
+
*
|
|
135
|
+
* The four endings are not interchangeable, and collapsing them is what left a user staring at an
|
|
136
|
+
* unchanged panel: a deadline that passed and a popup the browser turned into a full-page redirect
|
|
137
|
+
* both resolved null, and both were reported as "not completed" — one of them while the document
|
|
138
|
+
* was already navigating away.
|
|
139
|
+
*/
|
|
125
140
|
async function hostedConnect(): Promise<void> {
|
|
126
141
|
_busy = true;
|
|
142
|
+
_error = "";
|
|
127
143
|
render();
|
|
128
144
|
try {
|
|
129
|
-
await getPlatform().cfConnect?.();
|
|
145
|
+
const outcome = (await getPlatform().cfConnect?.()) ?? null;
|
|
146
|
+
if (outcome?.status === "timeout") {
|
|
147
|
+
_error =
|
|
148
|
+
"The Cloudflare window didn't finish. Sign in there, then reconnect — nothing was changed.";
|
|
149
|
+
} else if (outcome?.status === "connected" && !outcome.connection.accountId) {
|
|
150
|
+
/* Lazily: this module is the publish modal, and the picker drags the dialog layer with it. */
|
|
151
|
+
const { openCfAccountPicker } = await import("../ui/cf-account-picker");
|
|
152
|
+
await openCfAccountPicker();
|
|
153
|
+
}
|
|
154
|
+
/* `canceled` and `redirect` say nothing. The user closed the popup, or the page is on its way
|
|
155
|
+
to Cloudflare — an apology painted over either one is noise. */
|
|
130
156
|
} catch (error) {
|
|
131
157
|
_error = error instanceof Error ? error.message : String(error);
|
|
132
158
|
}
|
|
133
159
|
_busy = false;
|
|
160
|
+
// The assistant's gate reads the same grant through /models, so a reconnect it does not hear
|
|
161
|
+
// About leaves it insisting the connection is dead.
|
|
162
|
+
resetModelCache();
|
|
134
163
|
await loadConnection();
|
|
135
164
|
}
|
|
136
165
|
|
|
@@ -194,6 +223,32 @@ function fieldRow(label: string, input: unknown) {
|
|
|
194
223
|
`;
|
|
195
224
|
}
|
|
196
225
|
|
|
226
|
+
/**
|
|
227
|
+
* The connection exists and has lapsed.
|
|
228
|
+
*
|
|
229
|
+
* Its own template rather than a line inside {@link credentialTpl}, because the honest words are the
|
|
230
|
+
* opposite of that one's: nothing needs to be set up, nothing was lost, and the only action is to
|
|
231
|
+
* sign in again. The panel used to say "Connect your Cloudflare account to publish this site" to a
|
|
232
|
+
* user who had already done exactly that.
|
|
233
|
+
*/
|
|
234
|
+
function lapsedTpl() {
|
|
235
|
+
return html`
|
|
236
|
+
<p>
|
|
237
|
+
Your Cloudflare connection has expired, so publishing cannot reach your account. Reconnect to
|
|
238
|
+
restore it — this site's Pages project and its settings are untouched.
|
|
239
|
+
</p>
|
|
240
|
+
<sp-button
|
|
241
|
+
variant="accent"
|
|
242
|
+
?disabled=${_busy}
|
|
243
|
+
@click=${() => {
|
|
244
|
+
void hostedConnect();
|
|
245
|
+
}}
|
|
246
|
+
>
|
|
247
|
+
${_busy ? "Reconnecting…" : "Reconnect Cloudflare"}
|
|
248
|
+
</sp-button>
|
|
249
|
+
`;
|
|
250
|
+
}
|
|
251
|
+
|
|
197
252
|
function credentialTpl() {
|
|
198
253
|
const platform = getPlatform();
|
|
199
254
|
if (platform.cfConnect) {
|
|
@@ -393,6 +448,11 @@ function bodyTpl() {
|
|
|
393
448
|
if (_connection === "loading") {
|
|
394
449
|
return html`<p>Checking Cloudflare connection…</p>`;
|
|
395
450
|
}
|
|
451
|
+
// Before the credential template, and that order is the fix: a lapsed row is `connected: true`,
|
|
452
|
+
// So neither branch below could ever have claimed it.
|
|
453
|
+
if (_connection?.connected && _connection.needsReconnect) {
|
|
454
|
+
return lapsedTpl();
|
|
455
|
+
}
|
|
396
456
|
if (!_connection?.connected) {
|
|
397
457
|
return credentialTpl();
|
|
398
458
|
}
|