@jxsuite/studio 2.4.3 → 4.0.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/Overlay.dev-723bv0df.js +23 -0
- package/dist/chunks/cf-settings-45fgyx8y.js +21 -0
- package/dist/chunks/{components-z6cnkyy1.js → components-zmx10t62.js} +6 -5
- package/dist/chunks/content-source-t88vb2dr.js +49 -0
- package/dist/chunks/{convert-to-repeater-k10dvcc7.js → convert-to-repeater-dhw1nrz9.js} +27 -18
- package/dist/chunks/entry-commands-6z6bhxg0.js +46 -0
- package/dist/chunks/{extension-sections-0ada0xcw.js → extension-sections-3ydgjbdy.js} +32 -23
- package/dist/chunks/file-ops-rskmhxyq.js +48 -0
- package/dist/chunks/files-0cratsr0.js +92 -0
- package/dist/chunks/{format-host-wewba4nq.js → format-host-tgcmfs4p.js} +3 -3
- package/dist/chunks/fs-events-hrrg328g.js +21 -0
- package/dist/chunks/{git-panel-2dh1aw50.js → git-panel-0yywyk76.js} +21 -15
- package/dist/chunks/{jx-validate-jj9rnt4n.js → jx-validate-8q2g05x1.js} +6 -4
- package/dist/chunks/layers-panel-z3tqe5cd.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-rws7k4br.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-54jvhskq.js +28 -0
- package/dist/chunks/quick-search-cgfmjwq5.js +65 -0
- package/dist/chunks/references-3qx8fkyp.js +29 -0
- package/dist/chunks/right-panel-cqp8bc4v.js +46 -0
- package/dist/chunks/sp-overlay.dev-yztmtrs1.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-xcvpnqr0.js +15 -0
- package/dist/chunks/{sp-tray.dev-64v4pxw8.js → sp-tray.dev-tkbscm06.js} +5 -5
- package/dist/chunks/{studio-f5yyvqxv.js → studio-1hbqnt19.js} +37 -31
- package/dist/chunks/studio-2zkz8evn.js +3163 -0
- package/dist/chunks/{studio-0b6fs05q.js → studio-4pqqxbrt.js} +23 -15
- package/dist/chunks/{studio-kvfx9z2c.js → studio-4rzyrtjz.js} +70 -31
- package/dist/chunks/{studio-ef8t53vc.js → studio-5q69gfag.js} +60 -56
- package/dist/chunks/{studio-v2c5pmjj.js → studio-5rywrkxz.js} +565 -133
- package/dist/chunks/{studio-6kv3apa9.js → studio-68n67094.js} +25 -9
- package/dist/chunks/{studio-0rqqhrdy.js → studio-7nte2hj4.js} +2 -2
- package/dist/chunks/{studio-hyn3vhwr.js → studio-8swrjbny.js} +31 -18
- package/dist/chunks/studio-92ab2jqe.js +282 -0
- package/dist/chunks/{studio-sas7mt3k.js → studio-9ab8gqsm.js} +2 -2
- package/dist/chunks/{studio-28xc8ws4.js → studio-9c4k4vhv.js} +14 -10
- package/dist/chunks/studio-9n4mrmnb.js +184 -0
- package/dist/chunks/{studio-tr20m8g8.js → studio-9r6j5ev1.js} +37 -2
- package/dist/chunks/{studio-aweyr4v1.js → studio-ahjfdy6e.js} +9 -7
- package/dist/chunks/{studio-s519zc99.js → studio-bgxt93ga.js} +3 -3
- package/dist/chunks/{studio-26sv44br.js → studio-cb12yz9w.js} +8543 -8784
- package/dist/chunks/studio-cft9r4r7.js +119 -0
- package/dist/chunks/{studio-rqemfhz6.js → studio-eenfkt4e.js} +6 -6
- package/dist/chunks/studio-eesa1myv.js +498 -0
- package/dist/chunks/{studio-ay8wcr2b.js → studio-eqq1w638.js} +7 -9
- package/dist/chunks/{studio-ke244pam.js → studio-ex8zyp98.js} +51 -25
- package/dist/chunks/studio-gv8vvshc.js +118 -0
- package/dist/chunks/studio-hbtp0eh1.js +1315 -0
- package/dist/chunks/{studio-cz2j1c0g.js → studio-htsf0w6m.js} +75 -123
- package/dist/chunks/{studio-pyrw8h0w.js → studio-kjjsjak4.js} +528 -212
- package/dist/chunks/{studio-5x5774ak.js → studio-kryvsw6j.js} +3 -3
- package/dist/chunks/studio-kyh9avvv.js +28 -0
- package/dist/chunks/{studio-jnakhg87.js → studio-kyk2v92a.js} +14 -14
- package/dist/chunks/{studio-twsafr1e.js → studio-m3hwn7y1.js} +3 -3
- package/dist/chunks/{studio-tzx3xmvx.js → studio-mmv72a27.js} +8 -8
- 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-sccge2wf.js → studio-p8j86wag.js} +2 -2
- package/dist/chunks/{studio-v1va0vhd.js → studio-pdfjntbk.js} +75 -139
- package/dist/chunks/studio-q43a5y9h.js +394 -0
- package/dist/chunks/studio-q7v10cg2.js +106 -0
- package/dist/chunks/{studio-2w11grav.js → studio-qq6jym04.js} +3 -3
- package/dist/chunks/studio-r1r7afxx.js +1066 -0
- 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-sf7zjgv1.js +91 -0
- package/dist/chunks/studio-t7qb9db8.js +665 -0
- package/dist/chunks/studio-tmc5emrw.js +8 -0
- package/dist/chunks/{studio-wdt9y92a.js → studio-txwkm6p9.js} +2 -2
- package/dist/chunks/{studio-dgj7gq44.js → studio-v38hmvx1.js} +7 -7
- package/dist/chunks/{studio-7ejemhej.js → studio-vadzgkrz.js} +6 -6
- package/dist/chunks/studio-wcy8trfx.js +263 -0
- package/dist/chunks/{studio-hb7c46jy.js → studio-wx5609xd.js} +21 -15
- package/dist/chunks/studio-xrapxwwc.js +1539 -0
- package/dist/chunks/{studio-ws6ef6js.js → studio-yxrkyv8a.js} +41 -29
- package/dist/chunks/{studio-xm3dbxvn.js → studio-zczps70q.js} +2 -2
- package/dist/chunks/workspace-xpres40v.js +89 -0
- package/dist/iframe-entry.js +450 -48
- package/dist/studio.js +2455 -1761
- package/package.json +16 -15
- package/src/browse/library-layouts.ts +3 -3
- package/src/browse/library-pane.ts +59 -5
- package/src/browse/library-preview.ts +19 -3
- package/src/canvas/asset-refs.ts +119 -0
- package/src/canvas/asset-resolve.ts +256 -0
- package/src/canvas/canvas-live-render.ts +23 -15
- package/src/canvas/canvas-render.ts +23 -1
- package/src/canvas/iframe-entry.ts +1 -0
- package/src/canvas/iframe-host.ts +4 -0
- package/src/canvas/iframe-patch.ts +47 -21
- package/src/canvas/iframe-protocol.ts +9 -0
- package/src/canvas/iframe-render.ts +50 -16
- 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/file-drop-action.ts +2 -1
- package/src/editor/shortcuts.ts +5 -0
- package/src/files/file-ops.ts +10 -42
- package/src/files/files.ts +487 -61
- package/src/files/fs-events.ts +9 -0
- package/src/files/gitignore.ts +432 -0
- package/src/files/media-paths.ts +37 -14
- package/src/files/media-upload.ts +47 -6
- 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-idle.ts +45 -0
- package/src/grid/grid-view.ts +35 -0
- package/src/grid/schema-columns.ts +2 -1
- package/src/grid/sources/content-source.ts +24 -3
- package/src/media/media-open.ts +58 -0
- package/src/media/media-pane.ts +310 -0
- package/src/new-project/import-tab.ts +333 -160
- 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 +21 -80
- package/src/panels/ai-panel.ts +116 -2
- package/src/panels/frontmatter-fields.ts +6 -2
- package/src/panels/navigator-panels.ts +7 -6
- package/src/panels/properties-panel.ts +8 -2
- package/src/panels/seo-modal.ts +5 -6
- package/src/panels/settings-menu.ts +799 -0
- package/src/panels/signals-panel.ts +2 -1
- package/src/panels/toolbar.ts +135 -15
- package/src/platforms/cloud.ts +34 -3
- package/src/platforms/devserver.ts +72 -34
- package/src/preview/preview-overlay.ts +226 -0
- package/src/services/ai-ask.ts +247 -0
- package/src/services/ai-import-tools.ts +494 -0
- package/src/services/ai-models.ts +101 -11
- package/src/services/ai-project-tools.ts +20 -26
- package/src/services/ai-settings.ts +69 -71
- package/src/services/ai-system-prompt.ts +88 -2
- package/src/services/ai-turn-signal.ts +70 -0
- package/src/services/cf-settings.ts +19 -37
- package/src/services/connection.ts +74 -0
- package/src/services/context-manager.ts +89 -0
- package/src/services/document-assistant.ts +49 -7
- package/src/services/idle.ts +6 -1
- 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 +181 -0
- package/src/services/settings/kernel.ts +335 -0
- package/src/services/settings/write-queue.ts +116 -0
- package/src/services/tool-executor.ts +34 -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 +6 -19
- package/src/settings/preferences-dialog.ts +18 -41
- package/src/settings/preferences-keymap.ts +8 -18
- package/src/settings/preferences-sections.ts +57 -0
- package/src/settings/schema-field-ui.ts +2 -2
- package/src/settings/settings-document.ts +65 -9
- package/src/shell.ts +6 -14
- package/src/studio.ts +65 -7
- package/src/tabs/transact.ts +42 -19
- package/src/types.ts +164 -8
- package/src/ui/ai-credentials-form.ts +52 -27
- package/src/ui/ai-model-picker.ts +162 -0
- package/src/ui/choice-field.ts +72 -0
- package/src/ui/layers.ts +83 -5
- package/src/ui/media-picker.ts +6 -11
- package/src/utils/studio-utils.ts +43 -31
- package/styles/forced-colors.css +7 -0
- package/styles/overlays.css +256 -0
- package/styles/panels.css +13 -1
- package/styles/shell.css +111 -0
- package/dist/chunks/Overlay.dev-d2ajy6sj.js +0 -23
- package/dist/chunks/cf-settings-g5fbf2zp.js +0 -17
- package/dist/chunks/content-source-k9724vcv.js +0 -39
- package/dist/chunks/files-yew27sn9.js +0 -69
- package/dist/chunks/layers-panel-qnsghwwd.js +0 -59
- package/dist/chunks/publish-panel-a0pdft5w.js +0 -21
- package/dist/chunks/quick-search-9avyc4pc.js +0 -54
- package/dist/chunks/right-panel-kcnhtc5j.js +0 -38
- 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-a7pzveq1.js +0 -110
- package/dist/chunks/studio-c31yb4t6.js +0 -55
- package/dist/chunks/studio-ra7ar50f.js +0 -6682
- package/src/canvas/content-assets.ts +0 -263
- package/src/canvas/site-style-css.ts +0 -121
- package/src/services/settings-store.ts +0 -99
|
@@ -20,8 +20,9 @@ import type { ToolRegistry, ToolResult } from "@jxsuite/ai/tools";
|
|
|
20
20
|
import type { JxMutableNode, ProjectConfig } from "@jxsuite/schema/types";
|
|
21
21
|
import { getPlatform } from "../platform";
|
|
22
22
|
import { workspace } from "../workspace/workspace";
|
|
23
|
+
import { projectState } from "../store";
|
|
23
24
|
import type { Tab } from "../tabs/tab";
|
|
24
|
-
import {
|
|
25
|
+
import { adoptCreatedProject } from "./project-adoption";
|
|
25
26
|
import { translateValidationError } from "./ai-tools";
|
|
26
27
|
import { validateDoc, validateProjectConfig } from "./jx-validate";
|
|
27
28
|
import { recordWrite } from "./ai-writes";
|
|
@@ -355,8 +356,16 @@ export function registerProjectTools(
|
|
|
355
356
|
/* The per-project entry document closes the composition over every enabled extension
|
|
356
357
|
(extensions.md §5.2), so this is what catches a typo'd section key or a misshapen
|
|
357
358
|
extension section. Without it the model shipped config its own tool call called clean
|
|
358
|
-
and Monaco flagged the moment a human opened the file.
|
|
359
|
-
|
|
359
|
+
and Monaco flagged the moment a human opened the file.
|
|
360
|
+
|
|
361
|
+
Errors the file ALREADY had are subtracted, the same way `services/ai-tools.ts`
|
|
362
|
+
subtracts them from a document edit: a project whose config was written by something
|
|
363
|
+
else — an importer, a hand edit, an extension that was removed — would otherwise refuse
|
|
364
|
+
every write the model attempted, blaming it for a violation it did not introduce and
|
|
365
|
+
cannot reach from the edit it was asked to make. */
|
|
366
|
+
const before = new Set(await validateProject(projectState?.projectConfig ?? {}));
|
|
367
|
+
const found = await validateProject(parsedConfig);
|
|
368
|
+
const configErrors = found.filter((error) => !before.has(error));
|
|
360
369
|
if (configErrors.length > 0) {
|
|
361
370
|
const formatted = configErrors
|
|
362
371
|
.map((e) => `- ${translateValidationError(e)}`)
|
|
@@ -585,30 +594,15 @@ export function registerProjectTools(
|
|
|
585
594
|
}
|
|
586
595
|
|
|
587
596
|
/*
|
|
588
|
-
*
|
|
589
|
-
*
|
|
590
|
-
*
|
|
591
|
-
|
|
592
|
-
const wasBatching = isBatching();
|
|
593
|
-
if (wasBatching) {
|
|
594
|
-
endBatch();
|
|
595
|
-
}
|
|
596
|
-
let adoptionError: string | null = null;
|
|
597
|
-
try {
|
|
598
|
-
await adoptProject(result.root);
|
|
599
|
-
} catch (error) {
|
|
600
|
-
adoptionError = error instanceof Error ? error.message : String(error);
|
|
601
|
-
}
|
|
602
|
-
if (wasBatching) {
|
|
603
|
-
beginBatch(getTab());
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
/*
|
|
607
|
-
* The adopter (openRecentProject) swallows failures into a status message, so a resolved
|
|
608
|
-
* promise is not proof of adoption — verify against the workspace before re-keying the
|
|
609
|
-
* chat session.
|
|
597
|
+
* Git init, then the full project-open flow, then a check that it landed — all three in
|
|
598
|
+
* `services/project-adoption.ts`, because `import_site` is the same act with a different
|
|
599
|
+
* backend. Git init in particular was missing here: `specs/desktop.md` §4.5 promises every
|
|
600
|
+
* create path initialises a repository, and only the New Project modal was keeping it.
|
|
610
601
|
*/
|
|
611
|
-
const adopted =
|
|
602
|
+
const { adopted, error: adoptionError } = await adoptCreatedProject(result.root, {
|
|
603
|
+
adopt: adoptProject,
|
|
604
|
+
getTab,
|
|
605
|
+
});
|
|
612
606
|
if (adopted) {
|
|
613
607
|
onProjectAdopted?.(result.root);
|
|
614
608
|
return {
|
|
@@ -1,52 +1,51 @@
|
|
|
1
1
|
/// <reference lib="dom" />
|
|
2
2
|
/**
|
|
3
|
-
* Ai-settings.js —
|
|
3
|
+
* Ai-settings.js — the AI assistant's provider settings, over the settings kernel.
|
|
4
4
|
*
|
|
5
5
|
* The Stack B proxy reads the OpenAI key from the `X-Api-Key` header (falling back to the server's
|
|
6
|
-
* `OPENAI_API_KEY` env var). Studio stores a user-supplied key
|
|
7
|
-
*
|
|
6
|
+
* `OPENAI_API_KEY` env var). Studio stores a user-supplied key so the browser/dev build works
|
|
7
|
+
* without an env var. The key never leaves the machine except to the same-origin proxy.
|
|
8
|
+
*
|
|
9
|
+
* This module is now a typed façade over `settings/kernel.ts` — the storage keys, defaults and
|
|
10
|
+
* normalisation live in `settings/definitions.ts`, and the kernel owns the cache, the write queue
|
|
11
|
+
* and change notification. What it keeps is the vocabulary: callers say `getOpenAiKey()`, not
|
|
12
|
+
* `readSetting(SETTINGS.aiOpenAiKey)`.
|
|
13
|
+
*
|
|
14
|
+
* **Blank no longer deletes.** `setOpenAiKey("")` stores an empty string; {@link clearAiProvider}
|
|
15
|
+
* is the revoke path. The old conflation read as a convenience right up until a form that blanked
|
|
16
|
+
* its own drafts on Save called these setters with them, and a second Save revoked the credentials
|
|
17
|
+
* the first had stored.
|
|
8
18
|
*
|
|
9
19
|
* @license MIT
|
|
10
20
|
*/
|
|
11
21
|
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
22
|
+
import { SETTINGS } from "./settings/definitions";
|
|
23
|
+
import {
|
|
24
|
+
clearSettings,
|
|
25
|
+
hasSetting,
|
|
26
|
+
readStoredSetting,
|
|
27
|
+
setSetting,
|
|
28
|
+
setSettings,
|
|
29
|
+
} from "./settings/kernel";
|
|
18
30
|
|
|
19
|
-
/** @returns {string} The stored OpenAI key, or "" if none
|
|
31
|
+
/** @returns {string} The stored OpenAI key, or "" if none. */
|
|
20
32
|
export function getOpenAiKey() {
|
|
21
|
-
|
|
22
|
-
return globalThis.localStorage?.getItem(KEY_STORAGE) ?? "";
|
|
23
|
-
} catch {
|
|
24
|
-
return "";
|
|
25
|
-
}
|
|
33
|
+
return readStoredSetting(SETTINGS.aiOpenAiKey);
|
|
26
34
|
}
|
|
27
35
|
|
|
28
36
|
/**
|
|
29
|
-
* Persist
|
|
37
|
+
* Persist the OpenAI key.
|
|
30
38
|
*
|
|
31
|
-
* @param {string} key - The key to store
|
|
39
|
+
* @param {string} key - The key to store. A blank value stores blank; it does NOT clear — see
|
|
40
|
+
* {@link clearAiProvider}.
|
|
32
41
|
*/
|
|
33
42
|
export function setOpenAiKey(key: string) {
|
|
34
|
-
|
|
35
|
-
const trimmed = (key || "").trim();
|
|
36
|
-
if (trimmed) {
|
|
37
|
-
globalThis.localStorage?.setItem(KEY_STORAGE, trimmed);
|
|
38
|
-
} else {
|
|
39
|
-
globalThis.localStorage?.removeItem(KEY_STORAGE);
|
|
40
|
-
}
|
|
41
|
-
persistSettings();
|
|
42
|
-
} catch {
|
|
43
|
-
/* LocalStorage unavailable — settings are not persisted. */
|
|
44
|
-
}
|
|
43
|
+
setSetting(SETTINGS.aiOpenAiKey, key || "");
|
|
45
44
|
}
|
|
46
45
|
|
|
47
46
|
/** @returns {boolean} Whether a non-empty key is stored. */
|
|
48
47
|
export function hasOpenAiKey() {
|
|
49
|
-
return
|
|
48
|
+
return hasSetting(SETTINGS.aiOpenAiKey);
|
|
50
49
|
}
|
|
51
50
|
|
|
52
51
|
/**
|
|
@@ -54,59 +53,58 @@ export function hasOpenAiKey() {
|
|
|
54
53
|
* or "" to use the proxy's default (`https://api.openai.com/v1` / server `OPENAI_BASE_URL`).
|
|
55
54
|
*/
|
|
56
55
|
export function getBaseUrl() {
|
|
57
|
-
|
|
58
|
-
return globalThis.localStorage?.getItem(BASE_URL_STORAGE) ?? "";
|
|
59
|
-
} catch {
|
|
60
|
-
return "";
|
|
61
|
-
}
|
|
56
|
+
return readStoredSetting(SETTINGS.aiBaseUrl);
|
|
62
57
|
}
|
|
63
58
|
|
|
59
|
+
// ─── Model selection ────────────────────────────────────────────────────────
|
|
60
|
+
|
|
64
61
|
/**
|
|
65
|
-
*
|
|
62
|
+
* The model the user actually chose, with no default standing in for silence.
|
|
66
63
|
*
|
|
67
|
-
*
|
|
64
|
+
* The getter this replaced masked _unset_ as `"gpt-4o"`, which is right for a sender and wrong for
|
|
65
|
+
* anything that writes back: a form prefilled from it and then saved persists a choice nobody made,
|
|
66
|
+
* which is how `jx.ai.model: "gpt-4o"` came to be the only surviving key in a settings file whose
|
|
67
|
+
* owner had configured a different provider entirely. Senders want
|
|
68
|
+
* {@link ../services/ai-models!preferredModel} instead.
|
|
69
|
+
*
|
|
70
|
+
* @returns {string} The stored model ID, or "" when none has been chosen.
|
|
68
71
|
*/
|
|
69
|
-
export function
|
|
70
|
-
|
|
71
|
-
const trimmed = (url || "").trim().replace(/\/+$/, "");
|
|
72
|
-
if (trimmed) {
|
|
73
|
-
globalThis.localStorage?.setItem(BASE_URL_STORAGE, trimmed);
|
|
74
|
-
} else {
|
|
75
|
-
globalThis.localStorage?.removeItem(BASE_URL_STORAGE);
|
|
76
|
-
}
|
|
77
|
-
persistSettings();
|
|
78
|
-
} catch {
|
|
79
|
-
/* LocalStorage unavailable — settings are not persisted. */
|
|
80
|
-
}
|
|
72
|
+
export function storedModel() {
|
|
73
|
+
return readStoredSetting(SETTINGS.aiModel);
|
|
81
74
|
}
|
|
82
75
|
|
|
83
|
-
|
|
76
|
+
/**
|
|
77
|
+
* Persist the selected model ID.
|
|
78
|
+
*
|
|
79
|
+
* @param {string} modelId - The model ID to store. A blank value stores blank; it does NOT clear.
|
|
80
|
+
*/
|
|
81
|
+
export function setModel(modelId: string) {
|
|
82
|
+
setSetting(SETTINGS.aiModel, modelId || "");
|
|
83
|
+
}
|
|
84
84
|
|
|
85
|
-
/**
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
85
|
+
/**
|
|
86
|
+
* Store the provider's key, endpoint and model as ONE change.
|
|
87
|
+
*
|
|
88
|
+
* What a credentials form's Save should call. Three separate setters coalesce into one write
|
|
89
|
+
* anyway, but they announce three times, and each intermediate announcement describes a provider
|
|
90
|
+
* the user never asked for — a new key against the old endpoint, then against no model.
|
|
91
|
+
*
|
|
92
|
+
* @param {{ apiKey: string; baseUrl: string; model: string }} provider
|
|
93
|
+
*/
|
|
94
|
+
export function saveAiProvider(provider: { apiKey: string; baseUrl: string; model: string }) {
|
|
95
|
+
setSettings([
|
|
96
|
+
[SETTINGS.aiOpenAiKey, provider.apiKey],
|
|
97
|
+
[SETTINGS.aiBaseUrl, provider.baseUrl],
|
|
98
|
+
[SETTINGS.aiModel, provider.model],
|
|
99
|
+
]);
|
|
92
100
|
}
|
|
93
101
|
|
|
94
102
|
/**
|
|
95
|
-
*
|
|
103
|
+
* Forget the provider entirely — key, endpoint and model.
|
|
96
104
|
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
105
|
+
* The one deletion path, and the one the Accounts list's Disconnect uses. Deliberately all three
|
|
106
|
+
* together: a key with an orphaned endpoint is a state no surface knows how to describe.
|
|
99
107
|
*/
|
|
100
|
-
export function
|
|
101
|
-
|
|
102
|
-
const trimmed = (modelId || "").trim();
|
|
103
|
-
if (trimmed) {
|
|
104
|
-
globalThis.localStorage?.setItem(MODEL_STORAGE, trimmed);
|
|
105
|
-
} else {
|
|
106
|
-
globalThis.localStorage?.removeItem(MODEL_STORAGE);
|
|
107
|
-
}
|
|
108
|
-
persistSettings();
|
|
109
|
-
} catch {
|
|
110
|
-
/* LocalStorage unavailable — settings are not persisted. */
|
|
111
|
-
}
|
|
108
|
+
export function clearAiProvider() {
|
|
109
|
+
clearSettings([SETTINGS.aiOpenAiKey, SETTINGS.aiBaseUrl, SETTINGS.aiModel]);
|
|
112
110
|
}
|
|
@@ -29,6 +29,12 @@ interface BuildSystemPromptOptions {
|
|
|
29
29
|
* prompt-shape tests — advertises the same list it always did.
|
|
30
30
|
*/
|
|
31
31
|
treeEditable?: boolean | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* Whether the platform can import a site. Defaults TRUE so this stays a pure function of what it
|
|
34
|
+
* is given — a caller that knows nothing about the platform gets the full list, and only
|
|
35
|
+
* `document-assistant.ts`, which does know, narrows it.
|
|
36
|
+
*/
|
|
37
|
+
canImport?: boolean | undefined;
|
|
32
38
|
/** Project-relative file paths for the inventory section (project modes; capped). */
|
|
33
39
|
fileInventory?: string[] | undefined;
|
|
34
40
|
}
|
|
@@ -52,13 +58,21 @@ const FILE_INVENTORY_CAP = 100;
|
|
|
52
58
|
* a weaker test than `structurallyEditable`, so a repeater template or `$switch` case was removable
|
|
53
59
|
* by the agent and not by the person.
|
|
54
60
|
*/
|
|
55
|
-
export type AiToolTier = "no-project" | "project" | "document" | "document-tree";
|
|
61
|
+
export type AiToolTier = "always" | "no-project" | "project" | "document" | "document-tree";
|
|
56
62
|
|
|
57
63
|
export interface AiToolInfo {
|
|
58
64
|
name: string;
|
|
59
65
|
tier: AiToolTier;
|
|
60
66
|
/** One-line signature + purpose shown in the system prompt's tool list. */
|
|
61
67
|
blurb: string;
|
|
68
|
+
/**
|
|
69
|
+
* A PAL member the tool needs, beyond whatever its tier requires.
|
|
70
|
+
*
|
|
71
|
+
* The tier answers "what is open"; this answers "can this backend do it at all". Cloud ships no
|
|
72
|
+
* `importSite`, so `import_site` must not be advertised there — and a tier cannot express that,
|
|
73
|
+
* because "no project is open" is exactly as true on cloud as anywhere else.
|
|
74
|
+
*/
|
|
75
|
+
capability?: "importSite";
|
|
62
76
|
}
|
|
63
77
|
|
|
64
78
|
/**
|
|
@@ -67,7 +81,27 @@ export interface AiToolInfo {
|
|
|
67
81
|
* list cannot drift (a test asserts the names match the registered tools).
|
|
68
82
|
*/
|
|
69
83
|
export const AI_TOOL_TIERS: AiToolInfo[] = [
|
|
84
|
+
// Always — a question is not gated on what happens to be open
|
|
85
|
+
{
|
|
86
|
+
name: "ask_user",
|
|
87
|
+
tier: "always",
|
|
88
|
+
blurb:
|
|
89
|
+
"ask_user(question, options?, context?) — pause and put a question to the user; the turn " +
|
|
90
|
+
"waits for their reply and resumes with it. Only for a judgement that is genuinely theirs.",
|
|
91
|
+
},
|
|
70
92
|
// Bootstrap (no project open)
|
|
93
|
+
{
|
|
94
|
+
name: "import_site",
|
|
95
|
+
tier: "no-project",
|
|
96
|
+
capability: "importSite",
|
|
97
|
+
blurb:
|
|
98
|
+
"import_site(url, directory?, depth?, maxPages?, aiComponents?, maxBreakpoints?, " +
|
|
99
|
+
"breakpointWidths?, breakpointRounding?) — clone a live website into a new Jx project and " +
|
|
100
|
+
"open it: pages, styles, assets, a shared layout and recurring components. The project opens " +
|
|
101
|
+
"as soon as the destination exists, so the user watches it fill up while the crawl runs. " +
|
|
102
|
+
"When the user came through the New Project Import form the destination and options are " +
|
|
103
|
+
"already settled — pass the url alone.",
|
|
104
|
+
},
|
|
71
105
|
{
|
|
72
106
|
name: "create_project",
|
|
73
107
|
tier: "no-project",
|
|
@@ -174,6 +208,8 @@ export const AI_TOOL_TIERS: AiToolInfo[] = [
|
|
|
174
208
|
export interface AiToolState {
|
|
175
209
|
hasProject: boolean;
|
|
176
210
|
hasDocument: boolean;
|
|
211
|
+
/** Whether the platform implements `importSite`. Cloud does not. */
|
|
212
|
+
canImport?: boolean;
|
|
177
213
|
/**
|
|
178
214
|
* The open document is an element tree the canvas is editing — `editor.kind === "canvas"`.
|
|
179
215
|
*
|
|
@@ -189,6 +225,9 @@ export interface AiToolState {
|
|
|
189
225
|
* tools need an active document (even in single-file mode without a project).
|
|
190
226
|
*/
|
|
191
227
|
export function tierActive(tier: AiToolTier, state: AiToolState): boolean {
|
|
228
|
+
if (tier === "always") {
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
192
231
|
if (tier === "no-project") {
|
|
193
232
|
return !state.hasProject;
|
|
194
233
|
}
|
|
@@ -201,12 +240,56 @@ export function tierActive(tier: AiToolTier, state: AiToolState): boolean {
|
|
|
201
240
|
return state.hasDocument;
|
|
202
241
|
}
|
|
203
242
|
|
|
243
|
+
/**
|
|
244
|
+
* Whether a tool is active: its tier, AND any capability it declares.
|
|
245
|
+
*
|
|
246
|
+
* The one predicate the prompt's tool list and the gating registry both run, which is the whole
|
|
247
|
+
* point of {@link AI_TOOL_TIERS} — a tool the model is told about and then refused is a surprise to
|
|
248
|
+
* it, and one it is refused without being told is invisible.
|
|
249
|
+
*
|
|
250
|
+
* @param {AiToolInfo} tool
|
|
251
|
+
* @param {AiToolState} state
|
|
252
|
+
* @returns {boolean}
|
|
253
|
+
*/
|
|
254
|
+
export function toolActive(tool: AiToolInfo, state: AiToolState): boolean {
|
|
255
|
+
if (tool.capability === "importSite" && state.canImport === false) {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
return tierActive(tool.tier, state);
|
|
259
|
+
}
|
|
260
|
+
|
|
204
261
|
// ─── Jx Schema Reference (condensed) ────────────────────────────────────────
|
|
205
262
|
|
|
206
263
|
/**
|
|
207
264
|
* Condensed reference of Jx document structure rules. Included inline in the system prompt so the
|
|
208
265
|
* LLM understands the schema without consuming excessive tokens.
|
|
209
266
|
*/
|
|
267
|
+
/**
|
|
268
|
+
* When to stop and ask, and — more importantly — when not to.
|
|
269
|
+
*
|
|
270
|
+
* Rendered into the closing section of every prompt, because `ask_user` is an `always` tier tool.
|
|
271
|
+
* The prohibitions carry most of the weight: a tool that suspends a turn on a human is only worth
|
|
272
|
+
* having if the questions are ones a human actually wants. A model that asks what it could have
|
|
273
|
+
* read, or asks about a knob its answer cannot reach, trains the author to stop reading the
|
|
274
|
+
* questions.
|
|
275
|
+
*/
|
|
276
|
+
const ASKING_THE_USER = `## Asking the user
|
|
277
|
+
|
|
278
|
+
You can stop and ask with ask_user; the turn waits and resumes with the reply. Waiting on a person
|
|
279
|
+
is not free, so ask only when all three hold:
|
|
280
|
+
|
|
281
|
+
1. It is genuinely THEIR judgement — taste, priority, or intent. "Which of these pages matter?",
|
|
282
|
+
"keep their brand colours or restyle?", "is this page close enough?"
|
|
283
|
+
2. You could not answer it yourself with another tool. Read the file, list the directory, look at
|
|
284
|
+
the document. Asking what you could have discovered is the commonest way to waste the question.
|
|
285
|
+
3. You can ACT on any answer they give. Never ask about an option you have no way to change.
|
|
286
|
+
|
|
287
|
+
Ask one question at a time. Offer options when the sensible answers are a short list; the user can
|
|
288
|
+
always reply in their own words instead. If they decline, decide yourself and say what you chose.
|
|
289
|
+
|
|
290
|
+
Prefer doing to asking. On a vague prompt, make a defensible choice, do the work, and say what you
|
|
291
|
+
assumed — one good question beats five, and beats none only when it changes what you build.`;
|
|
292
|
+
|
|
210
293
|
const JX_SCHEMA_REFERENCE = `## Jx Document Format
|
|
211
294
|
|
|
212
295
|
A Jx document is a JSON object. Key top-level fields:
|
|
@@ -560,6 +643,7 @@ export function buildSystemPrompt({
|
|
|
560
643
|
projectRoot,
|
|
561
644
|
hasProject = Boolean(projectRoot),
|
|
562
645
|
treeEditable = true,
|
|
646
|
+
canImport = true,
|
|
563
647
|
fileInventory,
|
|
564
648
|
}: BuildSystemPromptOptions = {}) {
|
|
565
649
|
const hasDocument = Boolean(document);
|
|
@@ -568,7 +652,7 @@ export function buildSystemPrompt({
|
|
|
568
652
|
// The list the model is TOLD about and the list the gate will honour are the same filter, so a
|
|
569
653
|
// Refusal is never a surprise to it.
|
|
570
654
|
const toolList = AI_TOOL_TIERS.filter((t) =>
|
|
571
|
-
|
|
655
|
+
toolActive(t, { canImport, hasDocument, hasProject, treeEditable }),
|
|
572
656
|
)
|
|
573
657
|
.map((t) => `- ${t.blurb}`)
|
|
574
658
|
.join("\n");
|
|
@@ -608,6 +692,8 @@ Your document edits apply to the live canvas immediately and are individually un
|
|
|
608
692
|
|
|
609
693
|
const closing = `You have a limited number of tool-call rounds per message. On vague or open-ended prompts ("make it look better", "improve this"), prefer a small number of targeted, high-impact changes over attempting to rebuild the entire page. Explain what you changed and offer to do more.
|
|
610
694
|
|
|
695
|
+
${ASKING_THE_USER}
|
|
696
|
+
|
|
611
697
|
Be concise. Don't explain what Jx is unless asked. Just build.`;
|
|
612
698
|
|
|
613
699
|
const sections = [`${role}\n\n${workflow}\n\n${closing}`];
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ai-turn-signal.ts — what the agent loop knows that a tool needs and cannot be passed.
|
|
3
|
+
*
|
|
4
|
+
* `ToolRegistry.execute(name, args)` takes no signal, and its signature is fixed in the published
|
|
5
|
+
* `@jxsuite/ai`. So `runAgentLoop` could hand its signal to `streamChat` and to nothing else: a tool
|
|
6
|
+
* that ran for minutes (an import) could not be cancelled, and a tool that waits for a human
|
|
7
|
+
* (`ask_user`) would have hung on Stop forever, because the promise the loop is awaiting has no
|
|
8
|
+
* other way to learn the turn is over.
|
|
9
|
+
*
|
|
10
|
+
* An ambient slot rather than a parameter, for the same reason `services/ai-writes.ts` is one: the
|
|
11
|
+
* loop already brackets each turn with `beginTurn`/`endTurn` in a try/finally, and this rides that
|
|
12
|
+
* bracket. Only one turn runs at a time — `sendMessage` and `beginAssistantTurn` are both no-ops
|
|
13
|
+
* while streaming — so a module-level slot is the whole of the state.
|
|
14
|
+
*
|
|
15
|
+
* @license MIT
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
let _signal: AbortSignal | undefined;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Publish the turn's signal for the tools it will run. Called by `runAgentLoop`.
|
|
22
|
+
*
|
|
23
|
+
* @param {AbortSignal} [signal] - The turn's signal; `undefined` leaves tools uncancellable, which
|
|
24
|
+
* is what a caller that passed no signal asked for.
|
|
25
|
+
*/
|
|
26
|
+
export function beginTurnSignal(signal?: AbortSignal): void {
|
|
27
|
+
_signal = signal;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The running turn's signal, or `undefined` outside a turn.
|
|
32
|
+
*
|
|
33
|
+
* A tool must treat `undefined` as "not cancellable" rather than as an error: the evals runner and
|
|
34
|
+
* several tests drive the loop with no signal at all.
|
|
35
|
+
*
|
|
36
|
+
* @returns {AbortSignal | undefined}
|
|
37
|
+
*/
|
|
38
|
+
export function turnSignal(): AbortSignal | undefined {
|
|
39
|
+
return _signal;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Clear the slots. Called from `runAgentLoop`'s `finally`, so it runs on every exit path. */
|
|
43
|
+
export function endTurnSignal(): void {
|
|
44
|
+
_signal = undefined;
|
|
45
|
+
_toolCallId = "";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let _toolCallId = "";
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Publish the id of the call about to execute. Called by `runAgentLoop` per tool call.
|
|
52
|
+
*
|
|
53
|
+
* A tool that outlives its own execution — `ask_user`, whose question stays on screen until a human
|
|
54
|
+
* answers — needs the id to join what it registered to the chip the transcript is drawing. Without
|
|
55
|
+
* it a restored, permanently unanswered question is indistinguishable from the live one.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} id
|
|
58
|
+
*/
|
|
59
|
+
export function beginToolCall(id: string): void {
|
|
60
|
+
_toolCallId = id;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The id of the tool call currently executing, or `""` outside one.
|
|
65
|
+
*
|
|
66
|
+
* @returns {string}
|
|
67
|
+
*/
|
|
68
|
+
export function currentToolCallId(): string {
|
|
69
|
+
return _toolCallId;
|
|
70
|
+
}
|
|
@@ -1,58 +1,40 @@
|
|
|
1
1
|
/// <reference lib="dom" />
|
|
2
2
|
/**
|
|
3
|
-
* Cf-settings —
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* platform settings backend where one exists. It only ever leaves the machine
|
|
7
|
-
* to the same-origin `/__studio/cf/proxy`, which forwards it as a bearer to
|
|
3
|
+
* Cf-settings — the Cloudflare publish connection on platforms without a hosted OAuth broker (dev
|
|
4
|
+
* server, desktop). The API token is stored like the AI key, over `settings/kernel.ts`. It only ever
|
|
5
|
+
* leaves the machine to the same-origin `/__studio/cf/proxy`, which forwards it as a bearer to
|
|
8
6
|
* api.cloudflare.com (not CORS-enabled, hence the proxy).
|
|
9
7
|
*
|
|
8
|
+
* As with the AI provider, a blank value is a VALUE: {@link clearCfConnection} is the deletion path,
|
|
9
|
+
* so nothing can revoke a connection by rendering an empty field into a setter.
|
|
10
|
+
*
|
|
10
11
|
* @license MIT
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
const TOKEN_STORAGE = "jx.cf.token";
|
|
16
|
-
const ACCOUNT_STORAGE = "jx.cf.accountId";
|
|
17
|
-
|
|
18
|
-
function read(key: string): string {
|
|
19
|
-
try {
|
|
20
|
-
return globalThis.localStorage?.getItem(key) ?? "";
|
|
21
|
-
} catch {
|
|
22
|
-
return "";
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function write(key: string, value: string): void {
|
|
27
|
-
try {
|
|
28
|
-
const trimmed = (value || "").trim();
|
|
29
|
-
if (trimmed) {
|
|
30
|
-
globalThis.localStorage?.setItem(key, trimmed);
|
|
31
|
-
} else {
|
|
32
|
-
globalThis.localStorage?.removeItem(key);
|
|
33
|
-
}
|
|
34
|
-
} catch {
|
|
35
|
-
// Storage unavailable (private mode) — the connection just won't persist.
|
|
36
|
-
}
|
|
37
|
-
persistSettings();
|
|
38
|
-
}
|
|
14
|
+
import { SETTINGS } from "./settings/definitions";
|
|
15
|
+
import { clearSettings, readStoredSetting, setSetting } from "./settings/kernel";
|
|
39
16
|
|
|
40
17
|
/** The stored Cloudflare API token, or "" when not connected. */
|
|
41
18
|
export function getCfToken(): string {
|
|
42
|
-
return
|
|
19
|
+
return readStoredSetting(SETTINGS.cfToken);
|
|
43
20
|
}
|
|
44
21
|
|
|
45
|
-
/** Persist
|
|
22
|
+
/** Persist the Cloudflare API token. Blank stores blank — see {@link clearCfConnection}. */
|
|
46
23
|
export function setCfToken(token: string): void {
|
|
47
|
-
|
|
24
|
+
setSetting(SETTINGS.cfToken, token || "");
|
|
48
25
|
}
|
|
49
26
|
|
|
50
27
|
/** The selected Cloudflare account id, or "" when none chosen yet. */
|
|
51
28
|
export function getCfAccountId(): string {
|
|
52
|
-
return
|
|
29
|
+
return readStoredSetting(SETTINGS.cfAccountId);
|
|
53
30
|
}
|
|
54
31
|
|
|
55
|
-
/** Persist
|
|
32
|
+
/** Persist the selected Cloudflare account id. Blank stores blank. */
|
|
56
33
|
export function setCfAccountId(accountId: string): void {
|
|
57
|
-
|
|
34
|
+
setSetting(SETTINGS.cfAccountId, accountId || "");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Forget the connection — token and account together. What Disconnect calls. */
|
|
38
|
+
export function clearCfConnection(): void {
|
|
39
|
+
clearSettings([SETTINGS.cfToken, SETTINGS.cfAccountId]);
|
|
58
40
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The backend connection, said out loud.
|
|
3
|
+
*
|
|
4
|
+
* Only one launcher can lose a backend mid-session: the chromium desktop shell holds a single
|
|
5
|
+
* long-lived WebSocket to its project server. When that socket died, every later call returned a
|
|
6
|
+
* promise that NEVER SETTLED — `WebSocket.send()` on a closed socket discards the frame instead of
|
|
7
|
+
* throwing, so nothing rejected, nothing resolved, and the visible symptom was **Open Project**
|
|
8
|
+
* doing nothing at all. No toast, no log, no failed state. Restarting Studio was the only
|
|
9
|
+
* recovery.
|
|
10
|
+
*
|
|
11
|
+
* The transport now rejects those calls, and this is the other half: a person needs to be told
|
|
12
|
+
* which of "it is broken" and "it is coming back" they are in. One keyed problem so a flapping
|
|
13
|
+
* connection cannot stack up a column of identical rows, dismissed on recovery so a fixed thing
|
|
14
|
+
* does not stay on the Problems list (§16.1's third tier, and §16.6's rule that a record is retired
|
|
15
|
+
* by whoever owns its life).
|
|
16
|
+
*
|
|
17
|
+
* A platform without `subscribeConnection` — the dev server, electrobun — installs nothing. There
|
|
18
|
+
* is no connection to lose there, and a notice about one would be a lie.
|
|
19
|
+
*
|
|
20
|
+
* @docs studio/interface/problems-and-progress
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { getPlatform } from "../platform";
|
|
24
|
+
import { dismiss, notify } from "./notify";
|
|
25
|
+
import type { Notification } from "./notify";
|
|
26
|
+
|
|
27
|
+
/** One key for every connection notice, so a flapping socket replaces rather than accumulates. */
|
|
28
|
+
const CONNECTION_KEY = "platform:connection";
|
|
29
|
+
|
|
30
|
+
/** The outstanding "we are disconnected" record, so recovery can retire it. */
|
|
31
|
+
let outstanding: Notification | null = null;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Start reporting the backend connection. Returns an unsubscribe, or null on a platform that has no
|
|
35
|
+
* connection to report.
|
|
36
|
+
*
|
|
37
|
+
* @returns {(() => void) | null}
|
|
38
|
+
*/
|
|
39
|
+
export function watchConnection(): (() => void) | null {
|
|
40
|
+
const subscribe = getPlatform().subscribeConnection;
|
|
41
|
+
if (!subscribe) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const unsubscribe = subscribe(({ online, reason }) => {
|
|
45
|
+
if (online) {
|
|
46
|
+
if (outstanding) {
|
|
47
|
+
dismiss(outstanding.id);
|
|
48
|
+
outstanding = null;
|
|
49
|
+
notify.success("Reconnected to the Jx backend.", { source: "Backend" });
|
|
50
|
+
}
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
/* Already reported. A reconnect attempt failing is the same state, not a new one. */
|
|
54
|
+
if (outstanding) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
outstanding = notify.error(reason ?? "Lost connection to the Jx backend.", {
|
|
58
|
+
detail:
|
|
59
|
+
"Editing continues, but anything that reads or writes files will fail until it comes " +
|
|
60
|
+
"back. Studio is retrying.",
|
|
61
|
+
key: CONNECTION_KEY,
|
|
62
|
+
source: "Backend",
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
/* Unsubscribing retires the notice with it: the watcher is the only thing that would ever have
|
|
66
|
+
dismissed it, so leaving it on the Problems list would leave a report nobody can clear. */
|
|
67
|
+
return () => {
|
|
68
|
+
unsubscribe();
|
|
69
|
+
if (outstanding) {
|
|
70
|
+
dismiss(outstanding.id);
|
|
71
|
+
outstanding = null;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|