@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.
Files changed (113) 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 +103709 -81001
  5. package/dist/studio.js.map +201 -157
  6. package/package.json +47 -45
  7. package/src/account-status.ts +39 -0
  8. package/src/browse/browse.ts +10 -14
  9. package/src/canvas/canvas-live-render.ts +21 -2
  10. package/src/canvas/canvas-render.ts +35 -16
  11. package/src/canvas/canvas-utils.ts +118 -72
  12. package/src/canvas/iframe-entry.ts +20 -0
  13. package/src/canvas/iframe-eval.ts +155 -0
  14. package/src/canvas/iframe-host.ts +132 -6
  15. package/src/canvas/iframe-inline-edit.ts +107 -1
  16. package/src/canvas/iframe-protocol.ts +43 -3
  17. package/src/canvas/iframe-render.ts +18 -0
  18. package/src/collab/collab-session.ts +23 -8
  19. package/src/collab/collab-state.ts +6 -3
  20. package/src/component-props.ts +104 -0
  21. package/src/editor/context-menu.ts +1 -1
  22. package/src/editor/inline-edit-apply.ts +36 -1
  23. package/src/editor/inline-edit.ts +58 -1
  24. package/src/editor/repeater-scope.ts +8 -13
  25. package/src/editor/shortcuts.ts +41 -12
  26. package/src/files/file-ops.ts +14 -0
  27. package/src/files/files.ts +56 -1
  28. package/src/format/format-host.ts +63 -1
  29. package/src/grid/cell-editors.ts +288 -0
  30. package/src/grid/cell-popovers.ts +108 -0
  31. package/src/grid/csv-codec.ts +122 -0
  32. package/src/grid/edit-buffer.ts +490 -0
  33. package/src/grid/grid-controller.ts +417 -0
  34. package/src/grid/grid-layout.ts +83 -0
  35. package/src/grid/grid-open.ts +167 -0
  36. package/src/grid/grid-panel.ts +302 -0
  37. package/src/grid/grid-source.ts +210 -0
  38. package/src/grid/grid-view.ts +367 -0
  39. package/src/grid/schema-columns.ts +261 -0
  40. package/src/grid/sources/connector-source.ts +217 -0
  41. package/src/grid/sources/content-source.ts +542 -0
  42. package/src/grid/sources/csv-file-source.ts +247 -0
  43. package/src/grid/tabulator-tables.d.ts +120 -0
  44. package/src/new-project/add-repo-modal.ts +183 -0
  45. package/src/new-project/new-project-modal.ts +22 -3
  46. package/src/page-params.ts +34 -8
  47. package/src/panels/ai-chat/chat-markdown.ts +2 -2
  48. package/src/panels/ai-panel.ts +61 -4
  49. package/src/panels/block-action-bar.ts +8 -3
  50. package/src/panels/chat-panel.ts +98 -0
  51. package/src/panels/data-grid.ts +241 -0
  52. package/src/panels/editors.ts +43 -17
  53. package/src/panels/events-panel.ts +137 -44
  54. package/src/panels/formula-workspace.ts +379 -0
  55. package/src/panels/frontmatter-fields.ts +231 -0
  56. package/src/panels/frontmatter-panel.ts +132 -0
  57. package/src/panels/git-panel.ts +1 -1
  58. package/src/panels/head-panel.ts +11 -175
  59. package/src/panels/properties-panel.ts +154 -144
  60. package/src/panels/right-panel.ts +9 -47
  61. package/src/panels/signals-panel.ts +217 -460
  62. package/src/panels/statement-editor.ts +710 -0
  63. package/src/panels/statusbar.ts +1 -1
  64. package/src/panels/style-panel.ts +25 -1
  65. package/src/panels/stylebook-panel.ts +0 -2
  66. package/src/panels/tab-bar.ts +175 -6
  67. package/src/panels/tab-strip.ts +62 -6
  68. package/src/panels/toolbar.ts +37 -3
  69. package/src/panels/welcome-screen.ts +50 -0
  70. package/src/platform-errors.ts +30 -0
  71. package/src/platforms/cloud.ts +172 -89
  72. package/src/platforms/devserver.ts +172 -0
  73. package/src/services/ai-project-tools.ts +541 -0
  74. package/src/services/ai-session-store.ts +28 -0
  75. package/src/services/ai-system-prompt.ts +194 -24
  76. package/src/services/ai-tools.ts +88 -21
  77. package/src/services/automation.ts +16 -0
  78. package/src/services/context-resolver.ts +73 -0
  79. package/src/services/data-service.ts +155 -0
  80. package/src/services/document-assistant.ts +81 -11
  81. package/src/services/gated-registry.ts +72 -0
  82. package/src/services/live-preview.ts +0 -0
  83. package/src/services/monaco-setup.ts +75 -26
  84. package/src/services/preview-eval.ts +68 -0
  85. package/src/services/project-adoption.ts +31 -0
  86. package/src/settings/contributed-section.ts +406 -0
  87. package/src/settings/extension-sections.ts +145 -0
  88. package/src/settings/schema-field-ui.ts +4 -2
  89. package/src/settings/settings-modal.ts +101 -42
  90. package/src/site-context.ts +12 -1
  91. package/src/store.ts +4 -0
  92. package/src/studio.ts +107 -52
  93. package/src/tabs/patch-ops.ts +25 -0
  94. package/src/tabs/tab.ts +39 -1
  95. package/src/tabs/transact.ts +61 -14
  96. package/src/types.ts +132 -1
  97. package/src/ui/dynamic-slot.ts +272 -0
  98. package/src/ui/expression-editor.ts +423 -125
  99. package/src/ui/field-row.ts +5 -0
  100. package/src/ui/form-controls.ts +322 -0
  101. package/src/ui/formula-catalog.ts +557 -0
  102. package/src/ui/formula-chips.ts +216 -0
  103. package/src/ui/formula-palette.ts +211 -0
  104. package/src/ui/layers.ts +40 -0
  105. package/src/ui/media-picker.ts +1 -1
  106. package/src/ui/panel-resize.ts +15 -1
  107. package/src/ui/progress-modal.ts +2 -2
  108. package/src/ui/schema-form.ts +524 -0
  109. package/src/utils/preview-format.ts +26 -0
  110. package/src/utils/studio-utils.ts +4 -3
  111. package/src/view.ts +4 -4
  112. package/src/workspace/workspace.ts +14 -0
  113. package/src/settings/content-types-editor.ts +0 -599
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Data-service — thin PAL wrapper over the optional data-surface members
3
+ * (dataConnections/dataConnectionTest/dataPush/dataRows/row CRUD/listSecrets/setSecrets).
4
+ *
5
+ * Every helper degrades cleanly when the platform omits the member family (older backends, cloud
6
+ * without DB reach): list-shaped calls resolve null/empty, action-shaped calls resolve an error
7
+ * result instead of throwing. Secret VALUES only ever flow INTO setSecrets — reads are names-only
8
+ * (specs/extensions.md §13).
9
+ */
10
+
11
+ import { getPlatform } from "../platform";
12
+ import type {
13
+ DataConnectionsResponse,
14
+ DataConnectionTestResult,
15
+ DataPushResult,
16
+ DataRowDelete,
17
+ DataRowInsert,
18
+ DataRowsQuery,
19
+ DataRowsResult,
20
+ DataRowUpdate,
21
+ SecretsSetRequest,
22
+ SecretsSetResponse,
23
+ StudioPlatform,
24
+ } from "../types";
25
+
26
+ /** The registered platform, or null before registration (early boot, bare tests). */
27
+ function platformOrNull(): StudioPlatform | null {
28
+ try {
29
+ return getPlatform();
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+
35
+ /** True when the platform serves the data grid (the family's gating member is dataRows). */
36
+ export function dataSurfaceAvailable(): boolean {
37
+ return typeof platformOrNull()?.dataRows === "function";
38
+ }
39
+
40
+ /** True when the platform can store secret values (backs the "secret" form control). */
41
+ export function secretsAvailable(): boolean {
42
+ return typeof platformOrNull()?.setSecrets === "function";
43
+ }
44
+
45
+ /** Connections with configured state and table names; null when the backend lacks the route. */
46
+ export async function fetchConnections(): Promise<DataConnectionsResponse | null> {
47
+ const platform = platformOrNull();
48
+ if (typeof platform?.dataConnections !== "function") {
49
+ return null;
50
+ }
51
+ return platform.dataConnections();
52
+ }
53
+
54
+ /** Probe one connection; degrades to an error result instead of throwing. */
55
+ export async function testConnection(connection: string): Promise<DataConnectionTestResult> {
56
+ const platform = platformOrNull();
57
+ if (typeof platform?.dataConnectionTest !== "function") {
58
+ return { error: "Connection tests are not supported by this backend", ok: false };
59
+ }
60
+ try {
61
+ return await platform.dataConnectionTest(connection);
62
+ } catch (error) {
63
+ return { error: error instanceof Error ? error.message : String(error), ok: false };
64
+ }
65
+ }
66
+
67
+ /** Push table schemas (dryRun compiles the plan only); degrades to an error result. */
68
+ export async function pushSchema(opts?: {
69
+ connection?: string;
70
+ dryRun?: boolean;
71
+ }): Promise<DataPushResult> {
72
+ const platform = platformOrNull();
73
+ if (typeof platform?.dataPush !== "function") {
74
+ return { applied: false, errors: ["Schema push is not supported by this backend"], plan: [] };
75
+ }
76
+ try {
77
+ return await platform.dataPush(opts);
78
+ } catch (error) {
79
+ return {
80
+ applied: false,
81
+ errors: [error instanceof Error ? error.message : String(error)],
82
+ plan: [],
83
+ };
84
+ }
85
+ }
86
+
87
+ /** A required data member, throwing a uniform degradation error when absent. */
88
+ function requireMember<K extends keyof StudioPlatform>(name: K): NonNullable<StudioPlatform[K]> {
89
+ const platform = platformOrNull();
90
+ const member = platform?.[name];
91
+ if (member === undefined || member === null) {
92
+ throw new Error(`The ${String(name)} surface is not supported by this backend`);
93
+ }
94
+ return member as NonNullable<StudioPlatform[K]>;
95
+ }
96
+
97
+ /** Page a table's rows (grid callers handle errors per load). */
98
+ export function fetchRows(query: DataRowsQuery): Promise<DataRowsResult> {
99
+ return requireMember("dataRows")(query);
100
+ }
101
+
102
+ export function insertRow(req: DataRowInsert): Promise<{ row: Record<string, unknown> }> {
103
+ return requireMember("dataInsertRow")(req);
104
+ }
105
+
106
+ export function updateRow(req: DataRowUpdate): Promise<{ row: Record<string, unknown> }> {
107
+ return requireMember("dataUpdateRow")(req);
108
+ }
109
+
110
+ export function deleteRow(req: DataRowDelete): Promise<{ ok: boolean }> {
111
+ return requireMember("dataDeleteRow")(req);
112
+ }
113
+
114
+ /** Configured secret env-var NAMES; null when the backend has no secrets surface. */
115
+ export async function listSecretNames(): Promise<string[] | null> {
116
+ const platform = platformOrNull();
117
+ if (typeof platform?.listSecrets !== "function") {
118
+ return null;
119
+ }
120
+ return platform.listSecrets();
121
+ }
122
+
123
+ /** Write/remove secrets through the platform store (values flow in, names come back). */
124
+ export function saveSecrets(req: SecretsSetRequest): Promise<SecretsSetResponse> {
125
+ return requireMember("setSecrets")(req);
126
+ }
127
+
128
+ /**
129
+ * Derive the env-var NAME a secret-marked field stores its value under: CONSTANT_CASE of the entry
130
+ * key (falling back to the section key) plus the field key minus its `Env` suffix — e.g. connection
131
+ * "main" field "urlEnv" → "MAIN_URL"; section "auth" field "secretEnv" → "AUTH_SECRET". The NAME
132
+ * goes into project.json; the VALUE goes to setSecrets.
133
+ *
134
+ * @param {string} sectionKey
135
+ * @param {string | null} entryKey - The map-layout entry (connection) name, when any
136
+ * @param {string} fieldKey
137
+ * @returns {string}
138
+ */
139
+ export function deriveSecretEnvName(
140
+ sectionKey: string,
141
+ entryKey: string | null,
142
+ fieldKey: string,
143
+ ): string {
144
+ const constant = (raw: string) =>
145
+ raw
146
+ .replaceAll(/([a-z0-9])([A-Z])/g, "$1_$2")
147
+ .replaceAll(/[^A-Za-z0-9]+/g, "_")
148
+ .replaceAll(/^_+|_+$/g, "")
149
+ .toUpperCase();
150
+ const parts = [entryKey ?? sectionKey, fieldKey.replace(/Env$/, "")]
151
+ .map((part) => constant(part))
152
+ .filter(Boolean);
153
+ const name = parts.join("_") || "SECRET";
154
+ return /^\d/.test(name) ? `JX_${name}` : name;
155
+ }
@@ -12,16 +12,22 @@
12
12
  import { createChatState, createProxyStreamingClient, createToolRegistry } from "@jxsuite/ai";
13
13
  import type { ProjectConfig } from "@jxsuite/schema/types";
14
14
  import { getPlatform } from "../platform";
15
- import { activeTab, workspace } from "../workspace/workspace";
15
+ import { activeTab, setWorkspaceProject, workspace } from "../workspace/workspace";
16
16
  import { toRaw } from "../reactivity";
17
+ import { projectState, setProjectState } from "../store";
18
+ import type { Tab } from "../tabs/tab";
17
19
  import { componentRegistry } from "../files/components";
18
20
  import { registerAiTools } from "./ai-tools";
21
+ import { registerProjectTools } from "./ai-project-tools";
22
+ import { createGatedToolRegistry } from "./gated-registry";
23
+ import type { ToolAvailability } from "./gated-registry";
24
+ import { adoptProject } from "./project-adoption";
19
25
  import { runAgentLoop } from "./tool-executor";
20
- import { buildSystemPrompt } from "./ai-system-prompt";
26
+ import { AI_TOOL_TIERS, buildSystemPrompt, tierActive } from "./ai-system-prompt";
21
27
  import { getBaseUrl, getModel, getOpenAiKey } from "./ai-settings";
22
28
  import { trimContext } from "./context-manager";
23
29
  import { renderCheck } from "./render-critic";
24
- import { openFileInTab } from "../files/files";
30
+ import { openFileInTab, reloadFileInTab } from "../files/files";
25
31
  import * as sessionStore from "./ai-session-store";
26
32
 
27
33
  /**
@@ -51,8 +57,23 @@ function projectRoot() {
51
57
  export function createDocumentAssistant() {
52
58
  const chatState = createChatState({ model: getModel() });
53
59
 
54
- const toolRegistry = createToolRegistry();
55
- registerAiTools(toolRegistry, {
60
+ /** The persisted session backing the live chat; null = fresh unsaved chat. */
61
+ let sessionId: string | null = null;
62
+
63
+ const getProjectStyle = () =>
64
+ (workspace.projectConfig as ProjectConfig | null)?.style as Record<string, string> | undefined;
65
+
66
+ const findOpenTab = (path: string): Tab | null => {
67
+ for (const tab of workspace.tabs.values()) {
68
+ if (tab.documentPath === path) {
69
+ return tab;
70
+ }
71
+ }
72
+ return null;
73
+ };
74
+
75
+ const innerRegistry = createToolRegistry();
76
+ registerAiTools(innerRegistry, {
56
77
  getTab: () => activeTab.value,
57
78
  saveFile: async (relPath: string, content: string) => {
58
79
  const plat = getPlatform();
@@ -62,23 +83,72 @@ export function createDocumentAssistant() {
62
83
  doc: unknown,
63
84
  ) => Promise<{ ok: true } | { ok: false; error: string }>,
64
85
  openDocument: openFileInTab,
65
- projectStyle: (workspace.projectConfig as ProjectConfig | null)?.style as
66
- | Record<string, string>
67
- | undefined,
86
+ getProjectStyle,
87
+ findOpenTab,
88
+ reloadTab: reloadFileInTab,
89
+ });
90
+ registerProjectTools(innerRegistry, {
91
+ getTab: () => activeTab.value,
92
+ renderCheck: renderCheck as (
93
+ doc: unknown,
94
+ ) => Promise<{ ok: true } | { ok: false; error: string }>,
95
+ getProjectStyle,
96
+ findOpenTab,
97
+ reloadTab: reloadFileInTab,
98
+ adoptProject,
99
+ // Re-key the live chat from the unscoped store to the adopted project so the bootstrap
100
+ // Conversation keeps persisting (saveSession drops writes for ids missing from the index).
101
+ onProjectAdopted: (root: string) => {
102
+ if (sessionId) {
103
+ sessionStore.moveSession("", root, sessionId);
104
+ }
105
+ },
106
+ onProjectConfigWritten: (config: object) => {
107
+ setWorkspaceProject(workspace.projectRoot, config);
108
+ if (projectState) {
109
+ setProjectState({ ...projectState, projectConfig: config as ProjectConfig });
110
+ }
111
+ },
68
112
  });
69
113
 
70
- let controller: AbortController | null = null;
114
+ /*
115
+ * Gate tool advertisement/execution on live studio state, derived from the same tier table the
116
+ * system prompt renders — the model is never shown a tool it cannot execute. listForLLM() runs
117
+ * every agent-loop round, so a mid-loop create_project unlocks the higher tiers immediately.
118
+ */
119
+ const TIER_REQUIREMENTS = {
120
+ "no-project": "no project to be open (it bootstraps one)",
121
+ project: "an open project",
122
+ document: "an open document (use open_document first)",
123
+ } as const;
124
+ const availability = new Map<string, ToolAvailability>(
125
+ AI_TOOL_TIERS.map((t) => [
126
+ t.name,
127
+ {
128
+ when: () => tierActive(t.tier, Boolean(workspace.projectRoot), Boolean(activeTab.value)),
129
+ requires: TIER_REQUIREMENTS[t.tier],
130
+ },
131
+ ]),
132
+ );
133
+ const toolRegistry = createGatedToolRegistry(innerRegistry, availability);
71
134
 
72
- /** The persisted session backing the live chat; null = fresh unsaved chat. */
73
- let sessionId: string | null = null;
135
+ let controller: AbortController | null = null;
74
136
 
75
137
  function buildPrompt() {
76
138
  const tab = activeTab.value;
139
+ const inventory = projectState
140
+ ? [...projectState.dirs.values()]
141
+ .flat()
142
+ .filter((e) => e.type === "file")
143
+ .map((e) => e.path)
144
+ : undefined;
77
145
  return buildSystemPrompt({
78
146
  document: tab ? toRaw(tab.doc.document) : undefined,
79
147
  projectConfig: (workspace.projectConfig as ProjectConfig | null) || undefined,
80
148
  components: componentRegistry.length > 0 ? componentRegistry : undefined,
81
149
  projectRoot: workspace.projectRoot || undefined,
150
+ hasProject: Boolean(workspace.projectRoot),
151
+ ...(inventory && inventory.length > 0 ? { fileInventory: inventory } : {}),
82
152
  });
83
153
  }
84
154
 
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Gated-registry.ts — state-aware tool advertisement for the AI assistant.
3
+ *
4
+ * Wraps a `@jxsuite/ai` ToolRegistry so each tool is only advertised to the LLM while its
5
+ * availability predicate holds (no project open → bootstrap tools only; project open → file tools;
6
+ * document open → document tools). `runAgentLoop` calls `listForLLM()` every round, so availability
7
+ * flips mid-loop: after `create_project` succeeds, the very next round advertises the file and
8
+ * document tools without any extra plumbing.
9
+ *
10
+ * Executing a tool whose predicate currently fails returns a tool error naming the missing state
11
+ * instead of throwing, so the agent loop can self-correct.
12
+ *
13
+ * @license MIT
14
+ */
15
+
16
+ import type { ToolDefinition, ToolRegistry } from "@jxsuite/ai/tools";
17
+
18
+ export interface ToolAvailability {
19
+ /** Whether the tool is currently advertised/executable. */
20
+ when: () => boolean;
21
+ /** Human-readable requirement, surfaced when execution is refused (e.g. "a project is open"). */
22
+ requires: string;
23
+ }
24
+
25
+ /**
26
+ * Wrap `inner` so tools listed in `availability` are only advertised/executable while their
27
+ * predicate holds. Tools without an entry are always available.
28
+ *
29
+ * @param {ToolRegistry} inner
30
+ * @param {Map<string, ToolAvailability>} availability
31
+ * @returns {ToolRegistry}
32
+ */
33
+ export function createGatedToolRegistry(
34
+ inner: ToolRegistry,
35
+ availability: Map<string, ToolAvailability>,
36
+ ): ToolRegistry {
37
+ function isAvailable(name: string): boolean {
38
+ const gate = availability.get(name);
39
+ return gate ? gate.when() : true;
40
+ }
41
+
42
+ return {
43
+ register(tool: ToolDefinition) {
44
+ inner.register(tool);
45
+ },
46
+ list() {
47
+ return inner.list().filter((t) => isAvailable(t.name));
48
+ },
49
+ listForLLM() {
50
+ const available = new Set(this.list().map((t) => t.name));
51
+ return inner
52
+ .listForLLM()
53
+ .filter((t) => available.has((t as { function?: { name?: string } }).function?.name ?? ""));
54
+ },
55
+ getDefinition(toolName: string) {
56
+ return inner.getDefinition(toolName);
57
+ },
58
+ validate(toolName: string, args: object) {
59
+ return inner.validate(toolName, args);
60
+ },
61
+ async execute(toolName: string, args: object) {
62
+ if (inner.getDefinition(toolName) && !isAvailable(toolName)) {
63
+ const gate = availability.get(toolName);
64
+ return {
65
+ success: false,
66
+ error: `Tool "${toolName}" is not available right now — it requires ${gate?.requires ?? "a different studio state"}.`,
67
+ };
68
+ }
69
+ return inner.execute(toolName, args);
70
+ },
71
+ };
72
+ }
Binary file
@@ -35,29 +35,78 @@ if (typeof document !== "undefined" && document.fonts?.ready) {
35
35
  });
36
36
  }
37
37
 
38
- // oxlint-disable-next-line typescript/no-unsafe-call, typescript/no-unsafe-member-access -- jsonDefaults is imported from Monaco's untyped ESM contribution (see @ts-expect-error above); no type declarations exist for this named export
39
- jsonDefaults.setDiagnosticsOptions({
40
- allowComments: false,
41
- schemas: [
42
- {
43
- fileMatch: [
44
- "pages/*.json",
45
- "pages/**/*.json",
46
- "layouts/*.json",
47
- "layouts/**/*.json",
48
- "components/*.json",
49
- "components/**/*.json",
50
- "elements/*.json",
51
- "elements/**/*.json",
52
- ],
53
- schema: jxSchema,
54
- uri: "https://jxsuite.com/schema/v1",
55
- },
56
- {
57
- fileMatch: ["project.json"],
58
- schema: projectSchema,
59
- uri: "https://jxsuite.com/schema/project/v1",
60
- },
61
- ],
62
- validate: true,
63
- });
38
+ /** Folder globs whose JSON files validate against the (per-project or core) document schema. */
39
+ const DOCUMENT_FILE_MATCH = [
40
+ "pages/*.json",
41
+ "pages/**/*.json",
42
+ "layouts/*.json",
43
+ "layouts/**/*.json",
44
+ "components/*.json",
45
+ "components/**/*.json",
46
+ "elements/*.json",
47
+ "elements/**/*.json",
48
+ ];
49
+
50
+ /**
51
+ * Register the JSON diagnostics schemas. Fetched per-project schemas are inline OBJECTS (never URLs
52
+ * — `enableSchemaRequest` stays off), so backends must serve them PRE-BUNDLED; the bundled core
53
+ * schemas remain the offline fallback.
54
+ */
55
+ function applyJsonSchemas(documentSchema: unknown, projectJsonSchema: unknown) {
56
+ // oxlint-disable-next-line typescript/no-unsafe-call, typescript/no-unsafe-member-access -- jsonDefaults is imported from Monaco's untyped ESM contribution (see @ts-expect-error above); no type declarations exist for this named export
57
+ jsonDefaults.setDiagnosticsOptions({
58
+ allowComments: false,
59
+ schemas: [
60
+ {
61
+ fileMatch: DOCUMENT_FILE_MATCH,
62
+ schema: documentSchema,
63
+ uri: "https://jxsuite.com/schema/v1",
64
+ },
65
+ {
66
+ fileMatch: ["project.json"],
67
+ schema: projectJsonSchema,
68
+ uri: "https://jxsuite.com/schema/project/v1",
69
+ },
70
+ ],
71
+ validate: true,
72
+ });
73
+ }
74
+
75
+ // Bundled core schemas register at module init — the offline fallback every platform starts from.
76
+ applyJsonSchemas(jxSchema, projectSchema);
77
+
78
+ /**
79
+ * Swap Monaco's JSON schemas to the ACTIVE project's generated entry documents, fetched pre-bundled
80
+ * through the platform's optional `fetchProjectSchemas` member (dev server:
81
+ * `/__studio/project-schemas`; desktop: RPC into the project session). Call on project activation
82
+ * and after project.json `extensions` changes.
83
+ *
84
+ * @param {object} platform - The studio platform (only `fetchProjectSchemas` is consulted)
85
+ * @returns {Promise<boolean>} True when per-project schemas were applied; false on fallback
86
+ */
87
+ export async function refreshProjectSchemas(platform: {
88
+ fetchProjectSchemas?: () => Promise<{ project?: unknown; document?: unknown }>;
89
+ }): Promise<boolean> {
90
+ const fetcher = platform.fetchProjectSchemas;
91
+ if (!fetcher) {
92
+ return false;
93
+ }
94
+ let schemas: { project?: unknown; document?: unknown };
95
+ try {
96
+ schemas = (await fetcher.call(platform)) ?? {};
97
+ } catch {
98
+ // Editor degradation: keep the bundled core schemas.
99
+ return false;
100
+ }
101
+ const { document, project } = schemas;
102
+ if (!document && !project) {
103
+ return false;
104
+ }
105
+ applyJsonSchemas(document ?? jxSchema, project ?? projectSchema);
106
+ return true;
107
+ }
108
+
109
+ /** Restore the bundled core schemas (project closed / tests). */
110
+ export function resetProjectSchemas(): void {
111
+ applyJsonSchemas(jxSchema, projectSchema);
112
+ }
@@ -0,0 +1,68 @@
1
+ /// <reference lib="dom" />
2
+ /**
3
+ * Parent-side live expression preview (spec §19.9).
4
+ *
5
+ * Evaluates an expression node against the canvas iframe's last dataScope snapshot
6
+ * (tab.session.canvas.scope) on a structuredClone, so mutating operators are safe — they mutate the
7
+ * clone, never the live canvas. The engine's trace hook reports every sub-node's value keyed by its
8
+ * path within the tree; values are formatted to display strings at report time so later mutations
9
+ * of the clone cannot retroactively change a badge.
10
+ */
11
+
12
+ import { evaluateExpression, isMutating } from "@jxsuite/runtime/expression";
13
+ import { toRaw } from "../reactivity";
14
+ // Imported for local use AND re-exported below so existing consumers keep their import site.
15
+ // oxlint-disable-next-line unicorn/prefer-export-from
16
+ import { formatPreviewValue } from "../utils/preview-format";
17
+
18
+ import type { ExpressionNode } from "@jxsuite/runtime/expression";
19
+
20
+ export interface ExpressionPreview {
21
+ /** Path-keyed display values: "" is the root node, "value/target" a nested operand, etc. */
22
+ values: Map<string, string>;
23
+ /** Evaluation error message, if the expression threw. */
24
+ error: string | null;
25
+ /** Whether the root operator mutates its target (badge renders as an effect, not a value). */
26
+ mutating: boolean;
27
+ }
28
+
29
+ // The formatter is shared with the in-iframe live evaluator: it lives in utils/preview-format.ts
30
+ // (dependency-light, safe for the iframe bundle) and is re-exported here so existing consumers
31
+ // Keep their import site.
32
+ export { formatPreviewValue };
33
+
34
+ /**
35
+ * Evaluate `node` against a scope snapshot for display. Returns null when no snapshot is available
36
+ * (canvas not yet rendered) — the editor renders no badges rather than wrong ones.
37
+ */
38
+ export function previewExpression(
39
+ node: unknown,
40
+ scope?: Record<string, unknown> | null,
41
+ ): ExpressionPreview | null {
42
+ if (!scope || !node || typeof node !== "object" || !("operator" in node)) {
43
+ return null;
44
+ }
45
+
46
+ let state: Record<string, unknown>;
47
+ try {
48
+ // Callers hand over tab.session.canvas.scope, which the reactive session tree wraps in a
49
+ // Proxy — structuredClone rejects proxies, so unwrap to the raw snapshot first.
50
+ state = structuredClone(toRaw(scope));
51
+ } catch {
52
+ // Snapshot values are JSON-safe by construction (serialize-scope), but stay defensive.
53
+ return null;
54
+ }
55
+
56
+ const values = new Map<string, string>();
57
+ const expr = node as ExpressionNode;
58
+ let errorMessage: string | null = null;
59
+ try {
60
+ evaluateExpression(expr, state, null, undefined, {
61
+ report: (path, value) => values.set(path.join("/"), formatPreviewValue(value)),
62
+ });
63
+ } catch (error) {
64
+ errorMessage = error instanceof Error ? error.message : String(error);
65
+ }
66
+
67
+ return { error: errorMessage, mutating: isMutating(expr.operator), values };
68
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Project-adoption.ts — a late-bound slot for opening a freshly created project in this window.
3
+ *
4
+ * The AI `create_project` tool (services/ai-project-tools.ts) needs to run the full project-open
5
+ * flow (`openRecentProject` in studio.ts), but services must not import studio.ts — the entry
6
+ * module imports the services. studio.ts registers the adopter at boot; the tool calls
7
+ * `adoptProject(root)`.
8
+ *
9
+ * @license MIT
10
+ */
11
+
12
+ let _adopter: ((root: string) => Promise<void>) | null = null;
13
+
14
+ /** Register the project-open flow (studio.ts boot). */
15
+ export function setProjectAdopter(fn: (root: string) => Promise<void>): void {
16
+ _adopter = fn;
17
+ }
18
+
19
+ /**
20
+ * Open the project at `root` in this window via the registered flow.
21
+ *
22
+ * @param {string} root - Absolute project root.
23
+ */
24
+ export async function adoptProject(root: string): Promise<void> {
25
+ if (!_adopter) {
26
+ throw new Error(
27
+ "Project adoption is not available in this environment — open the project manually from the welcome screen.",
28
+ );
29
+ }
30
+ await _adopter(root);
31
+ }