@jxsuite/studio 0.37.1 → 1.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/iframe-entry.js +622 -49
- package/dist/iframe-entry.js.map +15 -12
- package/dist/studio.css +1333 -0
- package/dist/studio.js +103709 -81001
- package/dist/studio.js.map +201 -157
- package/package.json +47 -45
- package/src/account-status.ts +39 -0
- package/src/browse/browse.ts +10 -14
- package/src/canvas/canvas-live-render.ts +21 -2
- package/src/canvas/canvas-render.ts +35 -16
- package/src/canvas/canvas-utils.ts +118 -72
- package/src/canvas/iframe-entry.ts +20 -0
- package/src/canvas/iframe-eval.ts +155 -0
- package/src/canvas/iframe-host.ts +132 -6
- package/src/canvas/iframe-inline-edit.ts +107 -1
- package/src/canvas/iframe-protocol.ts +43 -3
- package/src/canvas/iframe-render.ts +18 -0
- package/src/collab/collab-session.ts +23 -8
- package/src/collab/collab-state.ts +6 -3
- package/src/component-props.ts +104 -0
- package/src/editor/context-menu.ts +1 -1
- package/src/editor/inline-edit-apply.ts +36 -1
- package/src/editor/inline-edit.ts +58 -1
- package/src/editor/repeater-scope.ts +8 -13
- package/src/editor/shortcuts.ts +41 -12
- package/src/files/file-ops.ts +14 -0
- package/src/files/files.ts +56 -1
- package/src/format/format-host.ts +63 -1
- package/src/grid/cell-editors.ts +288 -0
- package/src/grid/cell-popovers.ts +108 -0
- package/src/grid/csv-codec.ts +122 -0
- package/src/grid/edit-buffer.ts +490 -0
- package/src/grid/grid-controller.ts +417 -0
- package/src/grid/grid-layout.ts +83 -0
- package/src/grid/grid-open.ts +167 -0
- package/src/grid/grid-panel.ts +302 -0
- package/src/grid/grid-source.ts +210 -0
- package/src/grid/grid-view.ts +367 -0
- package/src/grid/schema-columns.ts +261 -0
- package/src/grid/sources/connector-source.ts +217 -0
- package/src/grid/sources/content-source.ts +542 -0
- package/src/grid/sources/csv-file-source.ts +247 -0
- package/src/grid/tabulator-tables.d.ts +120 -0
- package/src/new-project/add-repo-modal.ts +183 -0
- package/src/new-project/new-project-modal.ts +22 -3
- package/src/page-params.ts +34 -8
- package/src/panels/ai-chat/chat-markdown.ts +2 -2
- package/src/panels/ai-panel.ts +61 -4
- package/src/panels/block-action-bar.ts +8 -3
- package/src/panels/chat-panel.ts +98 -0
- package/src/panels/data-grid.ts +241 -0
- package/src/panels/editors.ts +43 -17
- package/src/panels/events-panel.ts +137 -44
- package/src/panels/formula-workspace.ts +379 -0
- package/src/panels/frontmatter-fields.ts +231 -0
- package/src/panels/frontmatter-panel.ts +132 -0
- package/src/panels/git-panel.ts +1 -1
- package/src/panels/head-panel.ts +11 -175
- package/src/panels/properties-panel.ts +154 -144
- package/src/panels/right-panel.ts +9 -47
- package/src/panels/signals-panel.ts +217 -460
- package/src/panels/statement-editor.ts +710 -0
- package/src/panels/statusbar.ts +1 -1
- package/src/panels/style-panel.ts +25 -1
- package/src/panels/stylebook-panel.ts +0 -2
- package/src/panels/tab-bar.ts +175 -6
- package/src/panels/tab-strip.ts +62 -6
- package/src/panels/toolbar.ts +37 -3
- package/src/panels/welcome-screen.ts +50 -0
- package/src/platform-errors.ts +30 -0
- package/src/platforms/cloud.ts +172 -89
- package/src/platforms/devserver.ts +172 -0
- package/src/services/ai-project-tools.ts +541 -0
- package/src/services/ai-session-store.ts +28 -0
- package/src/services/ai-system-prompt.ts +194 -24
- package/src/services/ai-tools.ts +88 -21
- package/src/services/automation.ts +16 -0
- package/src/services/context-resolver.ts +73 -0
- package/src/services/data-service.ts +155 -0
- package/src/services/document-assistant.ts +81 -11
- package/src/services/gated-registry.ts +72 -0
- package/src/services/live-preview.ts +0 -0
- package/src/services/monaco-setup.ts +75 -26
- package/src/services/preview-eval.ts +68 -0
- package/src/services/project-adoption.ts +31 -0
- package/src/settings/contributed-section.ts +406 -0
- package/src/settings/extension-sections.ts +145 -0
- package/src/settings/schema-field-ui.ts +4 -2
- package/src/settings/settings-modal.ts +101 -42
- package/src/site-context.ts +12 -1
- package/src/store.ts +4 -0
- package/src/studio.ts +107 -52
- package/src/tabs/patch-ops.ts +25 -0
- package/src/tabs/tab.ts +39 -1
- package/src/tabs/transact.ts +61 -14
- package/src/types.ts +132 -1
- package/src/ui/dynamic-slot.ts +272 -0
- package/src/ui/expression-editor.ts +423 -125
- package/src/ui/field-row.ts +5 -0
- package/src/ui/form-controls.ts +322 -0
- package/src/ui/formula-catalog.ts +557 -0
- package/src/ui/formula-chips.ts +216 -0
- package/src/ui/formula-palette.ts +211 -0
- package/src/ui/layers.ts +40 -0
- package/src/ui/media-picker.ts +1 -1
- package/src/ui/panel-resize.ts +15 -1
- package/src/ui/progress-modal.ts +2 -2
- package/src/ui/schema-form.ts +524 -0
- package/src/utils/preview-format.ts +26 -0
- package/src/utils/studio-utils.ts +4 -3
- package/src/view.ts +4 -4
- package/src/workspace/workspace.ts +14 -0
- package/src/settings/content-types-editor.ts +0 -599
|
@@ -22,6 +22,149 @@ interface BuildSystemPromptOptions {
|
|
|
22
22
|
components?: ComponentEntry[] | undefined;
|
|
23
23
|
/** Project root path. */
|
|
24
24
|
projectRoot?: string | undefined;
|
|
25
|
+
/** Whether a project is open. Defaults to `!!projectRoot` (kept explicit for tests). */
|
|
26
|
+
hasProject?: boolean | undefined;
|
|
27
|
+
/** Project-relative file paths for the inventory section (project modes; capped). */
|
|
28
|
+
fileInventory?: string[] | undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Max file paths embedded in the prompt's inventory section. */
|
|
32
|
+
const FILE_INVENTORY_CAP = 100;
|
|
33
|
+
|
|
34
|
+
// ─── Tool tiers (single source of truth for prompt AND gating) ──────────────
|
|
35
|
+
|
|
36
|
+
/** Studio state a tool tier requires. Used by the gating registry AND the prompt tool list. */
|
|
37
|
+
export type AiToolTier = "no-project" | "project" | "document";
|
|
38
|
+
|
|
39
|
+
export interface AiToolInfo {
|
|
40
|
+
name: string;
|
|
41
|
+
tier: AiToolTier;
|
|
42
|
+
/** One-line signature + purpose shown in the system prompt's tool list. */
|
|
43
|
+
blurb: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Every assistant tool with its availability tier and prompt blurb. document-assistant.ts derives
|
|
48
|
+
* the gating predicates from the same rows, so the advertised tool list and the executable tool
|
|
49
|
+
* list cannot drift (a test asserts the names match the registered tools).
|
|
50
|
+
*/
|
|
51
|
+
export const AI_TOOL_TIERS: AiToolInfo[] = [
|
|
52
|
+
// Bootstrap (no project open)
|
|
53
|
+
{
|
|
54
|
+
name: "create_project",
|
|
55
|
+
tier: "no-project",
|
|
56
|
+
blurb:
|
|
57
|
+
"create_project(name, description?, template?, directory?, design?) — scaffold a new Jx project (project.json, conventional directories, starter pages) and open it in the studio.",
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: "list_starters",
|
|
61
|
+
tier: "no-project",
|
|
62
|
+
blurb: "list_starters() — list the starter templates available for new projects.",
|
|
63
|
+
},
|
|
64
|
+
// Cross-file (project open, document optional)
|
|
65
|
+
{
|
|
66
|
+
name: "list_files",
|
|
67
|
+
tier: "project",
|
|
68
|
+
blurb: "list_files(dir?) — list the project's files recursively (build folders excluded).",
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: "read_file",
|
|
72
|
+
tier: "project",
|
|
73
|
+
blurb: "read_file(path) — read any project file (Jx documents, markdown, CSS, data).",
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: "write_file",
|
|
77
|
+
tier: "project",
|
|
78
|
+
blurb:
|
|
79
|
+
"write_file(path, content) — create or overwrite a project file. Jx documents are schema-validated and render-checked BEFORE writing; refused while the file is open with unsaved changes. Not undoable.",
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: "search_files",
|
|
83
|
+
tier: "project",
|
|
84
|
+
blurb: "search_files(query, extensions?) — find files by file NAME (not content).",
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: "create_component",
|
|
88
|
+
tier: "project",
|
|
89
|
+
blurb: "create_component(path, content) — create a new .json component file on disk.",
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: "create_page",
|
|
93
|
+
tier: "project",
|
|
94
|
+
blurb: "create_page(path, content) — create a new .json page file on disk.",
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: "open_document",
|
|
98
|
+
tier: "project",
|
|
99
|
+
blurb:
|
|
100
|
+
"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.",
|
|
101
|
+
},
|
|
102
|
+
// Document (an active document on the canvas)
|
|
103
|
+
{
|
|
104
|
+
name: "read_document",
|
|
105
|
+
tier: "document",
|
|
106
|
+
blurb:
|
|
107
|
+
'read_document(path?) — inspect the whole document or the subtree at a path. Paths are JSON arrays of keys/indices from the root, e.g. ["children", 0, "children", 1].',
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: "set_property",
|
|
111
|
+
tier: "document",
|
|
112
|
+
blurb:
|
|
113
|
+
"set_property(path, key, value) — set or remove a property on the node at path (tagName, textContent, className, style, attributes, $props…). Pass value: null to remove.",
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: "set_style",
|
|
117
|
+
tier: "document",
|
|
118
|
+
blurb:
|
|
119
|
+
'set_style(path, property, value) — set or remove a CSS style property (camelCase) on a node. Values as strings: "10px", "var(--color-accent)". Pass value: null to remove.',
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: "set_text",
|
|
123
|
+
tier: "document",
|
|
124
|
+
blurb: 'set_text(path, value) — convenient alias for set_property with key: "textContent".',
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: "add_child",
|
|
128
|
+
tier: "document",
|
|
129
|
+
blurb:
|
|
130
|
+
"add_child(parentPath, index, node) — insert a new node into the children of parentPath at index.",
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: "remove_node",
|
|
134
|
+
tier: "document",
|
|
135
|
+
blurb: "remove_node(path) — remove the node at path.",
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: "move_node",
|
|
139
|
+
tier: "document",
|
|
140
|
+
blurb: "move_node(fromPath, toParentPath, toIndex) — move a node from one location to another.",
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
name: "add_state",
|
|
144
|
+
tier: "document",
|
|
145
|
+
blurb:
|
|
146
|
+
"add_state(key, value) — add a reactive state variable under the document's 'state' object. Value can be scalar, typed, computed, function, or data source.",
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: "update_state",
|
|
150
|
+
tier: "document",
|
|
151
|
+
blurb: "update_state(key, value) — update or remove (value: null) an existing state variable.",
|
|
152
|
+
},
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Whether a tier's tools are active for the given studio state. Shared semantics with the gating
|
|
157
|
+
* registry: bootstrap tools vanish once a project opens; project tools need a project; document
|
|
158
|
+
* tools need an active document (even in single-file mode without a project).
|
|
159
|
+
*/
|
|
160
|
+
export function tierActive(tier: AiToolTier, hasProject: boolean, hasDocument: boolean): boolean {
|
|
161
|
+
if (tier === "no-project") {
|
|
162
|
+
return !hasProject;
|
|
163
|
+
}
|
|
164
|
+
if (tier === "project") {
|
|
165
|
+
return hasProject;
|
|
166
|
+
}
|
|
167
|
+
return hasDocument;
|
|
25
168
|
}
|
|
26
169
|
|
|
27
170
|
// ─── Jx Schema Reference (condensed) ────────────────────────────────────────
|
|
@@ -381,41 +524,58 @@ export function buildSystemPrompt({
|
|
|
381
524
|
projectConfig,
|
|
382
525
|
components,
|
|
383
526
|
projectRoot,
|
|
527
|
+
hasProject = Boolean(projectRoot),
|
|
528
|
+
fileInventory,
|
|
384
529
|
}: BuildSystemPromptOptions = {}) {
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
530
|
+
const hasDocument = Boolean(document);
|
|
531
|
+
|
|
532
|
+
// 1. Role, state-appropriate workflow, and the tool list for the current state.
|
|
533
|
+
const toolList = AI_TOOL_TIERS.filter((t) => tierActive(t.tier, hasProject, hasDocument))
|
|
534
|
+
.map((t) => `- ${t.blurb}`)
|
|
535
|
+
.join("\n");
|
|
536
|
+
|
|
537
|
+
const role = `You are an expert Jx builder assistant embedded in Jx Studio. You help users build websites, components, pages, and layouts using the Jx JSON schema. The live jxsuite.com marketing site is built entirely with Jx — you can produce production-quality Jx code.`;
|
|
538
|
+
|
|
539
|
+
let workflow: string;
|
|
540
|
+
if (!hasProject && !hasDocument) {
|
|
541
|
+
workflow = `No project is open yet. Your first job is to bootstrap one:
|
|
542
|
+
1. Gather what the user wants (site type, name, look) — ask briefly only if essential details are missing.
|
|
543
|
+
2. Call create_project with a fitting name, template, and design quickstart (colors/fonts) derived from the request.
|
|
544
|
+
3. After it succeeds the studio opens the project, and the file and document tools become available in the next round — continue building pages, components, and layouts with them without waiting to be asked.
|
|
545
|
+
|
|
546
|
+
Tools available right now:
|
|
547
|
+
${toolList}`;
|
|
548
|
+
} else if (!hasDocument) {
|
|
549
|
+
workflow = `A project is open, but no document is on the canvas. Work across the project's files directly — you do not need to open documents to develop:
|
|
550
|
+
1. Discover with list_files / search_files, inspect with read_file.
|
|
551
|
+
2. Create or modify files with write_file / create_page / create_component. Jx documents are validated before writing; fix reported errors and retry.
|
|
552
|
+
3. Use open_document only when the user should see a page on the canvas, or when you want the finer-grained document tools for iterative edits.
|
|
553
|
+
|
|
554
|
+
Tools available right now:
|
|
555
|
+
${toolList}`;
|
|
556
|
+
} else {
|
|
557
|
+
workflow = `You have tools that read and modify the live Jx document directly, plus project-wide file tools. Always prefer tool calls over describing changes in text:
|
|
558
|
+
${toolList}
|
|
402
559
|
|
|
403
560
|
When the user asks you to build or modify something:
|
|
404
561
|
1. Call read_document first if needed to discover the current structure and valid paths.
|
|
405
562
|
2. Plan your changes — think about which tools you'll need.
|
|
406
563
|
3. Execute the tools in the right order (e.g., add_child before set_property on the new node).
|
|
407
|
-
4.
|
|
564
|
+
4. For changes spanning OTHER files, use read_file/write_file directly instead of opening each one.
|
|
565
|
+
5. Summarize what you changed clearly.
|
|
408
566
|
|
|
409
|
-
Your edits apply to the live canvas immediately and are individually undoable. After each edit the document is schema-validated: if a tool returns { success: false } reporting schema errors, your change introduced them — issue a follow-up edit to fix them
|
|
567
|
+
Your document edits apply to the live canvas immediately and are individually undoable (file writes are not). After each edit the document is schema-validated: if a tool returns { success: false } reporting schema errors, your change introduced them — issue a follow-up edit to fix them.`;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
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.
|
|
410
571
|
|
|
411
|
-
|
|
572
|
+
Be concise. Don't explain what Jx is unless asked. Just build.`;
|
|
412
573
|
|
|
413
|
-
|
|
414
|
-
];
|
|
574
|
+
const sections = [`${role}\n\n${workflow}\n\n${closing}`];
|
|
415
575
|
|
|
416
576
|
// eslint-disable-next-line unicorn/no-immediate-mutation -- conditional section builder: later sections are pushed only when their context exists
|
|
417
577
|
sections.push(
|
|
418
|
-
// 2. Jx schema reference
|
|
578
|
+
// 2. Jx schema reference — kept in ALL modes (pre-project the model plans starter content)
|
|
419
579
|
JX_SCHEMA_REFERENCE,
|
|
420
580
|
// 3. State shape decision tree
|
|
421
581
|
STATE_SHAPE_DECISION_TREE,
|
|
@@ -436,13 +596,23 @@ Be concise. Don't explain what Jx is unless asked. Just build.`,
|
|
|
436
596
|
}
|
|
437
597
|
|
|
438
598
|
// 6. Project context
|
|
439
|
-
if (projectConfig || components || projectRoot) {
|
|
599
|
+
if (hasProject && (projectConfig || components || projectRoot)) {
|
|
440
600
|
const projectSummary = buildProjectSummary({ projectConfig, components, projectRoot });
|
|
441
601
|
if (projectSummary) {
|
|
442
602
|
sections.push(`## Project Context\n\n${projectSummary}`);
|
|
443
603
|
}
|
|
444
604
|
}
|
|
445
605
|
|
|
606
|
+
// 6a. File inventory — a compact map of the project for cross-file work
|
|
607
|
+
if (hasProject && fileInventory && fileInventory.length > 0) {
|
|
608
|
+
const capped = fileInventory.slice(0, FILE_INVENTORY_CAP);
|
|
609
|
+
const more =
|
|
610
|
+
fileInventory.length > capped.length
|
|
611
|
+
? `\n… and ${fileInventory.length - capped.length} more (use list_files)`
|
|
612
|
+
: "";
|
|
613
|
+
sections.push(`## Project Files\n\n${capped.join("\n")}${more}`);
|
|
614
|
+
}
|
|
615
|
+
|
|
446
616
|
// 7. Error recovery guidance
|
|
447
617
|
sections.push(`## Error Recovery
|
|
448
618
|
|
package/src/services/ai-tools.ts
CHANGED
|
@@ -33,15 +33,26 @@ const PATH_DESCRIPTION =
|
|
|
33
33
|
"Path to a node in the document, as a JSON array of keys/indices from the root " +
|
|
34
34
|
'(e.g. ["children", 0, "children", 1]). Use read_document to discover valid paths.';
|
|
35
35
|
|
|
36
|
+
/** Steering error for document tools called with no active tab. */
|
|
37
|
+
function noDocError(): ToolResult {
|
|
38
|
+
return {
|
|
39
|
+
success: false,
|
|
40
|
+
error:
|
|
41
|
+
"No document is open — use open_document(path) to open one on the canvas, or " +
|
|
42
|
+
"read_file/write_file to work with project files directly.",
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
36
46
|
/**
|
|
37
47
|
* Translate a raw JSON Schema validation error into a Jx-specific actionable message. The LLM needs
|
|
38
|
-
* concrete guidance on HOW to fix errors, not just what rule was violated.
|
|
48
|
+
* concrete guidance on HOW to fix errors, not just what rule was violated. Shared with the
|
|
49
|
+
* project-level file tools (ai-project-tools.ts), which pre-validate Jx documents before writing.
|
|
39
50
|
*
|
|
40
51
|
* @param {string} rawError - Message from ajv (e.g. "/children/0/style: must NOT have additional
|
|
41
52
|
* property")
|
|
42
53
|
* @returns {string}
|
|
43
54
|
*/
|
|
44
|
-
function translateValidationError(rawError: string): string {
|
|
55
|
+
export function translateValidationError(rawError: string): string {
|
|
45
56
|
const lower = rawError.toLowerCase();
|
|
46
57
|
|
|
47
58
|
// Additional property — extract the offending key from the message if present
|
|
@@ -162,6 +173,9 @@ async function applyAndValidate(
|
|
|
162
173
|
* renderCheck?: (doc: unknown) => Promise<{ ok: true } | { ok: false; error: string }>;
|
|
163
174
|
* openDocument?: (path: string) => Promise<void>;
|
|
164
175
|
* projectStyle?: Record<string, string>;
|
|
176
|
+
* getProjectStyle?: () => Record<string, string> | undefined;
|
|
177
|
+
* findOpenTab?: (path: string) => import("../tabs/tab").Tab | null;
|
|
178
|
+
* reloadTab?: (path: string) => Promise<void>;
|
|
165
179
|
* }} ctx
|
|
166
180
|
*/
|
|
167
181
|
export function registerAiTools(
|
|
@@ -173,6 +187,9 @@ export function registerAiTools(
|
|
|
173
187
|
renderCheck,
|
|
174
188
|
openDocument,
|
|
175
189
|
projectStyle,
|
|
190
|
+
getProjectStyle,
|
|
191
|
+
findOpenTab,
|
|
192
|
+
reloadTab,
|
|
176
193
|
}: {
|
|
177
194
|
getTab: () => Tab | null;
|
|
178
195
|
validate?: (doc: unknown) => Promise<string[]>;
|
|
@@ -180,8 +197,44 @@ export function registerAiTools(
|
|
|
180
197
|
renderCheck?: (doc: unknown) => Promise<{ ok: true } | { ok: false; error: string }>;
|
|
181
198
|
openDocument?: (path: string) => Promise<void>;
|
|
182
199
|
projectStyle?: Record<string, string> | undefined;
|
|
200
|
+
/**
|
|
201
|
+
* Live variant of `projectStyle` — takes precedence; re-read per tool call so a project
|
|
202
|
+
* bootstrapped mid-session contributes its design tokens.
|
|
203
|
+
*/
|
|
204
|
+
getProjectStyle?: () => Record<string, string> | undefined;
|
|
205
|
+
/** The open tab whose documentPath equals a project-relative path (write reconciliation). */
|
|
206
|
+
findOpenTab?: (path: string) => Tab | null;
|
|
207
|
+
/** Reload an open tab's document from disk after a file write. */
|
|
208
|
+
reloadTab?: (path: string) => Promise<void>;
|
|
183
209
|
},
|
|
184
210
|
) {
|
|
211
|
+
const styleOf = getProjectStyle ?? (() => projectStyle);
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Guard a disk write against an open tab: refuse while the tab has unsaved changes (the write
|
|
215
|
+
* would silently diverge from the editor). Returns null when the write may proceed.
|
|
216
|
+
*/
|
|
217
|
+
function dirtyTabError(relPath: string): ToolResult | null {
|
|
218
|
+
const tab = findOpenTab?.(relPath);
|
|
219
|
+
if (tab?.doc.dirty) {
|
|
220
|
+
return {
|
|
221
|
+
success: false,
|
|
222
|
+
error:
|
|
223
|
+
`"${relPath}" is open in the editor with unsaved changes. Use open_document plus the ` +
|
|
224
|
+
`document tools to edit it, or ask the user to save or discard their changes first.`,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Refresh an open (clean) tab from disk after a write; annotate the summary accordingly. */
|
|
231
|
+
async function reconcileAfterWrite(relPath: string, summary: string): Promise<string> {
|
|
232
|
+
if (findOpenTab?.(relPath)) {
|
|
233
|
+
await reloadTab?.(relPath);
|
|
234
|
+
return `${summary} The file's open editor tab was refreshed. (Saved to disk; not undoable.)`;
|
|
235
|
+
}
|
|
236
|
+
return `${summary} (Saved to disk; not undoable.)`;
|
|
237
|
+
}
|
|
185
238
|
registry.register(
|
|
186
239
|
createToolDefinition({
|
|
187
240
|
name: "read_document",
|
|
@@ -202,7 +255,7 @@ export function registerAiTools(
|
|
|
202
255
|
execute(args) {
|
|
203
256
|
const tab = getTab();
|
|
204
257
|
if (!tab) {
|
|
205
|
-
return
|
|
258
|
+
return noDocError();
|
|
206
259
|
}
|
|
207
260
|
const { path } = args as { path?: JxPath };
|
|
208
261
|
const node =
|
|
@@ -246,7 +299,7 @@ export function registerAiTools(
|
|
|
246
299
|
async execute(args) {
|
|
247
300
|
const tab = getTab();
|
|
248
301
|
if (!tab) {
|
|
249
|
-
return
|
|
302
|
+
return noDocError();
|
|
250
303
|
}
|
|
251
304
|
const { path, key, value } = args as { path: JxPath; key: string; value?: JxNodeValue };
|
|
252
305
|
const node = getNodeAtPath(tab.doc.document, path);
|
|
@@ -259,7 +312,7 @@ export function registerAiTools(
|
|
|
259
312
|
`Set "${key}" at ${JSON.stringify(path)}.`,
|
|
260
313
|
validate,
|
|
261
314
|
renderCheck,
|
|
262
|
-
|
|
315
|
+
styleOf(),
|
|
263
316
|
);
|
|
264
317
|
},
|
|
265
318
|
}),
|
|
@@ -292,7 +345,7 @@ export function registerAiTools(
|
|
|
292
345
|
async execute(args) {
|
|
293
346
|
const tab = getTab();
|
|
294
347
|
if (!tab) {
|
|
295
|
-
return
|
|
348
|
+
return noDocError();
|
|
296
349
|
}
|
|
297
350
|
const {
|
|
298
351
|
parentPath,
|
|
@@ -337,7 +390,7 @@ export function registerAiTools(
|
|
|
337
390
|
`Inserted node at ${JSON.stringify([...parentPath, "children", index])}.`,
|
|
338
391
|
validate,
|
|
339
392
|
renderCheck,
|
|
340
|
-
|
|
393
|
+
styleOf(),
|
|
341
394
|
);
|
|
342
395
|
},
|
|
343
396
|
}),
|
|
@@ -378,7 +431,7 @@ export function registerAiTools(
|
|
|
378
431
|
async execute(args) {
|
|
379
432
|
const tab = getTab();
|
|
380
433
|
if (!tab) {
|
|
381
|
-
return
|
|
434
|
+
return noDocError();
|
|
382
435
|
}
|
|
383
436
|
const { path, property, value } = args as {
|
|
384
437
|
path: JxPath;
|
|
@@ -396,7 +449,7 @@ export function registerAiTools(
|
|
|
396
449
|
`Set style "${prop}" at ${JSON.stringify(path)}.`,
|
|
397
450
|
validate,
|
|
398
451
|
renderCheck,
|
|
399
|
-
|
|
452
|
+
styleOf(),
|
|
400
453
|
);
|
|
401
454
|
},
|
|
402
455
|
}),
|
|
@@ -424,7 +477,7 @@ export function registerAiTools(
|
|
|
424
477
|
async execute(args) {
|
|
425
478
|
const tab = getTab();
|
|
426
479
|
if (!tab) {
|
|
427
|
-
return
|
|
480
|
+
return noDocError();
|
|
428
481
|
}
|
|
429
482
|
const { path, value } = args as { path: JxPath; value: string };
|
|
430
483
|
if (getNodeAtPath(tab.doc.document, path) === undefined) {
|
|
@@ -440,7 +493,7 @@ export function registerAiTools(
|
|
|
440
493
|
`Set text at ${JSON.stringify(path)}.`,
|
|
441
494
|
validate,
|
|
442
495
|
renderCheck,
|
|
443
|
-
|
|
496
|
+
styleOf(),
|
|
444
497
|
);
|
|
445
498
|
},
|
|
446
499
|
}),
|
|
@@ -470,7 +523,7 @@ export function registerAiTools(
|
|
|
470
523
|
async execute(args) {
|
|
471
524
|
const tab = getTab();
|
|
472
525
|
if (!tab) {
|
|
473
|
-
return
|
|
526
|
+
return noDocError();
|
|
474
527
|
}
|
|
475
528
|
const { key, value } = args as { key: string; value: JxStateDefinition };
|
|
476
529
|
if (tab.doc.document.state && tab.doc.document.state[key] !== undefined) {
|
|
@@ -495,7 +548,7 @@ export function registerAiTools(
|
|
|
495
548
|
`Added state "${key}".`,
|
|
496
549
|
validate,
|
|
497
550
|
renderCheck,
|
|
498
|
-
|
|
551
|
+
styleOf(),
|
|
499
552
|
);
|
|
500
553
|
},
|
|
501
554
|
}),
|
|
@@ -526,7 +579,7 @@ export function registerAiTools(
|
|
|
526
579
|
async execute(args) {
|
|
527
580
|
const tab = getTab();
|
|
528
581
|
if (!tab) {
|
|
529
|
-
return
|
|
582
|
+
return noDocError();
|
|
530
583
|
}
|
|
531
584
|
const { key, value } = args as { key: string; value?: JxStateDefinition | null };
|
|
532
585
|
if (!tab.doc.document.state || tab.doc.document.state[key] === undefined) {
|
|
@@ -555,7 +608,7 @@ export function registerAiTools(
|
|
|
555
608
|
value == null ? `Removed state "${key}".` : `Updated state "${key}".`,
|
|
556
609
|
validate,
|
|
557
610
|
renderCheck,
|
|
558
|
-
|
|
611
|
+
styleOf(),
|
|
559
612
|
);
|
|
560
613
|
},
|
|
561
614
|
}),
|
|
@@ -592,7 +645,7 @@ export function registerAiTools(
|
|
|
592
645
|
async execute(args) {
|
|
593
646
|
const tab = getTab();
|
|
594
647
|
if (!tab) {
|
|
595
|
-
return
|
|
648
|
+
return noDocError();
|
|
596
649
|
}
|
|
597
650
|
const { fromPath, toParentPath, toIndex } = args as {
|
|
598
651
|
fromPath: JxPath;
|
|
@@ -620,7 +673,7 @@ export function registerAiTools(
|
|
|
620
673
|
`Moved node from ${JSON.stringify(fromPath)} to ${JSON.stringify([...toParentPath, "children", toIndex])}.`,
|
|
621
674
|
validate,
|
|
622
675
|
renderCheck,
|
|
623
|
-
|
|
676
|
+
styleOf(),
|
|
624
677
|
);
|
|
625
678
|
},
|
|
626
679
|
}),
|
|
@@ -658,6 +711,10 @@ export function registerAiTools(
|
|
|
658
711
|
};
|
|
659
712
|
}
|
|
660
713
|
const { path: relPath, content } = args as { path: string; content: object };
|
|
714
|
+
const dirtyError = dirtyTabError(relPath);
|
|
715
|
+
if (dirtyError) {
|
|
716
|
+
return dirtyError;
|
|
717
|
+
}
|
|
661
718
|
const errors = await validate(content);
|
|
662
719
|
if (errors.length > 0) {
|
|
663
720
|
const formatted = errors.map((e) => `- ${translateValidationError(e)}`).join("\n");
|
|
@@ -677,7 +734,10 @@ export function registerAiTools(
|
|
|
677
734
|
}
|
|
678
735
|
try {
|
|
679
736
|
await saveFile(relPath, JSON.stringify(content, null, 2));
|
|
680
|
-
return {
|
|
737
|
+
return {
|
|
738
|
+
success: true,
|
|
739
|
+
summary: await reconcileAfterWrite(relPath, `Created component at "${relPath}".`),
|
|
740
|
+
};
|
|
681
741
|
} catch (error) {
|
|
682
742
|
return {
|
|
683
743
|
success: false,
|
|
@@ -719,6 +779,10 @@ export function registerAiTools(
|
|
|
719
779
|
};
|
|
720
780
|
}
|
|
721
781
|
const { path: relPath, content } = args as { path: string; content: object };
|
|
782
|
+
const dirtyError = dirtyTabError(relPath);
|
|
783
|
+
if (dirtyError) {
|
|
784
|
+
return dirtyError;
|
|
785
|
+
}
|
|
722
786
|
const errors = await validate(content);
|
|
723
787
|
if (errors.length > 0) {
|
|
724
788
|
const formatted = errors.map((e) => `- ${translateValidationError(e)}`).join("\n");
|
|
@@ -738,7 +802,10 @@ export function registerAiTools(
|
|
|
738
802
|
}
|
|
739
803
|
try {
|
|
740
804
|
await saveFile(relPath, JSON.stringify(content, null, 2));
|
|
741
|
-
return {
|
|
805
|
+
return {
|
|
806
|
+
success: true,
|
|
807
|
+
summary: await reconcileAfterWrite(relPath, `Created page at "${relPath}".`),
|
|
808
|
+
};
|
|
742
809
|
} catch (error) {
|
|
743
810
|
return {
|
|
744
811
|
success: false,
|
|
@@ -831,7 +898,7 @@ export function registerAiTools(
|
|
|
831
898
|
async execute(args) {
|
|
832
899
|
const tab = getTab();
|
|
833
900
|
if (!tab) {
|
|
834
|
-
return
|
|
901
|
+
return noDocError();
|
|
835
902
|
}
|
|
836
903
|
const { path } = args as { path: JxPath };
|
|
837
904
|
if (path.length < 2) {
|
|
@@ -846,7 +913,7 @@ export function registerAiTools(
|
|
|
846
913
|
`Removed node at ${JSON.stringify(path)}.`,
|
|
847
914
|
validate,
|
|
848
915
|
renderCheck,
|
|
849
|
-
|
|
916
|
+
styleOf(),
|
|
850
917
|
);
|
|
851
918
|
},
|
|
852
919
|
}),
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { renderOnly, updateUi } from "../store";
|
|
13
13
|
import { activeTab, workspace } from "../workspace/workspace";
|
|
14
14
|
import { applyPanelCollapse, view } from "../view";
|
|
15
|
+
import { setEditZoom as applyEditZoomLevel } from "../canvas/canvas-utils";
|
|
15
16
|
import type { JxPath } from "../state";
|
|
16
17
|
|
|
17
18
|
/** Callbacks injected from studio.ts (module-local helpers or imports kept out of this module). */
|
|
@@ -43,6 +44,7 @@ export interface AutomationApi {
|
|
|
43
44
|
setStatus: (text: string) => void;
|
|
44
45
|
setTheme: (color: string) => void;
|
|
45
46
|
setZoom: (zoom: number) => void;
|
|
47
|
+
setEditZoom: (zoom: number) => void;
|
|
46
48
|
waitForCanvasReady: (timeoutMs?: number) => Promise<void>;
|
|
47
49
|
}
|
|
48
50
|
|
|
@@ -94,6 +96,15 @@ export function createAutomationApi(deps: AutomationDeps): AutomationApi {
|
|
|
94
96
|
deps.render();
|
|
95
97
|
},
|
|
96
98
|
setRightTab(tab: string) {
|
|
99
|
+
// "assistant" now lives in the persistent chat sidebar, not the right panel — keep the
|
|
100
|
+
// Screenshot-manifest verb working by opening that sidebar instead.
|
|
101
|
+
if (tab === "assistant") {
|
|
102
|
+
view.chatPanelCollapsed = false;
|
|
103
|
+
applyPanelCollapse();
|
|
104
|
+
renderOnly("chatPanel");
|
|
105
|
+
deps.render();
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
97
108
|
updateUi("rightTab", tab);
|
|
98
109
|
deps.render();
|
|
99
110
|
},
|
|
@@ -107,6 +118,11 @@ export function createAutomationApi(deps: AutomationDeps): AutomationApi {
|
|
|
107
118
|
updateUi("zoom", zoom);
|
|
108
119
|
deps.render();
|
|
109
120
|
},
|
|
121
|
+
setEditZoom(zoom: number) {
|
|
122
|
+
// Deliberately NOT deps.render(): live edit zoom must never re-render the canvas (it would
|
|
123
|
+
// Rebuild the iframe DOM) — setEditZoom applies bare style writes, matching production paths.
|
|
124
|
+
applyEditZoomLevel(zoom);
|
|
125
|
+
},
|
|
110
126
|
waitForCanvasReady(timeoutMs = 30_000) {
|
|
111
127
|
return new Promise((resolve, reject) => {
|
|
112
128
|
const started = Date.now();
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context resolver — generic `#/$context/<pointer>` resolution over the project config
|
|
3
|
+
* (specs/extensions.md §9.1).
|
|
4
|
+
*
|
|
5
|
+
* Pointers walk the project config segment by segment. `{@param}` segments substitute values from a
|
|
6
|
+
* caller-provided scope (e.g. the form's current value record), and the `$formats` virtual root
|
|
7
|
+
* resolves to the registered format names instead of a project.json key.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Options for {@link resolveContextPointer}. */
|
|
11
|
+
export interface ResolveContextOptions {
|
|
12
|
+
/** The project configuration object the pointer walks over. */
|
|
13
|
+
projectConfig: Record<string, unknown>;
|
|
14
|
+
/** Scope record supplying `{@param}` substitutions (e.g. the enclosing form value). */
|
|
15
|
+
scope?: Record<string, unknown> | undefined;
|
|
16
|
+
/** Registered formats backing the `$formats` virtual root. */
|
|
17
|
+
formats?: { name: string }[] | undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Matches a `{@param}` pointer segment, capturing the param name. */
|
|
21
|
+
const PARAM_SEGMENT = /^\{@(\w+)\}$/;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Resolve a `#/$context/<seg>/<seg>…` pointer.
|
|
25
|
+
*
|
|
26
|
+
* - `{@param}` segments substitute from `opts.scope`; an unresolvable param yields `undefined`.
|
|
27
|
+
* - The `$formats` virtual root returns the format names from `opts.formats`.
|
|
28
|
+
* - Every other pointer is a plain JSON-pointer walk over `opts.projectConfig` — no key is
|
|
29
|
+
* special-cased, so both legacy (`contentTypes`) and newer (`content`) sections resolve alike.
|
|
30
|
+
*
|
|
31
|
+
* @param {string} pointer
|
|
32
|
+
* @param {ResolveContextOptions} opts
|
|
33
|
+
* @returns {unknown} The resolved value, or `undefined` when the pointer does not resolve.
|
|
34
|
+
*/
|
|
35
|
+
export function resolveContextPointer(pointer: string, opts: ResolveContextOptions): unknown {
|
|
36
|
+
if (!pointer.startsWith("#/$context/")) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const segments: string[] = [];
|
|
41
|
+
for (const raw of pointer.slice("#/$context/".length).split("/")) {
|
|
42
|
+
const match = raw.match(PARAM_SEGMENT);
|
|
43
|
+
if (match) {
|
|
44
|
+
const value = opts.scope?.[match[1]!];
|
|
45
|
+
if (typeof value !== "string" && typeof value !== "number") {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
const substituted = String(value);
|
|
49
|
+
if (!substituted) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
segments.push(substituted);
|
|
53
|
+
} else {
|
|
54
|
+
segments.push(raw);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (segments[0] === "$formats") {
|
|
59
|
+
if (segments.length !== 1) {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
return (opts.formats ?? []).map((format) => format.name);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let node: unknown = opts.projectConfig;
|
|
66
|
+
for (const segment of segments) {
|
|
67
|
+
if (node === null || typeof node !== "object") {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
node = (node as Record<string, unknown>)[segment];
|
|
71
|
+
}
|
|
72
|
+
return node;
|
|
73
|
+
}
|