@jxsuite/studio 5.0.0 → 5.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/dist/chunks/{cf-account-picker-fyf63pmy.js → cf-account-picker-qwkm9zrf.js} +5 -5
- package/dist/chunks/{components-2w1f35qs.js → components-vj72gqtq.js} +6 -4
- package/dist/chunks/{content-source-3wqeas45.js → content-source-5mex4x1h.js} +20 -20
- package/dist/chunks/{convert-to-repeater-d0cgnb6d.js → convert-to-repeater-n64h8znc.js} +20 -20
- package/dist/chunks/entry-commands-234hjnqw.js +42 -0
- package/dist/chunks/{extension-sections-fy7q4fe9.js → extension-sections-3hpcambp.js} +22 -22
- package/dist/chunks/{file-ops-3btj6st0.js → file-ops-8yhd83xr.js} +19 -19
- package/dist/chunks/{files-ef8fgs3w.js → files-hnxqxtsg.js} +26 -26
- package/dist/chunks/{format-host-3c33nwr8.js → format-host-779sfp2n.js} +2 -2
- package/dist/chunks/{fs-events-13tj6sex.js → fs-events-b88z7g4h.js} +4 -4
- package/dist/chunks/{git-panel-4wp2rn9b.js → git-panel-xwj8fpab.js} +18 -18
- package/dist/chunks/{iframe-host-jgesd924.js → iframe-host-syr0f9sb.js} +9 -9
- package/dist/chunks/{layers-1nq5evsh.js → layers-ff81wthn.js} +5 -4
- package/dist/chunks/layers-panel-pnnghw12.js +40 -0
- package/dist/chunks/{media-picker-2qbetq73.js → media-picker-cqhhxhhe.js} +10 -10
- package/dist/chunks/menu-9p2w46gj.js +15 -0
- package/dist/chunks/{publish-panel-98f7sr3g.js → publish-panel-5zt840sy.js} +9 -9
- package/dist/chunks/quick-search-3748564r.js +45 -0
- package/dist/chunks/{references-n553f0e1.js → references-qvvxej7h.js} +3 -3
- package/dist/chunks/right-panel-eccz92qt.js +45 -0
- package/dist/chunks/{studio-x3rty6tr.js → studio-1knfqgy7.js} +6 -6
- package/dist/chunks/{studio-rkstz940.js → studio-1z1kcjv4.js} +17 -8
- package/dist/chunks/{studio-qahxvnan.js → studio-3773086z.js} +2 -2
- package/dist/chunks/{studio-arfpe389.js → studio-3rz5md06.js} +4 -4
- package/dist/chunks/{studio-pm6hgy7r.js → studio-5wt9nvhd.js} +16 -16
- package/dist/chunks/{studio-akbww0j5.js → studio-7xnmgv66.js} +19 -19
- package/dist/chunks/{studio-26d7myd1.js → studio-8scdew0e.js} +3 -3
- package/dist/chunks/{studio-3hxkd92t.js → studio-93zafjcm.js} +8 -8
- package/dist/chunks/{studio-5rt7sqa0.js → studio-a79k90n9.js} +11 -17
- package/dist/chunks/{studio-3gxv60tn.js → studio-aj37eer3.js} +3 -3
- package/dist/chunks/{studio-18avnhjp.js → studio-b0k5tqgp.js} +23 -21
- package/dist/chunks/{studio-fabxk6r9.js → studio-b4s9eykd.js} +13 -13
- package/dist/chunks/{studio-8qtrpzr3.js → studio-e1pgga3k.js} +5 -5
- package/dist/chunks/{studio-8jn5yhyn.js → studio-g3jevyjt.js} +2 -2
- package/dist/chunks/{studio-rpyah2ew.js → studio-gb13095a.js} +18 -17
- package/dist/chunks/{studio-57mf66w2.js → studio-js8nv53n.js} +5 -5
- package/dist/chunks/{studio-vz68yn9e.js → studio-jxp12rnj.js} +4 -4
- package/dist/chunks/{studio-az3zd49t.js → studio-k75n7vhp.js} +56 -4
- package/dist/chunks/{studio-5jy81t3s.js → studio-kkqkcp3d.js} +221 -16
- package/dist/chunks/{studio-tb3ev01j.js → studio-mhhyamxf.js} +4 -4
- package/dist/chunks/{studio-81390n6f.js → studio-nq5yf5mp.js} +1 -1
- package/dist/chunks/{studio-4m0haseg.js → studio-nzzpvzx9.js} +3 -3
- package/dist/chunks/{studio-em9nw8eh.js → studio-pxbgypmg.js} +12 -12
- package/dist/chunks/{studio-sgdc5pfc.js → studio-qhhm6hbt.js} +3 -3
- package/dist/chunks/{studio-d1ja2x9r.js → studio-qk4bfsq2.js} +3 -3
- package/dist/chunks/{studio-25hkdrkw.js → studio-qk90fgqm.js} +9 -9
- package/dist/chunks/{studio-s6z853aq.js → studio-r6nvacqs.js} +18 -17
- package/dist/chunks/{studio-7xptrg0j.js → studio-sbjyc8bt.js} +6 -6
- package/dist/chunks/{studio-5523bpzt.js → studio-syg2tadj.js} +3 -3
- package/dist/chunks/{studio-cpvb2ed1.js → studio-tqgryxwg.js} +7 -7
- package/dist/chunks/{studio-xsykpvsh.js → studio-try13h8c.js} +11 -11
- package/dist/chunks/{studio-erdatzvp.js → studio-vfq8qpjt.js} +34 -5
- package/dist/chunks/{studio-79y5sz51.js → studio-vw7mzp6z.js} +2 -2
- package/dist/chunks/{studio-9xxz1q69.js → studio-wqz6dggm.js} +5 -5
- package/dist/chunks/studio-zdn2g6dt.js +148 -0
- package/dist/chunks/{workspace-wrew8zk4.js → workspace-t55jdjbx.js} +3 -3
- package/dist/iframe-entry.js +97 -10
- package/dist/studio.js +127 -244
- package/package.json +6 -6
- package/src/canvas/canvas-utils.ts +29 -2
- package/src/commands/command-args.ts +37 -2
- package/src/commands/registry.ts +18 -8
- package/src/commands/run-reported.ts +120 -0
- package/src/editor/shortcuts.ts +11 -23
- package/src/files/components.ts +39 -0
- package/src/files/files.ts +3 -2
- package/src/i18n/i18n-commands.ts +10 -0
- package/src/packages/pull-package-sync.ts +5 -1
- package/src/panels/ai-panel.ts +5 -2
- package/src/panels/block-action-bar.ts +7 -2
- package/src/panels/i18n-panel.ts +8 -15
- package/src/panels/jump-bar.ts +3 -2
- package/src/panels/problems-panel.ts +6 -2
- package/src/panels/quick-search.ts +6 -12
- package/src/panels/settings-menu.ts +5 -15
- package/src/panels/tab-strip.ts +3 -3
- package/src/services/ai-command-tools.ts +78 -8
- package/src/services/ai-project-tools.ts +8 -2
- package/src/services/ai-system-prompt.ts +4 -8
- package/src/services/ai-tools.ts +8 -69
- package/src/services/document-assistant.ts +9 -6
- package/src/studio.ts +14 -2
- package/src/surfaces/commandbar.json +13 -12
- package/src/surfaces/commandbar.ts +6 -5
- package/src/surfaces/statusbar.ts +2 -1
- package/src/tabs/project-config.ts +45 -11
- package/src/ui/layers.ts +4 -1
- package/src/workspace/workspace.ts +60 -0
- package/dist/chunks/entry-commands-d6g50j7s.js +0 -42
- package/dist/chunks/layers-panel-ar2gh90g.js +0 -40
- package/dist/chunks/menu-rf7n72mf.js +0 -14
- package/dist/chunks/quick-search-srzz83k2.js +0 -45
- package/dist/chunks/right-panel-rrgx2dn3.js +0 -45
- package/dist/chunks/studio-d055c37e.js +0 -62
|
@@ -58,6 +58,7 @@
|
|
|
58
58
|
import { nothing } from "lit-html";
|
|
59
59
|
import { effect, effectScope } from "../reactivity";
|
|
60
60
|
import { activeRegistry } from "../commands/active-registry";
|
|
61
|
+
import { runReported } from "../commands/run-reported";
|
|
61
62
|
import { clearProblems, dismiss, problemCount, problems } from "../services/notify";
|
|
62
63
|
import { mountProblemsSurface } from "../surfaces/panel-problems";
|
|
63
64
|
import { registerPanel } from "./panel-registry";
|
|
@@ -184,14 +185,17 @@ const ACTIONS: ProblemsActions = {
|
|
|
184
185
|
openProblemPath(path);
|
|
185
186
|
},
|
|
186
187
|
/* By id, because a document can only hand back a value: the record and the registry are both
|
|
187
|
-
looked up at the moment of the click, which is also the moment the gate was last checked.
|
|
188
|
+
looked up at the moment of the click, which is also the moment the gate was last checked.
|
|
189
|
+
The action's `actionArgs` were written by whoever filed the problem, against a schema that
|
|
190
|
+
may have moved since, so the run goes through `commands/run-reported.ts`: a refusal is a new
|
|
191
|
+
row in this same list, keyed by the command, rather than an exception out of the button. */
|
|
188
192
|
runAction: (id) => {
|
|
189
193
|
const record = problems.find((candidate) => candidate.id === id);
|
|
190
194
|
const registry = activeRegistry();
|
|
191
195
|
if (record?.action === undefined || !registry) {
|
|
192
196
|
return;
|
|
193
197
|
}
|
|
194
|
-
void registry
|
|
198
|
+
void runReported(registry, record.action, record.actionArgs, "Problems");
|
|
195
199
|
},
|
|
196
200
|
};
|
|
197
201
|
|
|
@@ -41,6 +41,7 @@ import { reactive } from "../reactivity";
|
|
|
41
41
|
import paletteDoc from "../surfaces/palette.json";
|
|
42
42
|
import { activeTab } from "../workspace/workspace";
|
|
43
43
|
import { activeRegistry } from "../commands/active-registry";
|
|
44
|
+
import { runActiveReported, runReported } from "../commands/run-reported";
|
|
44
45
|
import type { PaletteMode } from "../commands/defaults";
|
|
45
46
|
import type { AnyCommand, CommandRegistry } from "../commands/registry";
|
|
46
47
|
import type { JxDocument, JxMutableNode } from "@jxsuite/schema/types";
|
|
@@ -671,17 +672,11 @@ function runCommand(command: AnyCommand, args?: Record<string, unknown>) {
|
|
|
671
672
|
}
|
|
672
673
|
trackRecentCommand(command.id);
|
|
673
674
|
closeQuickSearch();
|
|
674
|
-
|
|
675
|
-
console.error(`palette: command "${command.id}" failed`, error);
|
|
676
|
-
};
|
|
677
|
-
// Both shapes, deliberately: `run` may throw synchronously (a coercion refusal from
|
|
675
|
+
// Both shapes, through the one helper: `run` may throw synchronously (a coercion refusal from
|
|
678
676
|
// `command-args.ts` does) or reject later. A palette that let either escape would take the
|
|
679
|
-
// Keydown listener down with it, and the overlay is already closed by then
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
} catch (error) {
|
|
683
|
-
report(error);
|
|
684
|
-
}
|
|
677
|
+
// Keydown listener down with it, and the overlay is already closed by then — and a refusal that
|
|
678
|
+
// Went to the console, as this one did, was read by nobody. Problems is where it is read.
|
|
679
|
+
void runReported(registry, command.id, args, "Palette");
|
|
685
680
|
}
|
|
686
681
|
|
|
687
682
|
function selectRow(row: PaletteRow) {
|
|
@@ -702,9 +697,8 @@ function selectRow(row: PaletteRow) {
|
|
|
702
697
|
return;
|
|
703
698
|
}
|
|
704
699
|
case "node": {
|
|
705
|
-
const registry = activeRegistry();
|
|
706
700
|
closeQuickSearch();
|
|
707
|
-
void
|
|
701
|
+
void runActiveReported("selection.set", { path: row.path }, "Palette");
|
|
708
702
|
return;
|
|
709
703
|
}
|
|
710
704
|
case "arg": {
|
|
@@ -36,10 +36,9 @@
|
|
|
36
36
|
* @docs studio/projects/settings
|
|
37
37
|
*/
|
|
38
38
|
|
|
39
|
-
import { errorMessage } from "@jxsuite/schema/parse";
|
|
40
39
|
import { activeRegistry } from "../commands/active-registry";
|
|
40
|
+
import { runReported } from "../commands/run-reported";
|
|
41
41
|
import { LEVELS } from "../commands/levels";
|
|
42
|
-
import { notify } from "../services/notify";
|
|
43
42
|
import { onSettingsDocumentChanged, sortedSettingsSections } from "../settings/section-registry";
|
|
44
43
|
import { PREFERENCES_SECTIONS } from "../settings/preferences-sections";
|
|
45
44
|
import { openMenu } from "../surfaces/menu";
|
|
@@ -92,25 +91,16 @@ function menuSectionsFor(id: string): readonly MenuSection[] {
|
|
|
92
91
|
* Run a row's command, surfacing a refusal instead of stranding it.
|
|
93
92
|
*
|
|
94
93
|
* `settings.open`'s `run` is async and throws a `RangeError` AFTER awaiting the contributed-section
|
|
95
|
-
* sync, and `registry.run` does not catch
|
|
96
|
-
*
|
|
97
|
-
*
|
|
94
|
+
* sync, and `registry.run` does not catch; a gate that turned false between the render and the
|
|
95
|
+
* click throws BEFORE `run`, synchronously. `commands/run-reported.ts` catches both and files the
|
|
96
|
+
* sentence in Problems, under this menu's name.
|
|
98
97
|
*/
|
|
99
98
|
function runRow(id: string, args?: Record<string, unknown>): void {
|
|
100
99
|
const registry = activeRegistry();
|
|
101
100
|
if (!registry) {
|
|
102
101
|
return;
|
|
103
102
|
}
|
|
104
|
-
|
|
105
|
-
try {
|
|
106
|
-
result = registry.run(id, args as never);
|
|
107
|
-
} catch (error) {
|
|
108
|
-
notify.error(errorMessage(error), { source: id });
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
void Promise.resolve(result).catch((error: unknown) => {
|
|
112
|
-
notify.error(errorMessage(error), { source: id });
|
|
113
|
-
});
|
|
103
|
+
void runReported(registry, id, args, "Settings");
|
|
114
104
|
}
|
|
115
105
|
|
|
116
106
|
// ─── Rows ─────────────────────────────────────────────────────────────────────
|
package/src/panels/tab-strip.ts
CHANGED
|
@@ -60,6 +60,7 @@ import { DRAFT_FIELD, isDraftEntry } from "../content/draft-state";
|
|
|
60
60
|
import { entryFields } from "../content/entry-fields";
|
|
61
61
|
import { collectionOfPath } from "../content/entry-model";
|
|
62
62
|
import { activeRegistry } from "../commands/active-registry";
|
|
63
|
+
import { runActiveReported, runReported } from "../commands/run-reported";
|
|
63
64
|
import { PRESET_LABELS } from "../workspace/pane-derive";
|
|
64
65
|
import { localeLabel } from "@jxsuite/schema/locale";
|
|
65
66
|
import { tabOfPane } from "../canvas/canvas-surface";
|
|
@@ -575,8 +576,7 @@ function openTrailing(host: HTMLElement, anchor: HTMLElement | null) {
|
|
|
575
576
|
return;
|
|
576
577
|
}
|
|
577
578
|
if (_drawing.get(host)?.mode === "derivation") {
|
|
578
|
-
|
|
579
|
-
void registry?.run("pane.unsplit");
|
|
579
|
+
void runActiveReported("pane.unsplit", undefined, "Tabs");
|
|
580
580
|
return;
|
|
581
581
|
}
|
|
582
582
|
openOverflowMenu(pane, tabLabels(pane), anchor);
|
|
@@ -807,7 +807,7 @@ function placedTabItems(tab: Tab): MenuRowProjection[] {
|
|
|
807
807
|
? {
|
|
808
808
|
disabled: false,
|
|
809
809
|
run: () => {
|
|
810
|
-
void registry
|
|
810
|
+
void runReported(registry, command.id, args, "Tabs");
|
|
811
811
|
},
|
|
812
812
|
}
|
|
813
813
|
: { disabled: true, requires: reason }),
|
|
@@ -32,6 +32,25 @@
|
|
|
32
32
|
* the model is told "changed nothing" rather than congratulated. This is what closes the
|
|
33
33
|
* phantom-success finding for every bridged document write, not just the one tool it was found on.
|
|
34
34
|
*
|
|
35
|
+
* **The project witness** is the same comparison over `project.json`. Every configuration write is
|
|
36
|
+
* a transaction on the configuration document (`tabs/project-config.ts`), whose bind effect keeps
|
|
37
|
+
* `projectState.projectConfig` equal to `toRaw(document.doc.document)` — so a record with `undo:
|
|
38
|
+
* "project"` that left that reference alone wrote nothing, whatever its report says. The ledger
|
|
39
|
+
* then files nothing (`wrote: []`, mechanically, for every project record), and a report that
|
|
40
|
+
* named no `wrote` of its own — one with no latch to know it was a no-op — has its sentence
|
|
41
|
+
* replaced by "changed nothing", because "Wrote project.json" over an untouched file is the
|
|
42
|
+
* phantom the ledger exists to keep out. A record whose report answered `wrote: []` itself is one
|
|
43
|
+
* that KNEW, and its sentence ("already enabled") is the one the witness cannot write. That is why
|
|
44
|
+
* `add_project_locale` keeps its latch: the sentence, not the ledger, is what the latch is for.
|
|
45
|
+
*
|
|
46
|
+
* **A tool with nothing it could be called with is withheld.** A `derivedEnumProperty` on a
|
|
47
|
+
* REQUIRED argument serialises `enum: []` when the list behind it is empty — `enable_extension`
|
|
48
|
+
* with no catalogue — and `@jxsuite/ai`'s validator tolerates that only because every projected
|
|
49
|
+
* tool is `strict: false`. The record's gate cannot see its own argument list, so the bridge reads
|
|
50
|
+
* it: an enabled record whose required enum is empty is not advertised this round, and comes back
|
|
51
|
+
* the round the list does. `getDefinition` still answers, so a call the model makes anyway meets
|
|
52
|
+
* the coercion's own refusal (`is not declared — declared: none`) rather than `Unknown tool`.
|
|
53
|
+
*
|
|
35
54
|
* **`validate()` is a no-op, on purpose.** `@jxsuite/ai`'s validator never checks `enum` and treats
|
|
36
55
|
* a required `null` as missing (`packages/ai/src/tools.ts`), which would refuse `select_node
|
|
37
56
|
* { path: null }`. The registry's coercion is the single validator, so every projected tool is
|
|
@@ -46,6 +65,7 @@ import { activeRegistry } from "../commands/active-registry";
|
|
|
46
65
|
import { argsSchema } from "../commands/command-args";
|
|
47
66
|
import { inCanvas } from "../commands/context";
|
|
48
67
|
import { toRaw } from "../reactivity";
|
|
68
|
+
import { projectState } from "../state";
|
|
49
69
|
import { recordWrite } from "./ai-writes";
|
|
50
70
|
import { reportDocumentWrite, snapshotBeforeWrite } from "./ai-write-report";
|
|
51
71
|
import type { WriteReportDeps, WriteSnapshot } from "./ai-write-report";
|
|
@@ -76,10 +96,25 @@ const EMPTY_ARGS = argsSchema({});
|
|
|
76
96
|
|
|
77
97
|
/** The loosely-typed view of a record's `args` this module reads. */
|
|
78
98
|
interface ArgsShape {
|
|
79
|
-
properties?: Record<string,
|
|
99
|
+
properties?: Record<string, { enum?: unknown }>;
|
|
80
100
|
required?: readonly string[];
|
|
81
101
|
}
|
|
82
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Whether a required argument's choice list is empty right now.
|
|
105
|
+
*
|
|
106
|
+
* Reading `enum` is what serialises a `derivedEnumProperty` — the getter runs here exactly as it
|
|
107
|
+
* runs on the wire — so this is the same answer the model would have been shown. Only a REQUIRED
|
|
108
|
+
* property counts: an optional enum that is empty is an argument the model can leave out.
|
|
109
|
+
*/
|
|
110
|
+
function hasEmptyRequiredEnum(args: object | undefined): boolean {
|
|
111
|
+
const shape = (args ?? EMPTY_ARGS) as ArgsShape;
|
|
112
|
+
return (shape.required ?? []).some((key) => {
|
|
113
|
+
const declared = shape.properties?.[key]?.enum;
|
|
114
|
+
return Array.isArray(declared) && declared.length === 0;
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
83
118
|
/**
|
|
84
119
|
* The selector's own `paths` property object, taken BY REFERENCE from the live record so the
|
|
85
120
|
* description the model reads is the selector's. `undefined` when the selector is not registered,
|
|
@@ -141,12 +176,14 @@ function selectionAdvertised(registry: CommandRegistry): boolean {
|
|
|
141
176
|
/**
|
|
142
177
|
* Whether one record's tool is advertised this round. For a project- or document-level record it is
|
|
143
178
|
* the record's own gate, evaluated as the palette evaluates it — a module-state `enablement`
|
|
144
|
-
* closure included. Selection-level records are the one derived case, above.
|
|
179
|
+
* closure included. Selection-level records are the one derived case, above. And a record the gate
|
|
180
|
+
* admits is still withheld while a required argument has an empty choice list: there is nothing it
|
|
181
|
+
* could be called with, and a tool advertised with `enum: []` costs a round to discover that.
|
|
145
182
|
*/
|
|
146
183
|
function advertised(registry: CommandRegistry, command: AnyCommand): boolean {
|
|
147
|
-
|
|
148
|
-
? selectionAdvertised(registry)
|
|
149
|
-
|
|
184
|
+
const open =
|
|
185
|
+
command.level === "selection" ? selectionAdvertised(registry) : registry.isEnabled(command.id);
|
|
186
|
+
return open && !hasEmptyRequiredEnum(command.args);
|
|
150
187
|
}
|
|
151
188
|
|
|
152
189
|
/** Every record carrying a projection, enabled or not. */
|
|
@@ -251,6 +288,19 @@ function normaliseReport(report: string | AiToolReport): AiToolReport {
|
|
|
251
288
|
return typeof report === "string" ? { summary: report } : report;
|
|
252
289
|
}
|
|
253
290
|
|
|
291
|
+
/**
|
|
292
|
+
* The configuration object the app is live on, unwrapped, or `null` before a project has one.
|
|
293
|
+
*
|
|
294
|
+
* The same read `tabs/project-config.ts`'s `liveConfig()` makes: `projectState.projectConfig` is
|
|
295
|
+
* held to `toRaw(document.doc.document)` by the bind effect, so a `transactDoc` on the
|
|
296
|
+
* configuration document is a new reference here — and an unchanged reference is a run that never
|
|
297
|
+
* transacted, however the record's report reads.
|
|
298
|
+
*/
|
|
299
|
+
function projectConfigRef(): object | null {
|
|
300
|
+
const config = projectState?.projectConfig;
|
|
301
|
+
return config ? toRaw(config as unknown as object) : null;
|
|
302
|
+
}
|
|
303
|
+
|
|
254
304
|
/**
|
|
255
305
|
* The ledger paths a record's write defaults to when `report` names none, by undo scope. `"none"`
|
|
256
306
|
* has no default on purpose — see {@link AiToolReport.wrote}.
|
|
@@ -322,6 +372,10 @@ export function createCommandToolRegistry(deps: CommandToolDeps): ToolRegistry {
|
|
|
322
372
|
what `transactDoc` replaces, and the snapshot is what the verdict compares against. */
|
|
323
373
|
const tab = command.undo === "document" ? deps.getTab() : null;
|
|
324
374
|
const rootBefore = tab ? toRaw(tab.doc.document) : null;
|
|
375
|
+
/* The project witness's half: the configuration object the chokepoint holds, which every
|
|
376
|
+
applied `project.json` transaction replaces. Read for `undo: "project"` records only — the
|
|
377
|
+
comparison is meaningless for a record that does not claim the file. */
|
|
378
|
+
const configBefore = command.undo === "project" ? projectConfigRef() : null;
|
|
325
379
|
let snapshot: WriteSnapshot | null = null;
|
|
326
380
|
let before: CommandContext;
|
|
327
381
|
try {
|
|
@@ -357,6 +411,12 @@ export function createCommandToolRegistry(deps: CommandToolDeps): ToolRegistry {
|
|
|
357
411
|
};
|
|
358
412
|
}
|
|
359
413
|
|
|
414
|
+
/* The project witness's verdict, decided before the report so a report that throws is filed
|
|
415
|
+
against it too. Unlike the document witness this is not a refusal: an idempotent project verb
|
|
416
|
+
asked for a state it already had is an ordinary answer, and the record's own sentence may
|
|
417
|
+
say why. What the witness owns is the LEDGER — nothing landed, so nothing is filed. */
|
|
418
|
+
const projectUnchanged = command.undo === "project" && projectConfigRef() === configBefore;
|
|
419
|
+
|
|
360
420
|
const after = registry.context();
|
|
361
421
|
let report: AiToolReport;
|
|
362
422
|
try {
|
|
@@ -366,13 +426,23 @@ export function createCommandToolRegistry(deps: CommandToolDeps): ToolRegistry {
|
|
|
366
426
|
Left to the loop, the throw would be labelled "Failed to parse arguments" and the ledger
|
|
367
427
|
would never be filed. So the ledger is filed from `undo`'s defaults — the report that would
|
|
368
428
|
have named the paths is the thing that failed — and the model is told both halves. */
|
|
369
|
-
fileLedger(command, tab);
|
|
429
|
+
fileLedger(command, tab, projectUnchanged ? [] : undefined);
|
|
370
430
|
const message = error instanceof Error ? error.message : String(error);
|
|
371
431
|
return { error: `${command.title} ran, but its report failed: ${message}`, success: false };
|
|
372
432
|
}
|
|
373
|
-
fileLedger(command, tab, report.wrote);
|
|
374
433
|
|
|
375
|
-
let { summary } = report;
|
|
434
|
+
let { summary, wrote } = report;
|
|
435
|
+
if (projectUnchanged) {
|
|
436
|
+
/* The ledger files nothing whatever the report named. A report that did not itself know it
|
|
437
|
+
was a no-op has its sentence replaced — a claim of a write over an untouched file is
|
|
438
|
+
exactly the phantom this exists to catch. A report that answered `wrote: []` knew, and its
|
|
439
|
+
sentence is kept: it says WHY nothing changed, which the witness cannot. */
|
|
440
|
+
wrote = [];
|
|
441
|
+
if (report.wrote === undefined || report.wrote.length > 0) {
|
|
442
|
+
summary = `${command.title} changed nothing: project.json is exactly as it was.`;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
fileLedger(command, tab, wrote);
|
|
376
446
|
if (tab && snapshot) {
|
|
377
447
|
const verdict = await reportDocumentWrite(tab, snapshot, summary, deps);
|
|
378
448
|
if (!verdict.success) {
|
|
@@ -115,8 +115,14 @@ export interface ProjectToolsCtx {
|
|
|
115
115
|
* tab happened to be re-read from disk afterwards ({@link ProjectToolsCtx.reloadTab}); that
|
|
116
116
|
* re-read is gone, because a second parse of `project.json` is the very rival object the
|
|
117
117
|
* configuration document exists to prevent.
|
|
118
|
+
*
|
|
119
|
+
* `text` is the bytes that reached the file, of which `config` is the parse. The document keeps a
|
|
120
|
+
* layout record for the file it holds (§9.4), and the assistant's write replaces the file — so
|
|
121
|
+
* the record has to travel with the write, or the next settings commit re-lays the file the model
|
|
122
|
+
* just formatted with the record of the file it had read before (issue 331). Handing the text
|
|
123
|
+
* over is what lets the document derive that record without reading the file back.
|
|
118
124
|
*/
|
|
119
|
-
onProjectConfigWritten?: (config: ProjectConfig) => void | Promise<void>;
|
|
125
|
+
onProjectConfigWritten?: (config: ProjectConfig, text: string) => void | Promise<void>;
|
|
120
126
|
}
|
|
121
127
|
|
|
122
128
|
/**
|
|
@@ -399,7 +405,7 @@ export function registerProjectTools(
|
|
|
399
405
|
that tab, which is the split the adoption just closed. Every other path is a plain file
|
|
400
406
|
and its tab has to be told from disk. */
|
|
401
407
|
if (projectConfig) {
|
|
402
|
-
await onProjectConfigWritten?.(projectConfig);
|
|
408
|
+
await onProjectConfigWritten?.(projectConfig, content);
|
|
403
409
|
} else if (openTab) {
|
|
404
410
|
await reloadTab(relPath);
|
|
405
411
|
}
|
|
@@ -117,8 +117,8 @@ export interface AiToolInfo {
|
|
|
117
117
|
*
|
|
118
118
|
* The other kind of tool is not here and must not be added here: a command record that declares
|
|
119
119
|
* `aiTool` is projected by `services/ai-command-tools.ts`, gated by the record's own `when` /
|
|
120
|
-
* `enablement`, and described by the record. `enable_extension`, `disable_extension`
|
|
121
|
-
*
|
|
120
|
+
* `enablement`, and described by the record. `enable_extension`, `disable_extension`, `delete_node`
|
|
121
|
+
* (né `remove_node`) and `open_document` left this table for that one.
|
|
122
122
|
*/
|
|
123
123
|
export const AI_TOOL_TIERS: AiToolInfo[] = [
|
|
124
124
|
// Always — a question is not gated on what happens to be open
|
|
@@ -185,12 +185,8 @@ export const AI_TOOL_TIERS: AiToolInfo[] = [
|
|
|
185
185
|
tier: "project",
|
|
186
186
|
blurb: "create_page(path, content) — create a new .json page file on disk.",
|
|
187
187
|
},
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
tier: "project",
|
|
191
|
-
blurb:
|
|
192
|
-
"open_document(path) — open a file on the canvas as the active document; the document tools then operate on it. Use when the user should SEE the page, or for iterative visual refinement.",
|
|
193
|
-
},
|
|
188
|
+
/* `open_document` is not a row: it is the projection of the `document.open` record
|
|
189
|
+
(`workspace/workspace.ts`), advertised by the record's own gate and described by the record. */
|
|
194
190
|
// Document (an active document on the canvas)
|
|
195
191
|
{
|
|
196
192
|
name: "read_document",
|
package/src/services/ai-tools.ts
CHANGED
|
@@ -14,9 +14,6 @@ import type { JxMutableNode, JxPath, JxStateDefinition } from "@jxsuite/schema/t
|
|
|
14
14
|
import { getNodeAtPath } from "../state";
|
|
15
15
|
import type { Tab } from "../tabs/tab";
|
|
16
16
|
import {
|
|
17
|
-
beginBatch,
|
|
18
|
-
endBatch,
|
|
19
|
-
isBatching,
|
|
20
17
|
mutateInsertNode,
|
|
21
18
|
mutateMoveNode,
|
|
22
19
|
mutateUpdateProperty,
|
|
@@ -110,7 +107,6 @@ async function applyAndValidate(
|
|
|
110
107
|
* validate?: (doc: unknown) => Promise<string[]>;
|
|
111
108
|
* saveFile?: (relPath: string, content: string) => Promise<void>;
|
|
112
109
|
* renderCheck?: (doc: unknown) => Promise<{ ok: true } | { ok: false; error: string }>;
|
|
113
|
-
* openDocument?: (path: string) => Promise<void>;
|
|
114
110
|
* projectStyle?: Record<string, string>;
|
|
115
111
|
* getProjectStyle?: () => Record<string, string> | undefined;
|
|
116
112
|
* findOpenTab?: (path: string) => import("../tabs/tab").Tab | null;
|
|
@@ -124,7 +120,6 @@ export function registerAiTools(
|
|
|
124
120
|
validate = validateDoc,
|
|
125
121
|
saveFile,
|
|
126
122
|
renderCheck,
|
|
127
|
-
openDocument,
|
|
128
123
|
projectStyle,
|
|
129
124
|
getProjectStyle,
|
|
130
125
|
findOpenTab,
|
|
@@ -134,7 +129,6 @@ export function registerAiTools(
|
|
|
134
129
|
validate?: (doc: unknown) => Promise<string[]>;
|
|
135
130
|
saveFile?: (relPath: string, content: string) => Promise<void>;
|
|
136
131
|
renderCheck?: (doc: unknown) => Promise<{ ok: true } | { ok: false; error: string }>;
|
|
137
|
-
openDocument?: (path: string) => Promise<void>;
|
|
138
132
|
projectStyle?: Record<string, string> | undefined;
|
|
139
133
|
/**
|
|
140
134
|
* Live variant of `projectStyle` — takes precedence; re-read per tool call so a project
|
|
@@ -776,67 +770,12 @@ export function registerAiTools(
|
|
|
776
770
|
}),
|
|
777
771
|
);
|
|
778
772
|
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
"document. Use this to iteratively refine pages or components after creating them " +
|
|
788
|
-
"with create_page or create_component.",
|
|
789
|
-
parameters: {
|
|
790
|
-
type: "object",
|
|
791
|
-
properties: {
|
|
792
|
-
path: {
|
|
793
|
-
type: "string",
|
|
794
|
-
description:
|
|
795
|
-
'File path relative to the project root, e.g. "pages/about.json" or ' +
|
|
796
|
-
'"components/nav-bar.json". Must be an existing file.',
|
|
797
|
-
},
|
|
798
|
-
},
|
|
799
|
-
required: ["path"],
|
|
800
|
-
},
|
|
801
|
-
async execute(args) {
|
|
802
|
-
if (!openDocument) {
|
|
803
|
-
return {
|
|
804
|
-
success: false,
|
|
805
|
-
error: "File navigation is not available in this environment.",
|
|
806
|
-
};
|
|
807
|
-
}
|
|
808
|
-
const { path: relPath } = args as { path: string };
|
|
809
|
-
try {
|
|
810
|
-
await openDocument(relPath);
|
|
811
|
-
const tab = getTab();
|
|
812
|
-
if (!tab) {
|
|
813
|
-
return {
|
|
814
|
-
success: false,
|
|
815
|
-
error: `File "${relPath}" could not be opened — no active tab after navigation.`,
|
|
816
|
-
};
|
|
817
|
-
}
|
|
818
|
-
/*
|
|
819
|
-
* The agent loop opens a single undo batch on the tab that was active at loop start
|
|
820
|
-
* (tool-executor.js → beginBatch). Switching the active document mid-loop would strand
|
|
821
|
-
* the new tab's edits with no history snapshot — undo would have nothing to roll back.
|
|
822
|
-
* Flush the previous tab's batch and re-open one on the newly-active tab so edits in
|
|
823
|
-
* each document remain individually undoable.
|
|
824
|
-
*/
|
|
825
|
-
if (isBatching()) {
|
|
826
|
-
endBatch();
|
|
827
|
-
beginBatch(tab);
|
|
828
|
-
}
|
|
829
|
-
return {
|
|
830
|
-
success: true,
|
|
831
|
-
summary: `Switched to "${relPath}". All tools now operate on this document.`,
|
|
832
|
-
};
|
|
833
|
-
} catch (error) {
|
|
834
|
-
return {
|
|
835
|
-
success: false,
|
|
836
|
-
error: `Failed to open "${relPath}": ${error instanceof Error ? error.message : String(error)}`,
|
|
837
|
-
};
|
|
838
|
-
}
|
|
839
|
-
},
|
|
840
|
-
}),
|
|
841
|
-
);
|
|
773
|
+
/*
|
|
774
|
+
* `open_document` is NOT here any more. It is the projection of the `document.open` record
|
|
775
|
+
* (`workspace/workspace.ts`'s `tabCommands`), so the person's palette row and the model's tool
|
|
776
|
+
* are one `run` and one gate (§12.4). The hand tool read `getTab()` after the open and called
|
|
777
|
+
* whatever it found a success — a missing file left the previous document active and reported
|
|
778
|
+
* "Switched to" — and it re-anchored the undo batch itself, which `tool-executor.ts` has done
|
|
779
|
+
* after EVERY tool since project adoption started replacing tabs mid-loop.
|
|
780
|
+
*/
|
|
842
781
|
}
|
|
@@ -40,7 +40,7 @@ import { preferredModel } from "./ai-models";
|
|
|
40
40
|
import { pruneOrphanToolMessages, trimContext } from "./context-manager";
|
|
41
41
|
import { renderCheck } from "./render-critic";
|
|
42
42
|
import { validateDoc } from "./jx-validate";
|
|
43
|
-
import {
|
|
43
|
+
import { reloadFileInTab } from "../files/files";
|
|
44
44
|
import { getExtensionCatalog, refreshExtensionUi } from "../format/format-host";
|
|
45
45
|
import * as sessionStore from "./ai-session-store";
|
|
46
46
|
|
|
@@ -98,7 +98,6 @@ export function createDocumentAssistant() {
|
|
|
98
98
|
renderCheck: renderCheck as (
|
|
99
99
|
doc: unknown,
|
|
100
100
|
) => Promise<{ ok: true } | { ok: false; error: string }>,
|
|
101
|
-
openDocument: openFileInTab,
|
|
102
101
|
getProjectStyle,
|
|
103
102
|
findOpenTab,
|
|
104
103
|
reloadTab: reloadFileInTab,
|
|
@@ -130,12 +129,15 @@ export function createDocumentAssistant() {
|
|
|
130
129
|
sessionStore.moveSession("", root, sessionId);
|
|
131
130
|
}
|
|
132
131
|
},
|
|
133
|
-
onProjectConfigWritten: async (config: ProjectConfig) => {
|
|
132
|
+
onProjectConfigWritten: async (config: ProjectConfig, text: string) => {
|
|
134
133
|
/* Into the configuration document, not beside it. This used to assign a fresh object to
|
|
135
134
|
`projectState.projectConfig`, leaving the document holding the previous configuration —
|
|
136
135
|
and the next settings commit wrote that stale document back over the assistant's file.
|
|
137
|
-
`adoptProjectConfig` is the one door for a config that reached disk another way.
|
|
138
|
-
|
|
136
|
+
`adoptProjectConfig` is the one door for a config that reached disk another way. The text
|
|
137
|
+
goes with it: the document derives the file's layout record from the bytes the tool wrote,
|
|
138
|
+
so the next settings commit is a one-line diff on the file as the model laid it out, not a
|
|
139
|
+
re-layout of it with the record of the file it read before (issue 331). */
|
|
140
|
+
await adoptProjectConfig(config, text);
|
|
139
141
|
/* An `extensions` edit changes what the generated entry documents compose, and the backends
|
|
140
142
|
regenerate them from project.json on demand — so without this the editor and the assistant
|
|
141
143
|
both keep judging by the PREVIOUS project's schemas until the window reopens. */
|
|
@@ -157,7 +159,8 @@ export function createDocumentAssistant() {
|
|
|
157
159
|
document: "an open document (use open_document first)",
|
|
158
160
|
"document-tree":
|
|
159
161
|
"an open document whose element tree the canvas is editing — Project Settings and the " +
|
|
160
|
-
"grid editors are documents, but not trees to restructure"
|
|
162
|
+
"grid editors are documents, but not trees to restructure, and a page left in Code view " +
|
|
163
|
+
"returns to the canvas with set_canvas_mode",
|
|
161
164
|
} as const;
|
|
162
165
|
|
|
163
166
|
/**
|
package/src/studio.ts
CHANGED
|
@@ -160,7 +160,7 @@ import {
|
|
|
160
160
|
mountSignalsPanel,
|
|
161
161
|
registerSignalsCommands,
|
|
162
162
|
} from "./panels/signals-panel";
|
|
163
|
-
import { loadComponentRegistry } from "./files/components";
|
|
163
|
+
import { loadComponentRegistry, noteComponentSaved } from "./files/components";
|
|
164
164
|
import { ensureDependenciesInstalled } from "./packages/ensure-deps";
|
|
165
165
|
import { maybePromptJxsuiteUpdate } from "./packages/jxsuite-update";
|
|
166
166
|
import { autoSyncProjectOnOpen } from "./packages/pull-package-sync";
|
|
@@ -234,7 +234,11 @@ import {
|
|
|
234
234
|
platformUsesRepoPicker,
|
|
235
235
|
} from "./new-project/add-repo-modal";
|
|
236
236
|
import { openNewProjectModal, registerNewProjectCommands } from "./new-project/new-project-modal";
|
|
237
|
-
import {
|
|
237
|
+
import {
|
|
238
|
+
invalidatePageRouteCache,
|
|
239
|
+
registerInspectorCommands,
|
|
240
|
+
renderPropertiesPanel,
|
|
241
|
+
} from "./panels/properties-panel";
|
|
238
242
|
import { liveElementCommands, setContextMenuNavigate } from "./editor/context-menu";
|
|
239
243
|
import { registerSeoCommands, renderSeoModal } from "./panels/seo-modal";
|
|
240
244
|
import { ensurePopoverRevealWatch, setOpenPopover } from "./canvas/popover-state";
|
|
@@ -1497,9 +1501,17 @@ const liveChrome = createLiveSurfaceSaver({
|
|
|
1497
1501
|
redefineOnCanvases: redefineElementOnCanvases,
|
|
1498
1502
|
workspace,
|
|
1499
1503
|
});
|
|
1504
|
+
/* The third is the component registry: a saved definition rewrites its own entry, so an instance's
|
|
1505
|
+
Component Settings and the Library read the props the file has now rather than the ones the scan
|
|
1506
|
+
saw when the project opened. The Content tab and the Library are told, because neither reads the
|
|
1507
|
+
registry reactively. */
|
|
1500
1508
|
setDocumentSavedListener((path, doc) => {
|
|
1501
1509
|
void noteFileSaved(path);
|
|
1502
1510
|
void liveChrome(path, doc as unknown as JxDocument);
|
|
1511
|
+
if (noteComponentSaved(path, doc)) {
|
|
1512
|
+
renderPropertiesPanel();
|
|
1513
|
+
renderLeftPanel();
|
|
1514
|
+
}
|
|
1503
1515
|
});
|
|
1504
1516
|
/* Walking a comparison. No deps: the stepper reads the pane-keyed diff store directly and the
|
|
1505
1517
|
toolbar redraws itself, so there is nothing for the bootstrap to inject. */
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
"gap": "2px",
|
|
79
79
|
"flexShrink": "0"
|
|
80
80
|
},
|
|
81
|
-
"& [part=\"layout\"]
|
|
81
|
+
"& [part=\"layout\"]": {
|
|
82
82
|
"padding": "2px var(--jx-space-3)",
|
|
83
83
|
"border": "none",
|
|
84
84
|
"borderRadius": "var(--jx-radius-sm)",
|
|
@@ -101,9 +101,6 @@
|
|
|
101
101
|
"color": "var(--jx-fg)",
|
|
102
102
|
"background": "var(--jx-hover-bg)"
|
|
103
103
|
},
|
|
104
|
-
"& [part=\"layout-add\"]": {
|
|
105
|
-
"padding": "2px var(--jx-space-2)"
|
|
106
|
-
},
|
|
107
104
|
"& [part=\"center\"]": {
|
|
108
105
|
"display": "flex",
|
|
109
106
|
"alignItems": "center",
|
|
@@ -479,14 +476,16 @@
|
|
|
479
476
|
}
|
|
480
477
|
},
|
|
481
478
|
{
|
|
482
|
-
"tagName": "button",
|
|
479
|
+
"tagName": "jx-action-button",
|
|
480
|
+
"$props": {
|
|
481
|
+
"label": "Save layout",
|
|
482
|
+
"hint": "Save the current arrangement as a layout",
|
|
483
|
+
"icon": "plus",
|
|
484
|
+
"size": "sm"
|
|
485
|
+
},
|
|
483
486
|
"attributes": {
|
|
484
|
-
"part": "layout-add"
|
|
485
|
-
"type": "button",
|
|
486
|
-
"title": "Save the current arrangement as a layout",
|
|
487
|
-
"aria-label": "Save layout"
|
|
487
|
+
"part": "layout-add"
|
|
488
488
|
},
|
|
489
|
-
"textContent": "+",
|
|
490
489
|
"onclick": {
|
|
491
490
|
"$prototype": "Function",
|
|
492
491
|
"body": [
|
|
@@ -657,6 +656,9 @@
|
|
|
657
656
|
"label": {
|
|
658
657
|
"$ref": "$map/item/title"
|
|
659
658
|
},
|
|
659
|
+
"hint": {
|
|
660
|
+
"$ref": "$map/item/tooltip"
|
|
661
|
+
},
|
|
660
662
|
"disabled": {
|
|
661
663
|
"$ref": "$map/item/disabled"
|
|
662
664
|
},
|
|
@@ -664,8 +666,7 @@
|
|
|
664
666
|
"quiet": true
|
|
665
667
|
},
|
|
666
668
|
"attributes": {
|
|
667
|
-
"data-command-id": "${$map.item.id}"
|
|
668
|
-
"title": "${$map.item.tooltip}"
|
|
669
|
+
"data-command-id": "${$map.item.id}"
|
|
669
670
|
},
|
|
670
671
|
"onclick": {
|
|
671
672
|
"$prototype": "Function",
|
|
@@ -43,6 +43,7 @@ import { armPreviewOverlay, flushPreviewOverlay } from "../preview/preview-overl
|
|
|
43
43
|
import { documentUrlPattern, dynamicRouteParams } from "../page-params";
|
|
44
44
|
import { getNodeAtPath, nodeLabel, projectState } from "../store";
|
|
45
45
|
import { activeRegistry } from "../commands/active-registry";
|
|
46
|
+
import { runActiveReported, runReported } from "../commands/run-reported";
|
|
46
47
|
import { notify } from "../services/notify";
|
|
47
48
|
import { mountSurface, registerSurface } from "../ui/surface";
|
|
48
49
|
import { rectOf } from "../utils/geometry";
|
|
@@ -521,7 +522,7 @@ export async function saveLayoutPrompt(registry: CommandRegistry): Promise<void>
|
|
|
521
522
|
placeholder: "Layout name",
|
|
522
523
|
});
|
|
523
524
|
if (name) {
|
|
524
|
-
await registry
|
|
525
|
+
await runReported(registry, "view.saveLayout", { name }, "Command Bar");
|
|
525
526
|
}
|
|
526
527
|
}
|
|
527
528
|
|
|
@@ -533,7 +534,7 @@ export async function renameLayoutPrompt(
|
|
|
533
534
|
): Promise<void> {
|
|
534
535
|
const name = await showPromptDialog("Rename layout", { confirmLabel: "Rename", value: current });
|
|
535
536
|
if (name) {
|
|
536
|
-
await registry
|
|
537
|
+
await runReported(registry, "view.renameLayout", { layout, name }, "Command Bar");
|
|
537
538
|
}
|
|
538
539
|
}
|
|
539
540
|
|
|
@@ -707,7 +708,7 @@ function openStudioMenu(opener: HTMLElement): void {
|
|
|
707
708
|
rows: overflowRows(registry),
|
|
708
709
|
run: (id) => {
|
|
709
710
|
if (registry.isEnabled(id)) {
|
|
710
|
-
void registry
|
|
711
|
+
void runReported(registry, id, undefined, "Command Bar");
|
|
711
712
|
}
|
|
712
713
|
},
|
|
713
714
|
});
|
|
@@ -751,7 +752,7 @@ function state(): CommandBarScope {
|
|
|
751
752
|
run: (id) => {
|
|
752
753
|
const registry = activeRegistry();
|
|
753
754
|
if (registry?.isEnabled(id)) {
|
|
754
|
-
void registry
|
|
755
|
+
void runReported(registry, id, undefined, "Command Bar");
|
|
755
756
|
}
|
|
756
757
|
},
|
|
757
758
|
saveLayout: () => {
|
|
@@ -762,7 +763,7 @@ function state(): CommandBarScope {
|
|
|
762
763
|
},
|
|
763
764
|
segments: [],
|
|
764
765
|
setLayout: (id) => {
|
|
765
|
-
void
|
|
766
|
+
void runActiveReported("view.setLayout", { layout: id }, "Command Bar");
|
|
766
767
|
},
|
|
767
768
|
windowControl: (action) => {
|
|
768
769
|
windowControls()?.[action]();
|