@jxsuite/studio 1.0.0 → 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.
Files changed (86) hide show
  1. package/dist/iframe-entry.js +622 -49
  2. package/dist/iframe-entry.js.map +15 -12
  3. package/dist/studio.css +1333 -0
  4. package/dist/studio.js +53069 -31746
  5. package/dist/studio.js.map +109 -74
  6. package/package.json +11 -9
  7. package/src/canvas/canvas-live-render.ts +21 -2
  8. package/src/canvas/canvas-render.ts +35 -16
  9. package/src/canvas/canvas-utils.ts +118 -72
  10. package/src/canvas/iframe-entry.ts +20 -0
  11. package/src/canvas/iframe-eval.ts +155 -0
  12. package/src/canvas/iframe-host.ts +131 -5
  13. package/src/canvas/iframe-inline-edit.ts +107 -1
  14. package/src/canvas/iframe-protocol.ts +43 -3
  15. package/src/canvas/iframe-render.ts +18 -0
  16. package/src/collab/collab-session.ts +23 -8
  17. package/src/collab/collab-state.ts +6 -3
  18. package/src/component-props.ts +104 -0
  19. package/src/editor/inline-edit-apply.ts +36 -1
  20. package/src/editor/inline-edit.ts +58 -1
  21. package/src/editor/shortcuts.ts +41 -12
  22. package/src/files/file-ops.ts +14 -0
  23. package/src/files/files.ts +53 -1
  24. package/src/grid/cell-editors.ts +288 -0
  25. package/src/grid/cell-popovers.ts +108 -0
  26. package/src/grid/csv-codec.ts +122 -0
  27. package/src/grid/edit-buffer.ts +490 -0
  28. package/src/grid/grid-controller.ts +417 -0
  29. package/src/grid/grid-layout.ts +83 -0
  30. package/src/grid/grid-open.ts +167 -0
  31. package/src/grid/grid-panel.ts +302 -0
  32. package/src/grid/grid-source.ts +210 -0
  33. package/src/grid/grid-view.ts +367 -0
  34. package/src/grid/schema-columns.ts +261 -0
  35. package/src/grid/sources/connector-source.ts +217 -0
  36. package/src/grid/sources/content-source.ts +542 -0
  37. package/src/grid/sources/csv-file-source.ts +247 -0
  38. package/src/grid/tabulator-tables.d.ts +120 -0
  39. package/src/panels/block-action-bar.ts +8 -3
  40. package/src/panels/chat-panel.ts +98 -0
  41. package/src/panels/data-grid.ts +9 -387
  42. package/src/panels/editors.ts +43 -17
  43. package/src/panels/events-panel.ts +137 -44
  44. package/src/panels/formula-workspace.ts +379 -0
  45. package/src/panels/frontmatter-fields.ts +231 -0
  46. package/src/panels/frontmatter-panel.ts +132 -0
  47. package/src/panels/git-panel.ts +1 -1
  48. package/src/panels/head-panel.ts +11 -175
  49. package/src/panels/properties-panel.ts +154 -144
  50. package/src/panels/right-panel.ts +9 -47
  51. package/src/panels/signals-panel.ts +115 -23
  52. package/src/panels/statement-editor.ts +710 -0
  53. package/src/panels/style-panel.ts +25 -1
  54. package/src/panels/stylebook-panel.ts +0 -2
  55. package/src/panels/tab-bar.ts +175 -6
  56. package/src/panels/tab-strip.ts +62 -6
  57. package/src/panels/toolbar.ts +37 -3
  58. package/src/services/ai-project-tools.ts +541 -0
  59. package/src/services/ai-session-store.ts +28 -0
  60. package/src/services/ai-system-prompt.ts +194 -24
  61. package/src/services/ai-tools.ts +88 -21
  62. package/src/services/automation.ts +16 -0
  63. package/src/services/document-assistant.ts +81 -11
  64. package/src/services/gated-registry.ts +72 -0
  65. package/src/services/live-preview.ts +0 -0
  66. package/src/services/preview-eval.ts +68 -0
  67. package/src/services/project-adoption.ts +31 -0
  68. package/src/site-context.ts +2 -0
  69. package/src/store.ts +4 -0
  70. package/src/studio.ts +83 -52
  71. package/src/tabs/patch-ops.ts +25 -0
  72. package/src/tabs/tab.ts +39 -1
  73. package/src/tabs/transact.ts +60 -13
  74. package/src/types.ts +12 -0
  75. package/src/ui/dynamic-slot.ts +272 -0
  76. package/src/ui/expression-editor.ts +423 -125
  77. package/src/ui/field-row.ts +5 -0
  78. package/src/ui/formula-catalog.ts +557 -0
  79. package/src/ui/formula-chips.ts +216 -0
  80. package/src/ui/formula-palette.ts +211 -0
  81. package/src/ui/layers.ts +40 -0
  82. package/src/ui/media-picker.ts +1 -1
  83. package/src/ui/panel-resize.ts +15 -1
  84. package/src/utils/preview-format.ts +26 -0
  85. package/src/view.ts +4 -4
  86. package/src/workspace/workspace.ts +14 -0
@@ -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
- // 1. Role & capabilities
386
- const sections = [
387
- `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.
388
-
389
- You have access to these tools that read and modify the live Jx document directly. Always prefer tool calls over describing changes in text:
390
- - 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].
391
- - 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.
392
- - 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.
393
- - set_text(path, value) — convenient alias for set_property with key: "textContent".
394
- - add_child(parentPath, index, node) — insert a new node into the children of parentPath at index.
395
- - remove_node(path) remove the node at path.
396
- - move_node(fromPath, toParentPath, toIndex) move a node from one location to another.
397
- - 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.
398
- - update_state(key, value) update or remove (value: null) an existing state variable.
399
- - create_component(path, content)create a new .json component file on disk.
400
- - create_page(path, content) — create a new .json page file on disk.
401
- - open_document(path) switch the active document to another file. After opening, all tools operate on the new document. Use this after create_page/create_component to iteratively refine the new file.
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. Summarize what you changed clearly.
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
- 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.
572
+ Be concise. Don't explain what Jx is unless asked. Just build.`;
412
573
 
413
- Be concise. Don't explain what Jx is unless asked. Just build.`,
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
 
@@ -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 { success: false, error: "No document is open." };
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 { success: false, error: "No document is open." };
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
- projectStyle,
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 { success: false, error: "No document is open." };
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
- projectStyle,
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 { success: false, error: "No document is open." };
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
- projectStyle,
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 { success: false, error: "No document is open." };
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
- projectStyle,
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 { success: false, error: "No document is open." };
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
- projectStyle,
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 { success: false, error: "No document is open." };
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
- projectStyle,
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 { success: false, error: "No document is open." };
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
- projectStyle,
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 { success: true, summary: `Created component at "${relPath}".` };
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 { success: true, summary: `Created page at "${relPath}".` };
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 { success: false, error: "No document is open." };
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
- projectStyle,
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();