@jxsuite/studio 3.0.0 → 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-zj2cj1ar.js → cf-settings-45fgyx8y.js} +6 -5
- 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-6vvx96n2.js → convert-to-repeater-dhw1nrz9.js} +27 -19
- package/dist/chunks/entry-commands-6z6bhxg0.js +46 -0
- package/dist/chunks/{extension-sections-kwzvpemr.js → extension-sections-3ydgjbdy.js} +32 -24
- package/dist/chunks/file-ops-rskmhxyq.js +48 -0
- package/dist/chunks/files-0cratsr0.js +92 -0
- package/dist/chunks/{format-host-8fr3545e.js → format-host-tgcmfs4p.js} +3 -3
- package/dist/chunks/fs-events-hrrg328g.js +21 -0
- package/dist/chunks/{git-panel-g01d93vc.js → git-panel-0yywyk76.js} +21 -16
- 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-8ezdegcx.js → studio-1hbqnt19.js} +36 -32
- package/dist/chunks/{studio-r5xbtctf.js → studio-2zkz8evn.js} +446 -2451
- package/dist/chunks/{studio-pkp61mh8.js → studio-4pqqxbrt.js} +23 -15
- package/dist/chunks/{studio-kvfx9z2c.js → studio-4rzyrtjz.js} +70 -31
- package/dist/chunks/{studio-3dkcd4c5.js → studio-5q69gfag.js} +60 -56
- package/dist/chunks/{studio-rh1cnamv.js → studio-5rywrkxz.js} +563 -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-xntfdy1x.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-ry1vz5sv.js → studio-9c4k4vhv.js} +15 -13
- package/dist/chunks/studio-9n4mrmnb.js +184 -0
- package/dist/chunks/{studio-tr20m8g8.js → studio-9r6j5ev1.js} +37 -2
- package/dist/chunks/{studio-2x9zbxmx.js → studio-ahjfdy6e.js} +9 -7
- package/dist/chunks/{studio-s519zc99.js → studio-bgxt93ga.js} +3 -3
- package/dist/chunks/{studio-dc8s8heb.js → studio-cb12yz9w.js} +8528 -8780
- package/dist/chunks/studio-cft9r4r7.js +119 -0
- package/dist/chunks/{studio-hcxzyk3x.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-7p35254k.js → studio-ex8zyp98.js} +30 -26
- package/dist/chunks/studio-gv8vvshc.js +118 -0
- package/dist/chunks/studio-hbtp0eh1.js +1315 -0
- package/dist/chunks/{studio-a0yp7syn.js → studio-htsf0w6m.js} +29 -114
- package/dist/chunks/{studio-j595e748.js → studio-kjjsjak4.js} +139 -63
- 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-g5gdqnyd.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-p5dja96s.js → studio-p8j86wag.js} +2 -2
- package/dist/chunks/{studio-6z5eyjwn.js → studio-pdfjntbk.js} +66 -129
- 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-k4jvjcfx.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-5efrtn1m.js → studio-wx5609xd.js} +21 -17
- package/dist/chunks/{studio-qpfq7xhm.js → studio-xrapxwwc.js} +22 -285
- package/dist/chunks/{studio-hj2se583.js → studio-yxrkyv8a.js} +41 -31
- package/dist/chunks/{studio-xm3dbxvn.js → studio-zczps70q.js} +2 -2
- package/dist/chunks/workspace-xpres40v.js +89 -0
- package/dist/iframe-entry.js +108 -24
- package/dist/studio.js +2362 -1695
- package/package.json +16 -15
- 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 +485 -59
- package/src/files/fs-events.ts +9 -0
- package/src/files/gitignore.ts +432 -0
- 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/media/media-open.ts +58 -0
- package/src/media/media-pane.ts +310 -0
- package/src/new-project/import-tab.ts +332 -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 -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 +20 -0
- package/src/platforms/devserver.ts +67 -33
- 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-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 +89 -0
- 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 +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-dialog.ts +18 -41
- 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 +103 -5
- 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/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 +111 -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-c31yb4t6.js +0 -55
- package/src/canvas/site-style-css.ts +0 -117
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Import-run.ts — the live state of a site import, for the transcript to draw.
|
|
3
|
+
*
|
|
4
|
+
* An import takes minutes and reports a line at a time. Those lines used to render into the New
|
|
5
|
+
* Project modal and die with it: a successful import destroyed its own account of what it had done
|
|
6
|
+
* at the moment it handed off. Now the run belongs to a tool call, so the record is keyed by that
|
|
7
|
+
* call's id and the chip and its progress are one thing rather than two.
|
|
8
|
+
*
|
|
9
|
+
* Reactive, so `panels/ai-panel.ts`'s watcher repaints on it through the same rAF coalescer that
|
|
10
|
+
* carries streaming tokens — a crawl emitting a line per page cannot outrun the frame rate.
|
|
11
|
+
*
|
|
12
|
+
* @license MIT
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { reactive } from "../reactivity";
|
|
16
|
+
|
|
17
|
+
import type { ImportProgressEvent } from "../types";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Lines kept per run.
|
|
21
|
+
*
|
|
22
|
+
* A wide crawl emits one per page plus one per asset batch, and every line is also held in the
|
|
23
|
+
* session's localStorage budget once the tool result lands — so there is a ceiling, and it is here.
|
|
24
|
+
* It was 200 back when the view showed the last SIX of them and the rest existed only to be
|
|
25
|
+
* summarised; the transcript now renders the whole log in a scroller the reader owns, so the number
|
|
26
|
+
* that matters is how far back they can scroll.
|
|
27
|
+
*/
|
|
28
|
+
export const MAX_LOG_LINES = 500;
|
|
29
|
+
|
|
30
|
+
export interface ImportRunRecord {
|
|
31
|
+
/** The tool-call id — the join key with the chip the transcript draws. */
|
|
32
|
+
id: string;
|
|
33
|
+
url: string;
|
|
34
|
+
directory: string;
|
|
35
|
+
phase: string;
|
|
36
|
+
message: string;
|
|
37
|
+
current: number | null;
|
|
38
|
+
total: number | null;
|
|
39
|
+
log: ImportProgressEvent[];
|
|
40
|
+
/** Lines the pipeline marked with `⚠`, kept apart so the summary can name them. */
|
|
41
|
+
warnings: string[];
|
|
42
|
+
status: "running" | "done" | "failed" | "stopped";
|
|
43
|
+
error: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* One run at a time, because one project is created at a time and `import_site` is `no-project`
|
|
48
|
+
* tiered. Keyed rather than bare so a finished run still renders under its own chip after the next
|
|
49
|
+
* one starts, which is what makes the transcript readable after two attempts.
|
|
50
|
+
*/
|
|
51
|
+
const runs = reactive<{ records: ImportRunRecord[] }>({ records: [] });
|
|
52
|
+
|
|
53
|
+
/** Runs kept before the oldest is dropped. Two attempts and a retry is the realistic ceiling. */
|
|
54
|
+
const MAX_RUNS = 5;
|
|
55
|
+
|
|
56
|
+
/** Aborts the in-flight run. Owned here because `ToolRegistry.execute` cannot be handed a signal. */
|
|
57
|
+
let controller: AbortController | null = null;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Open a run record and return the signal its request should carry.
|
|
61
|
+
*
|
|
62
|
+
* @param {string} id - The tool-call id.
|
|
63
|
+
* @param {{ url: string; directory: string }} init
|
|
64
|
+
* @returns {AbortSignal}
|
|
65
|
+
*/
|
|
66
|
+
export function beginImportRun(id: string, init: { url: string; directory: string }): AbortSignal {
|
|
67
|
+
controller?.abort();
|
|
68
|
+
controller = new AbortController();
|
|
69
|
+
runs.records.push({
|
|
70
|
+
current: null,
|
|
71
|
+
directory: init.directory,
|
|
72
|
+
error: "",
|
|
73
|
+
id,
|
|
74
|
+
log: [],
|
|
75
|
+
message: "Starting…",
|
|
76
|
+
phase: "start",
|
|
77
|
+
status: "running",
|
|
78
|
+
total: null,
|
|
79
|
+
url: init.url,
|
|
80
|
+
warnings: [],
|
|
81
|
+
});
|
|
82
|
+
if (runs.records.length > MAX_RUNS) {
|
|
83
|
+
runs.records.shift();
|
|
84
|
+
}
|
|
85
|
+
return controller.signal;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Record one progress line against a run.
|
|
90
|
+
*
|
|
91
|
+
* @param {string} id
|
|
92
|
+
* @param {ImportProgressEvent} evt
|
|
93
|
+
*/
|
|
94
|
+
export function recordImportProgress(id: string, evt: ImportProgressEvent): void {
|
|
95
|
+
const record = runs.records.find((r) => r.id === id);
|
|
96
|
+
if (!record) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
record.phase = evt.phase;
|
|
100
|
+
record.message = evt.message;
|
|
101
|
+
record.current = evt.current ?? null;
|
|
102
|
+
record.total = evt.total ?? null;
|
|
103
|
+
record.log.push(evt);
|
|
104
|
+
if (record.log.length > MAX_LOG_LINES) {
|
|
105
|
+
record.log.shift();
|
|
106
|
+
}
|
|
107
|
+
/* The pipeline reports a soft failure by prefixing its progress line, and `ImportSiteResult`'s
|
|
108
|
+
own `warnings` array does not cross the HTTP boundary — so this is the only place they can be
|
|
109
|
+
recovered from, and the summary needs them to be honest about what was skipped. */
|
|
110
|
+
if (evt.message.startsWith("⚠")) {
|
|
111
|
+
record.warnings.push(evt.message.replace(/^⚠\s*/, ""));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Close a run.
|
|
117
|
+
*
|
|
118
|
+
* @param {string} id
|
|
119
|
+
* @param {{ status: Exclude<ImportRunRecord["status"], "running">; error?: string }} outcome
|
|
120
|
+
*/
|
|
121
|
+
export function finishImportRun(
|
|
122
|
+
id: string,
|
|
123
|
+
outcome: { status: Exclude<ImportRunRecord["status"], "running">; error?: string },
|
|
124
|
+
): void {
|
|
125
|
+
const record = runs.records.find((r) => r.id === id);
|
|
126
|
+
if (record) {
|
|
127
|
+
record.status = outcome.status;
|
|
128
|
+
record.error = outcome.error ?? "";
|
|
129
|
+
}
|
|
130
|
+
controller = null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The record for one tool call, or null. Reactive — read it inside an `effect()` to repaint on it.
|
|
135
|
+
*
|
|
136
|
+
* @param {string} id
|
|
137
|
+
* @returns {ImportRunRecord | null}
|
|
138
|
+
*/
|
|
139
|
+
export function importRun(id: string): ImportRunRecord | null {
|
|
140
|
+
return runs.records.find((r) => r.id === id) ?? null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** The run still going, or null. What the panel's watcher tracks. */
|
|
144
|
+
export function activeImportRun(): ImportRunRecord | null {
|
|
145
|
+
return runs.records.find((r) => r.status === "running") ?? null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Abort the in-flight run. Called from `assistant.stop`, which cannot reach the tool directly. */
|
|
149
|
+
export function abortImportRun(): void {
|
|
150
|
+
controller?.abort();
|
|
151
|
+
controller = null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Drop every record. For New Chat and for tests. */
|
|
155
|
+
export function resetImportRuns(): void {
|
|
156
|
+
abortImportRun();
|
|
157
|
+
runs.records.length = 0;
|
|
158
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Import-seed.ts — the New Project Import form's hand-off to the assistant.
|
|
3
|
+
*
|
|
4
|
+
* The wizard collects a URL, crawl options, a model and a brief, then closes; the assistant takes
|
|
5
|
+
* the turn and runs the import as a tool call. This holds what the form gathered for the one turn
|
|
6
|
+
* that consumes it.
|
|
7
|
+
*
|
|
8
|
+
* **Module state, and deliberately NOT a copy of `agent-seed.ts`.** That one uses localStorage keyed
|
|
9
|
+
* by project root because the project it seeds opens in a NEW WINDOW, and the key is the
|
|
10
|
+
* cross-window channel. Neither half of that applies here: the project does not exist yet at
|
|
11
|
+
* hand-off time, so there is no root to key on, and the assistant taking the turn is in this window
|
|
12
|
+
* — `import_site` is `no-project` tiered precisely because it runs before there is a project. A
|
|
13
|
+
* localStorage clone would be a channel with nothing on the other end and a key that cannot be
|
|
14
|
+
* computed.
|
|
15
|
+
*
|
|
16
|
+
* @license MIT
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { ImportBreakpointPolicy } from "../types";
|
|
20
|
+
|
|
21
|
+
/** What the wizard gathered, as the tool needs it. */
|
|
22
|
+
export interface ImportBrief {
|
|
23
|
+
/** Absolute http(s) URL of the site to clone. */
|
|
24
|
+
url: string;
|
|
25
|
+
/** Human project name (the wizard prefills it from the hostname). */
|
|
26
|
+
name: string;
|
|
27
|
+
/** Absolute destination directory, already resolved from Location + folder name. */
|
|
28
|
+
directory: string;
|
|
29
|
+
depth: number;
|
|
30
|
+
maxPages: number;
|
|
31
|
+
/**
|
|
32
|
+
* How many of the site's declared breakpoints the project keeps.
|
|
33
|
+
*
|
|
34
|
+
* A real site declares as many as it has accumulated frameworks — nine is ordinary — and each one
|
|
35
|
+
* is a canvas size in Studio and a column in every style editor. The wizard defaults this to
|
|
36
|
+
* three because that is the complaint it answers, not because three is a magic number.
|
|
37
|
+
*/
|
|
38
|
+
breakpoints: ImportBreakpointPolicy;
|
|
39
|
+
aiComponents: boolean;
|
|
40
|
+
/** Build and screenshot-diff the result. Off by default — it roughly doubles the run. */
|
|
41
|
+
verify: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* The average fidelity, 0..100, the clone has to reach for `verify` to report success.
|
|
44
|
+
*
|
|
45
|
+
* Only meaningful when `verify` is on. A finding rather than a failure here: the project is
|
|
46
|
+
* written and opened either way, and the assistant says the bar was missed.
|
|
47
|
+
*/
|
|
48
|
+
minFidelity: number;
|
|
49
|
+
/** Empty means "whatever the assistant would use" (`preferredModel()`). */
|
|
50
|
+
model: string;
|
|
51
|
+
/** What the user wants done with the site once it is cloned. May be empty. */
|
|
52
|
+
prompt: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let _brief: ImportBrief | null = null;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Store the brief the assistant's next turn will act on.
|
|
59
|
+
*
|
|
60
|
+
* @param {ImportBrief} brief
|
|
61
|
+
*/
|
|
62
|
+
export function setPendingImportBrief(brief: ImportBrief): void {
|
|
63
|
+
_brief = brief;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The brief without consuming it — what `import_site` reads to fill in a destination the model was
|
|
68
|
+
* never told and must not invent.
|
|
69
|
+
*
|
|
70
|
+
* @returns {ImportBrief | null}
|
|
71
|
+
*/
|
|
72
|
+
export function pendingImportBrief(): ImportBrief | null {
|
|
73
|
+
return _brief;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Forget the brief.
|
|
78
|
+
*
|
|
79
|
+
* There is deliberately no consume-on-read here, unlike `agent-seed.ts`. TWO readers need the same
|
|
80
|
+
* brief and they read at different moments: the panel, to compose the turn, and `import_site`, for
|
|
81
|
+
* the destination — which is minutes later, after the model has decided to call it. A read that
|
|
82
|
+
* cleared would leave the tool inventing a path the user never chose. The tool clears it once the
|
|
83
|
+
* import has actually run.
|
|
84
|
+
*/
|
|
85
|
+
export function clearPendingImportBrief(): void {
|
|
86
|
+
_brief = null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ─── The hand-off itself ────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The assistant's side of the hand-off, registered at boot.
|
|
93
|
+
*
|
|
94
|
+
* A late-bound slot for the same reason `services/project-adoption.ts` is one: the New Project
|
|
95
|
+
* wizard must not import `panels/ai-panel.ts`. That import is not just a layering preference — the
|
|
96
|
+
* panel constructs a `DocumentAssistant` singleton at module load, so reaching for it from
|
|
97
|
+
* `new-project/` drags a live assistant into every surface that opens the wizard.
|
|
98
|
+
*/
|
|
99
|
+
let _handoff: ((brief: ImportBrief) => Promise<void>) | null = null;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Register the assistant's hand-off (studio.ts boot).
|
|
103
|
+
*
|
|
104
|
+
* @param {(brief: ImportBrief) => Promise<void>} fn
|
|
105
|
+
*/
|
|
106
|
+
export function setImportHandoff(fn: (brief: ImportBrief) => Promise<void>): void {
|
|
107
|
+
_handoff = fn;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Start the import as an assistant turn.
|
|
112
|
+
*
|
|
113
|
+
* A no-op with nothing registered — a reduced host (a test fixture, an embedder without the
|
|
114
|
+
* assistant) would otherwise crash on a button the wizard should simply not have offered.
|
|
115
|
+
*
|
|
116
|
+
* @param {ImportBrief} brief
|
|
117
|
+
*/
|
|
118
|
+
export async function runImportHandoff(brief: ImportBrief): Promise<void> {
|
|
119
|
+
await _handoff?.(brief);
|
|
120
|
+
}
|
|
@@ -21,7 +21,7 @@ import coreDocumentSchema from "@jxsuite/schema/schema.json";
|
|
|
21
21
|
import coreProjectSchema from "@jxsuite/schema/project-schema.json";
|
|
22
22
|
|
|
23
23
|
type ValidateFn = ((doc: unknown) => boolean) & {
|
|
24
|
-
errors?: { instancePath?: string; message?: string }[] | null;
|
|
24
|
+
errors?: { instancePath?: string; message?: string; params?: Record<string, unknown> }[] | null;
|
|
25
25
|
};
|
|
26
26
|
|
|
27
27
|
/** Which of the two entry documents a validator covers. */
|
|
@@ -78,6 +78,16 @@ function getValidator(kind: SchemaKind): Promise<ValidateFn | null> {
|
|
|
78
78
|
return compiling[kind];
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
/** A payload half that is usable as a JSON Schema — a non-empty, non-array object. */
|
|
82
|
+
function asSchema(value: unknown): object | undefined {
|
|
83
|
+
return typeof value === "object" &&
|
|
84
|
+
value !== null &&
|
|
85
|
+
!Array.isArray(value) &&
|
|
86
|
+
Object.keys(value).length > 0
|
|
87
|
+
? (value as object)
|
|
88
|
+
: undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
81
91
|
/**
|
|
82
92
|
* Swap in the active project's pre-bundled entry documents (the payload behind the platform's
|
|
83
93
|
* `fetchProjectSchemas` member). Each half falls back to its core schema when absent, so a partial
|
|
@@ -90,16 +100,24 @@ function getValidator(kind: SchemaKind): Promise<ValidateFn | null> {
|
|
|
90
100
|
export function applyProjectSchemas(
|
|
91
101
|
schemas: { project?: unknown; document?: unknown } | null,
|
|
92
102
|
): boolean {
|
|
93
|
-
|
|
94
|
-
|
|
103
|
+
/*
|
|
104
|
+
* A payload half that is not a schema OBJECT falls back to core.
|
|
105
|
+
*
|
|
106
|
+
* `ajv.compile({})` succeeds and accepts everything, so a backend answering `null`, a string, or
|
|
107
|
+
* an error envelope under `document` would not fail loudly — it would install a validator that
|
|
108
|
+
* calls every document valid, and every consumer would go on trusting it.
|
|
109
|
+
*/
|
|
110
|
+
const document = asSchema(schemas?.document) ?? CORE.document;
|
|
111
|
+
const project = asSchema(schemas?.project) ?? CORE.project;
|
|
112
|
+
const applied = document !== CORE.document || project !== CORE.project;
|
|
95
113
|
if (document === active.document && project === active.project) {
|
|
96
|
-
return
|
|
114
|
+
return applied;
|
|
97
115
|
}
|
|
98
116
|
active.document = document;
|
|
99
117
|
active.project = project;
|
|
100
118
|
compiling.document = null;
|
|
101
119
|
compiling.project = null;
|
|
102
|
-
return
|
|
120
|
+
return applied;
|
|
103
121
|
}
|
|
104
122
|
|
|
105
123
|
/** Restore the core schemas (project closed / tests). */
|
|
@@ -107,6 +125,17 @@ export function resetProjectSchemas(): void {
|
|
|
107
125
|
applyProjectSchemas(null);
|
|
108
126
|
}
|
|
109
127
|
|
|
128
|
+
/**
|
|
129
|
+
* The keys ajv uses to name the property an error is ABOUT, in the order they can appear.
|
|
130
|
+
*
|
|
131
|
+
* `message` alone is not a diagnosis for any of these three. "must NOT have unevaluated properties"
|
|
132
|
+
* says a key is unrecognised without saying which key — and at the root of a `project.json` that is
|
|
133
|
+
* the difference between a fixable problem and a mystery. An imported project once produced that
|
|
134
|
+
* line three times over, naming nothing, and the only way to learn it meant `title`, `description`
|
|
135
|
+
* and `$style` was to compile the schema by hand.
|
|
136
|
+
*/
|
|
137
|
+
const ERROR_PROPERTY_KEYS = ["unevaluatedProperty", "additionalProperty", "missingProperty"];
|
|
138
|
+
|
|
110
139
|
/**
|
|
111
140
|
* Format ajv errors the way the agent loop consumes them (see ai-tools.ts
|
|
112
141
|
* `translateValidationError`).
|
|
@@ -115,7 +144,11 @@ export function resetProjectSchemas(): void {
|
|
|
115
144
|
* @returns {string[]} One formatted string per error
|
|
116
145
|
*/
|
|
117
146
|
function formatErrors(validate: ValidateFn): string[] {
|
|
118
|
-
return (validate.errors || []).map((e) =>
|
|
147
|
+
return (validate.errors || []).map((e) => {
|
|
148
|
+
const key = ERROR_PROPERTY_KEYS.find((name) => typeof e.params?.[name] === "string");
|
|
149
|
+
const property = key ? ` (${String(e.params?.[key])})` : "";
|
|
150
|
+
return `${e.instancePath || "(root)"}: ${e.message}${property}`;
|
|
151
|
+
});
|
|
119
152
|
}
|
|
120
153
|
|
|
121
154
|
/**
|
|
@@ -132,6 +165,25 @@ export async function validateDoc(doc: unknown): Promise<string[]> {
|
|
|
132
165
|
return formatErrors(validate);
|
|
133
166
|
}
|
|
134
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Validate a Jx document, distinguishing "valid" from "could not be checked".
|
|
170
|
+
*
|
|
171
|
+
* {@link validateDoc} fails OPEN — an unavailable validator answers `[]`, which is the same answer
|
|
172
|
+
* as "no errors". That is right where validation decorates an editor and wrong where it GATES a
|
|
173
|
+
* destructive step: a conversion that says "this will be a valid JSON document" on the strength of
|
|
174
|
+
* a schema it never compiled is the "never render 0" defect §9.1.1 forbids, one layer down.
|
|
175
|
+
*
|
|
176
|
+
* @param doc - Parsed document
|
|
177
|
+
* @returns The errors, `[]` when valid, or `null` when no validator could be compiled
|
|
178
|
+
*/
|
|
179
|
+
export async function validateDocOrNull(doc: unknown): Promise<string[] | null> {
|
|
180
|
+
const validate = await getValidator("document");
|
|
181
|
+
if (!validate) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
return validate(doc) ? [] : formatErrors(validate);
|
|
185
|
+
}
|
|
186
|
+
|
|
135
187
|
/**
|
|
136
188
|
* Validate a `project.json` config against the active project schema. The per-project entry
|
|
137
189
|
* document is what closes the composition (`unevaluatedProperties: false` over core + every enabled
|
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
* @license MIT
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import { initProjectRepo } from "../files/files";
|
|
13
|
+
import { beginBatch, endBatch, isBatching } from "../tabs/transact";
|
|
14
|
+
import { workspace } from "../workspace/workspace";
|
|
15
|
+
import type { Tab } from "../tabs/tab";
|
|
16
|
+
|
|
12
17
|
let _adopter: ((root: string) => Promise<void>) | null = null;
|
|
13
18
|
|
|
14
19
|
/** Register the project-open flow (studio.ts boot). */
|
|
@@ -29,3 +34,69 @@ export async function adoptProject(root: string): Promise<void> {
|
|
|
29
34
|
}
|
|
30
35
|
await _adopter(root);
|
|
31
36
|
}
|
|
37
|
+
|
|
38
|
+
/** What {@link adoptCreatedProject} observed, so a tool reports the outcome instead of the intent. */
|
|
39
|
+
export interface AdoptOutcome {
|
|
40
|
+
/** The workspace is now on this root. False means the project exists but this window is not on it. */
|
|
41
|
+
adopted: boolean;
|
|
42
|
+
/** Why adoption failed, when it threw. Null when it did not (including when it silently did not). */
|
|
43
|
+
error: string | null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The seams {@link adoptCreatedProject} runs through, so each caller keeps the one it already had. */
|
|
47
|
+
export interface AdoptCreatedOptions {
|
|
48
|
+
/** The active tab, read AFTER adoption to re-anchor the agent loop's undo batch. */
|
|
49
|
+
getTab: () => Tab | null;
|
|
50
|
+
/** The project-open flow. Defaults to this module's late-bound {@link adoptProject}. */
|
|
51
|
+
adopt?: (root: string) => Promise<void>;
|
|
52
|
+
/** Version-control init. Defaults to `files/files.ts`'s `initProjectRepo`. */
|
|
53
|
+
initRepo?: (root: string) => Promise<boolean>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Everything that must happen to a project the moment it exists: version control, then adoption.
|
|
58
|
+
*
|
|
59
|
+
* Extracted because `create_project` and `import_site` are the same act with different backends,
|
|
60
|
+
* and the wizard is a third caller of half of it. Two things it does are easy to leave out, and
|
|
61
|
+
* both were:
|
|
62
|
+
*
|
|
63
|
+
* - **Git init.** `specs/desktop.md` §4.5 is normative — _"On the create path, every source,
|
|
64
|
+
* including Import and Agent, Studio therefore binds the backend to the new root (`activate`),
|
|
65
|
+
* reads `gitStatus`, and runs `gitInit`."_ `initProjectRepo` had exactly two call sites, both in
|
|
66
|
+
* the New Project modal, so a project the ASSISTANT bootstrapped was not a repository and nothing
|
|
67
|
+
* said so. It runs first because it is what calls `platform.activate(root)`, which the open flow
|
|
68
|
+
* below needs.
|
|
69
|
+
* - **Verifying.** `openRecentProject` swallows its failures into a status message, so a resolved
|
|
70
|
+
* promise is not proof of adoption. The caller must be told which of the two happened.
|
|
71
|
+
*
|
|
72
|
+
* The batch dance is the third: the agent loop may hold an undo batch on the pre-adoption tab, and
|
|
73
|
+
* adoption replaces every tab in the workspace. Flush it, adopt, re-open one on whatever tab
|
|
74
|
+
* adoption left active.
|
|
75
|
+
*
|
|
76
|
+
* @param {string} root - Absolute root of the project that was just created.
|
|
77
|
+
* @param {AdoptCreatedOptions} opts
|
|
78
|
+
* @returns {Promise<AdoptOutcome>}
|
|
79
|
+
*/
|
|
80
|
+
export async function adoptCreatedProject(
|
|
81
|
+
root: string,
|
|
82
|
+
{ getTab, adopt = adoptProject, initRepo = initProjectRepo }: AdoptCreatedOptions,
|
|
83
|
+
): Promise<AdoptOutcome> {
|
|
84
|
+
// Never fails the create: a project that was written stays written (initProjectRepo notifies).
|
|
85
|
+
await initRepo(root);
|
|
86
|
+
|
|
87
|
+
const wasBatching = isBatching();
|
|
88
|
+
if (wasBatching) {
|
|
89
|
+
endBatch();
|
|
90
|
+
}
|
|
91
|
+
let adoptionError: string | null = null;
|
|
92
|
+
try {
|
|
93
|
+
await adopt(root);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
adoptionError = error instanceof Error ? error.message : String(error);
|
|
96
|
+
}
|
|
97
|
+
if (wasBatching) {
|
|
98
|
+
beginBatch(getTab());
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return { adopted: workspace.projectRoot === root, error: adoptionError };
|
|
102
|
+
}
|
|
Binary file
|
|
@@ -127,6 +127,23 @@ export const SETTINGS = {
|
|
|
127
127
|
normalize: trimmed,
|
|
128
128
|
scope: "user",
|
|
129
129
|
},
|
|
130
|
+
/**
|
|
131
|
+
* Whether the Files tree draws what `.gitignore` masks.
|
|
132
|
+
*
|
|
133
|
+
* Unset — the default — HIDES them, which is the whole point: a project root holds the author's
|
|
134
|
+
* forty documents and a tool's forty thousand, and the author already said which is which. The
|
|
135
|
+
* setting exists so that answer is not a trap. `.env`, a built `dist/` an author wants to read, a
|
|
136
|
+
* generated file being debugged: each is a legitimate reason to look at an ignored file, and a
|
|
137
|
+
* sidebar that cannot show one sends the author to a different program.
|
|
138
|
+
*
|
|
139
|
+
* Roams, because "show me everything" is a way of working rather than a property of a window.
|
|
140
|
+
*/
|
|
141
|
+
showIgnoredFiles: {
|
|
142
|
+
default: "",
|
|
143
|
+
key: "jx.files.showIgnored",
|
|
144
|
+
normalize: trimmed,
|
|
145
|
+
scope: "user",
|
|
146
|
+
},
|
|
130
147
|
cfAccountId: {
|
|
131
148
|
default: "",
|
|
132
149
|
key: "jx.cf.accountId",
|
|
@@ -28,9 +28,24 @@ import type { ToolRegistry } from "@jxsuite/ai/tools";
|
|
|
28
28
|
import type { Tab } from "../tabs/tab";
|
|
29
29
|
import { batchTab, beginBatch, endBatch } from "../tabs/transact";
|
|
30
30
|
import { beginTurn, endTurn } from "./ai-writes";
|
|
31
|
+
import { beginToolCall, beginTurnSignal, endTurnSignal } from "./ai-turn-signal";
|
|
31
32
|
|
|
32
33
|
const MAX_ROUNDS = 5;
|
|
33
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Hard ceiling on rounds of every kind, so the loop terminates whatever the model does.
|
|
37
|
+
*
|
|
38
|
+
* {@link MAX_ROUNDS} bounds AUTONOMOUS work, and a round that ends by blocking on a person is the
|
|
39
|
+
* opposite of runaway — it cannot advance without them, which is the property the cap was ever a
|
|
40
|
+
* proxy for. So an interactive round does not spend that budget. This one is the backstop that
|
|
41
|
+
* keeps `runAgentLoop` provably terminating anyway; a human answering ten questions will have
|
|
42
|
+
* stopped it long before.
|
|
43
|
+
*/
|
|
44
|
+
const MAX_TOTAL_ROUNDS = 25;
|
|
45
|
+
|
|
46
|
+
/** Tools that suspend the turn on a human rather than doing work. */
|
|
47
|
+
const INTERACTIVE_TOOLS = new Set(["ask_user"]);
|
|
48
|
+
|
|
34
49
|
interface RunAgentLoopOptions {
|
|
35
50
|
chatState: ReturnType<typeof createChatState>;
|
|
36
51
|
streamingClient: StreamingClient;
|
|
@@ -59,6 +74,11 @@ export async function runAgentLoop({
|
|
|
59
74
|
const turnId = () => chatState.messages.at(-1)?.id ?? "";
|
|
60
75
|
beginTurn(`turn:${chatState.messages.length}`);
|
|
61
76
|
|
|
77
|
+
/* The signal, published for the tools rather than passed to them: `ToolRegistry.execute` takes
|
|
78
|
+
none. Without it a tool that waits for a human never learns the turn was stopped, and the
|
|
79
|
+
`await` below is the whole loop. See services/ai-turn-signal.ts. */
|
|
80
|
+
beginTurnSignal(signal);
|
|
81
|
+
|
|
62
82
|
// Batch all tool-call mutations into a single undo step, anchored on the tab being edited.
|
|
63
83
|
if (getTab) {
|
|
64
84
|
beginBatch(getTab());
|
|
@@ -83,7 +103,9 @@ export async function runAgentLoop({
|
|
|
83
103
|
};
|
|
84
104
|
|
|
85
105
|
try {
|
|
86
|
-
|
|
106
|
+
/* Rounds that DID something, which is what MAX_ROUNDS bounds — see MAX_TOTAL_ROUNDS. */
|
|
107
|
+
let workRounds = 0;
|
|
108
|
+
for (let round = 1; round <= MAX_TOTAL_ROUNDS && workRounds < MAX_ROUNDS; round++) {
|
|
87
109
|
const messages = chatState.toMessagesArray();
|
|
88
110
|
const tools = toolRegistry.listForLLM();
|
|
89
111
|
|
|
@@ -144,8 +166,11 @@ export async function runAgentLoop({
|
|
|
144
166
|
return;
|
|
145
167
|
}
|
|
146
168
|
|
|
169
|
+
let didWork = false;
|
|
147
170
|
for (const [id, call] of toolCalls) {
|
|
148
171
|
let result;
|
|
172
|
+
// Published, not passed: `ToolRegistry.execute` takes the args and nothing else.
|
|
173
|
+
beginToolCall(id);
|
|
149
174
|
try {
|
|
150
175
|
const args = call.arguments ? (JSON.parse(call.arguments) as object) : {};
|
|
151
176
|
result = await toolRegistry.execute(call.name, args);
|
|
@@ -155,6 +180,9 @@ export async function runAgentLoop({
|
|
|
155
180
|
error: `Failed to parse arguments: ${(error as Error).message}`,
|
|
156
181
|
};
|
|
157
182
|
}
|
|
183
|
+
if (!INTERACTIVE_TOOLS.has(call.name)) {
|
|
184
|
+
didWork = true;
|
|
185
|
+
}
|
|
158
186
|
reanchorBatch();
|
|
159
187
|
if (!result.success && result.error) {
|
|
160
188
|
allErrors.push(result.error);
|
|
@@ -166,7 +194,10 @@ export async function runAgentLoop({
|
|
|
166
194
|
chatState.pushToolResultMessage(id, JSON.stringify(result));
|
|
167
195
|
}
|
|
168
196
|
|
|
169
|
-
if (
|
|
197
|
+
if (didWork) {
|
|
198
|
+
workRounds += 1;
|
|
199
|
+
}
|
|
200
|
+
if (workRounds < MAX_ROUNDS && round < MAX_TOTAL_ROUNDS) {
|
|
170
201
|
chatState.beginAssistantTurn();
|
|
171
202
|
}
|
|
172
203
|
}
|
|
@@ -196,6 +227,7 @@ export async function runAgentLoop({
|
|
|
196
227
|
}
|
|
197
228
|
chatState.setError(tail);
|
|
198
229
|
} finally {
|
|
230
|
+
endTurnSignal();
|
|
199
231
|
endBatch();
|
|
200
232
|
endTurn(turnId());
|
|
201
233
|
}
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
* alone), parks any rejection under the control that caused it, and re-renders. §7.1's third tier:
|
|
31
31
|
* a bad value belongs at its control, not in a toast that expires.
|
|
32
32
|
*
|
|
33
|
-
* @docs studio/projects/
|
|
33
|
+
* @docs studio/projects/settings
|
|
34
34
|
*/
|
|
35
35
|
|
|
36
36
|
import { html, render as litRender, nothing } from "lit-html";
|
|
@@ -140,6 +140,15 @@ type ErrorTarget = string;
|
|
|
140
140
|
*/
|
|
141
141
|
const errors = new WeakMap<HTMLElement, { target: ErrorTarget; message: string }>();
|
|
142
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Schema errors the file already had when this section last wrote to it.
|
|
145
|
+
*
|
|
146
|
+
* Reported at section level rather than under a control, because they belong to no control — and
|
|
147
|
+
* kept per container for the same reason {@link errors} is. Populated by the first write, since
|
|
148
|
+
* validating on every render would compile a schema for a section nobody is editing.
|
|
149
|
+
*/
|
|
150
|
+
const inheritedErrors = new WeakMap<HTMLElement, string[]>();
|
|
151
|
+
|
|
143
152
|
/** Read the current failure for a container — exported for tests and for re-render helpers. */
|
|
144
153
|
export function contextsError(container: HTMLElement): { target: string; message: string } | null {
|
|
145
154
|
return errors.get(container) ?? null;
|
|
@@ -171,12 +180,26 @@ export function renderContextsSection(container: HTMLElement): void {
|
|
|
171
180
|
* shape, and the disk can refuse the write. The predecessor surfaces (`Properties › Media` and
|
|
172
181
|
* the CSS-variables "Enable dark scheme") did `void updateSiteConfig(...)` and dropped the
|
|
173
182
|
* rejection, so a failed write read as the field snapping back for no reason.
|
|
183
|
+
*
|
|
184
|
+
* **Only the errors THIS edit introduces block it.** The candidate is the whole `project.json`,
|
|
185
|
+
* so a violation anywhere else in the file used to refuse every context edit and park the reason
|
|
186
|
+
* under whichever control had been touched — typing `1280px` into Base width reported three
|
|
187
|
+
* unevaluated-property errors about `title`, `description` and `$style`, none of which is a
|
|
188
|
+
* width. The baseline is validated first and subtracted, the same way `services/ai-tools.ts`
|
|
189
|
+
* subtracts it before blaming the model for a document it inherited. A pre-existing problem is
|
|
190
|
+
* still worth saying, so it is reported once at section level, where a whole-file problem belongs
|
|
191
|
+
* — and it is reported rather than repaired, because `project.json` is the author's file and a
|
|
192
|
+
* settings screen that silently drops keys it did not recognise is worse than one that names
|
|
193
|
+
* them.
|
|
174
194
|
*/
|
|
175
195
|
const persistMedia = async (next: Record<string, string>, target: ErrorTarget) => {
|
|
176
196
|
const candidate = { ...config, $media: next } as unknown;
|
|
177
197
|
let schemaErrors: string[] = [];
|
|
198
|
+
let inherited: string[] = [];
|
|
178
199
|
try {
|
|
179
|
-
|
|
200
|
+
inherited = await validateProjectConfig(config);
|
|
201
|
+
const found = await validateProjectConfig(candidate);
|
|
202
|
+
schemaErrors = found.filter((error) => !inherited.includes(error));
|
|
180
203
|
} catch (error) {
|
|
181
204
|
/* A validator that cannot compile must not block the edit — but it must not be silent
|
|
182
205
|
either, so the message rides the same inline slot the schema errors would have used. */
|
|
@@ -187,6 +210,7 @@ export function renderContextsSection(container: HTMLElement): void {
|
|
|
187
210
|
reject(target, schemaErrors.join("; "));
|
|
188
211
|
return;
|
|
189
212
|
}
|
|
213
|
+
inheritedErrors.set(container, inherited);
|
|
190
214
|
try {
|
|
191
215
|
await updateSiteConfig({ $media: next });
|
|
192
216
|
errors.delete(container);
|
|
@@ -199,6 +223,24 @@ export function renderContextsSection(container: HTMLElement): void {
|
|
|
199
223
|
renderContextsSection(container);
|
|
200
224
|
};
|
|
201
225
|
|
|
226
|
+
/**
|
|
227
|
+
* What is already wrong with `project.json`, said once and not blamed on a control.
|
|
228
|
+
*
|
|
229
|
+
* It does not block anything — the edit that surfaced it went through. It is here because a file
|
|
230
|
+
* that fails its own schema will keep failing it, and until this notice existed the only place
|
|
231
|
+
* that fact appeared was as an unexplained refusal of an unrelated field.
|
|
232
|
+
*/
|
|
233
|
+
const inheritedNotice = () => {
|
|
234
|
+
const inherited = inheritedErrors.get(container) ?? [];
|
|
235
|
+
return inherited.length === 0
|
|
236
|
+
? nothing
|
|
237
|
+
: html`<p class="settings-section-notice" role="status">
|
|
238
|
+
project.json has ${inherited.length} pre-existing schema
|
|
239
|
+
${inherited.length === 1 ? "problem" : "problems"} that this section did not cause:
|
|
240
|
+
${inherited.join("; ")}
|
|
241
|
+
</p>`;
|
|
242
|
+
};
|
|
243
|
+
|
|
202
244
|
/** The error line for one control, when that is where the current failure belongs. */
|
|
203
245
|
const errorFor = (target: ErrorTarget) =>
|
|
204
246
|
shown?.target === target
|
|
@@ -370,7 +412,7 @@ export function renderContextsSection(container: HTMLElement): void {
|
|
|
370
412
|
The conditions this project's pages are rendered under. Define them once here; choose
|
|
371
413
|
between them on the pane's context control while you edit.
|
|
372
414
|
</p>
|
|
373
|
-
${errorFor("section")}
|
|
415
|
+
${errorFor("section")} ${inheritedNotice()}
|
|
374
416
|
|
|
375
417
|
<div class="settings-field">
|
|
376
418
|
<label class="settings-field-label">Base width</label>
|