@jxsuite/studio 1.2.0 → 1.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/studio",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Jx Studio — visual builder for Jx documents",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -21,7 +21,7 @@ import type { TemplateResult } from "lit-html";
21
21
  import { getPlatform } from "../platform";
22
22
  import { effect, effectScope } from "../reactivity";
23
23
  import { createDocumentAssistant } from "../services/document-assistant";
24
- import { hasOpenAiKey } from "../services/ai-settings";
24
+ import { hasOpenAiKey, setOpenAiKey } from "../services/ai-settings";
25
25
  import { fetchAvailableModels, isManagedProxy, isProxyConfigured } from "../services/ai-models";
26
26
  import { createAiCredentialsForm } from "../ui/ai-credentials-form";
27
27
  import { clearMarkdownCache } from "./ai-chat/chat-markdown";
@@ -263,6 +263,58 @@ export async function seedAssistantPrompt(text: string): Promise<void> {
263
263
  await handleAssistantSend(text);
264
264
  }
265
265
 
266
+ // ─── Automation seeding (screenshot runner) ─────────────────────────────────
267
+
268
+ /** A canned tool-call chip for {@link seedAssistantMessages}. */
269
+ export interface SeededToolCall {
270
+ name: string;
271
+ /** JSON-encoded arguments, e.g. `{"path":["children",0],"text":"…"}`. */
272
+ arguments: string;
273
+ }
274
+
275
+ /** A canned transcript entry for {@link seedAssistantMessages}. */
276
+ export interface SeededAssistantMessage {
277
+ role: "user" | "assistant";
278
+ content: string;
279
+ toolCalls?: SeededToolCall[];
280
+ }
281
+
282
+ let seededCount = 0;
283
+
284
+ /**
285
+ * Automation-only seam (scripts/screenshots): stage a canned conversation without ever invoking a
286
+ * model. Stores an inert demo key so the key gate opens (localStorage-only on the dev server — no
287
+ * request fires), switches to the chat view, and pushes fully-formed messages straight into the
288
+ * reactive chat state — the same path session restore uses — so the panel repaints through its
289
+ * normal watcher.
290
+ */
291
+ export function seedAssistantMessages(messages: SeededAssistantMessage[]): void {
292
+ setOpenAiKey("sk-demo");
293
+ keyEditing = false;
294
+ view = "chat";
295
+ stickToBottom = true;
296
+ for (const msg of messages) {
297
+ seededCount += 1;
298
+ const seq = seededCount;
299
+ assistant.chatState.messages.push({
300
+ content: msg.content,
301
+ id: `seeded_${seq}`,
302
+ role: msg.role,
303
+ timestamp: seq,
304
+ ...(msg.toolCalls
305
+ ? {
306
+ toolCalls: msg.toolCalls.map((tc, i) => ({
307
+ arguments: tc.arguments,
308
+ id: `seeded_${seq}_tc${i}`,
309
+ name: tc.name,
310
+ })),
311
+ }
312
+ : {}),
313
+ });
314
+ }
315
+ scheduleAiRender();
316
+ }
317
+
266
318
  // ─── Controls ─────────────────────────────────────────────────────────────────
267
319
 
268
320
  function stop() {
@@ -334,7 +334,9 @@ export function createCloudPlatform(project: CloudProject | null): StudioPlatfor
334
334
  * {kind:"git"} notices this handler ignores.
335
335
  */
336
336
  subscribeFileEvents(handler: (events: FsEvent[]) => void) {
337
- if (typeof WebSocket === "undefined" || typeof location === "undefined") {
337
+ // No project means an empty base, so the bare `/events` URL hits no gateway route.
338
+ // That fails in a reconnect loop; mirror `collab`'s guard and degrade to no-op on the hub.
339
+ if (!project || typeof WebSocket === "undefined" || typeof location === "undefined") {
338
340
  return () => {};
339
341
  }
340
342
  let socket: WebSocket | null = null;
@@ -36,3 +36,8 @@ export function getProjectList(): ProjectListEntry[] {
36
36
  export function resetProjectList(): void {
37
37
  cache = [];
38
38
  }
39
+
40
+ /** Seed seam for tests and the screenshot automation hook (staged catalogues). */
41
+ export function seedProjectList(entries: ProjectListEntry[]): void {
42
+ cache = entries;
43
+ }
@@ -339,6 +339,23 @@ function render(): void {
339
339
  }
340
340
  }
341
341
 
342
+ /**
343
+ * Automation-only seam (scripts/screenshots): open the modal directly in its connected state with a
344
+ * canned deployment, bypassing {@link loadConnection} so no Cloudflare request ever fires. The
345
+ * active project's `build.deploy` block still supplies the connected project/URL line.
346
+ */
347
+ export function seedPublishConnected(options: {
348
+ accountId?: string;
349
+ deployment: PagesDeploymentInfo;
350
+ }): void {
351
+ _connection = { accountId: options.accountId ?? "demo-account", connected: true };
352
+ _accounts = [];
353
+ _deployment = options.deployment;
354
+ _error = "";
355
+ _busy = false;
356
+ render();
357
+ }
358
+
342
359
  /** Open the publish modal for the active project. */
343
360
  export function openPublishPanel(): void {
344
361
  const config = currentConfig();
@@ -10,18 +10,30 @@
10
10
  */
11
11
 
12
12
  import { renderOnly, updateUi } from "../store";
13
- import { activeTab, workspace } from "../workspace/workspace";
13
+ import { setProjectState } from "../state";
14
+ import { seedProjectList } from "../project-list";
15
+ import { activeTab, closeAllTabs, workspace } from "../workspace/workspace";
16
+ import type { ProjectListEntry } from "../types";
14
17
  import { applyPanelCollapse, view } from "../view";
15
18
  import { setEditZoom as applyEditZoomLevel } from "../canvas/canvas-utils";
19
+ import { collabState } from "../collab/collab-state";
20
+ import type { PeerPresence } from "../collab/collab-state";
21
+ import type { SeededAssistantMessage } from "../panels/ai-panel";
22
+ import type { PagesDeploymentInfo } from "../publish/pages-service";
16
23
  import type { JxPath } from "../state";
17
24
 
18
25
  /** Callbacks injected from studio.ts (module-local helpers or imports kept out of this module). */
19
26
  export interface AutomationDeps {
20
27
  getCanvasMode: () => string;
21
28
  openBrowseModal: () => void;
29
+ openConnectorGrid: (connection: string | undefined, table: string) => void;
22
30
  openNewProjectModal: () => void;
31
+ openQuickSearchPalette: () => void;
32
+ openSettingsModal: (section?: string) => void;
23
33
  render: () => void;
24
34
  renderActivityBar: () => void;
35
+ seedAssistantMessages: (messages: SeededAssistantMessage[]) => void;
36
+ seedPublishConnected: (options: { accountId?: string; deployment: PagesDeploymentInfo }) => void;
25
37
  setCanvasMode: (mode: string) => void;
26
38
  statusMessage: (text: string) => void;
27
39
  }
@@ -30,7 +42,14 @@ export interface AutomationApi {
30
42
  editDef: (defName: string) => void;
31
43
  editFunction: (path: JxPath, eventKey: string) => void;
32
44
  openBrowse: () => void;
45
+ openDataGrid: (options: { connection?: string; table: string }) => void;
33
46
  openNewProject: () => void;
47
+ openQuickSearch: () => void;
48
+ openSettings: (section?: string) => void;
49
+ seedAssistant: (options: { messages: SeededAssistantMessage[] }) => void;
50
+ seedCollab: (options: { peers: PeerPresence[] }) => void;
51
+ seedPublish: (options: { accountId?: string; deployment: PagesDeploymentInfo }) => void;
52
+ showWelcome: (options?: { projects?: ProjectListEntry[] }) => void;
34
53
  getState: () => {
35
54
  activeTabId: string | null;
36
55
  canvasMode: string;
@@ -66,9 +85,56 @@ export function createAutomationApi(deps: AutomationDeps): AutomationApi {
66
85
  openBrowse() {
67
86
  deps.openBrowseModal();
68
87
  },
88
+ openDataGrid(options: { connection?: string; table: string }) {
89
+ deps.openConnectorGrid(options.connection, options.table);
90
+ },
69
91
  openNewProject() {
70
92
  deps.openNewProjectModal();
71
93
  },
94
+ openQuickSearch() {
95
+ deps.openQuickSearchPalette();
96
+ },
97
+ openSettings(section?: string) {
98
+ deps.openSettingsModal(section);
99
+ },
100
+ seedAssistant(options: { messages: SeededAssistantMessage[] }) {
101
+ deps.seedAssistantMessages(options.messages);
102
+ },
103
+ seedCollab(options: { peers: PeerPresence[] }) {
104
+ // Stage a live co-editing session on the active tab: the toolbar's presence chips and the
105
+ // Canvas peer-selection boxes both read this same reactive store.
106
+ const tab = activeTab.value;
107
+ if (!tab) {
108
+ return;
109
+ }
110
+ const state = collabState(tab);
111
+ state.status = "synced";
112
+ state.active = true;
113
+ state.peers = options.peers.map((peer) => ({
114
+ ...peer,
115
+ state: {
116
+ ...peer.state,
117
+ focusedPath: peer.state.focusedPath ?? tab.documentPath,
118
+ },
119
+ }));
120
+ deps.render();
121
+ },
122
+ seedPublish(options: { accountId?: string; deployment: PagesDeploymentInfo }) {
123
+ deps.seedPublishConnected(options);
124
+ },
125
+ showWelcome(options?: { projects?: ProjectListEntry[] }) {
126
+ // The devserver platform auto-opens the repo-root project on boot; the welcome
127
+ // Screen only renders with no project and no tabs, so stage that state directly.
128
+ // A staged catalogue replaces the real one (screenshots must not leak local paths).
129
+ closeAllTabs();
130
+ setProjectState(null);
131
+ if (options?.projects) {
132
+ seedProjectList(options.projects);
133
+ }
134
+ view.chatPanelCollapsed = true;
135
+ applyPanelCollapse();
136
+ deps.render();
137
+ },
72
138
  getState() {
73
139
  return {
74
140
  activeTabId: workspace.activeTabId,
@@ -106,11 +106,11 @@ let _activeSection = "general";
106
106
 
107
107
  let _contentEl: HTMLElement | null = null;
108
108
 
109
- export function openSettingsModal() {
109
+ export function openSettingsModal(section?: string) {
110
110
  if (_handle) {
111
111
  return;
112
112
  }
113
- _activeSection = "general";
113
+ _activeSection = section ?? "general";
114
114
  renderModal();
115
115
  // Refresh descriptor-contributed sections (cached payloads make this cheap) and rerender the
116
116
  // Nav once they land. Lazy import breaks the settings-modal ↔ extension-sections module cycle.
package/src/studio.ts CHANGED
@@ -96,6 +96,9 @@ import { renderHeadTemplate } from "./panels/head-panel";
96
96
  import { exportCemManifest as _exportCemManifest } from "./services/cem-export";
97
97
  import { installAutomationHook } from "./services/automation";
98
98
  import { openBrowseModal } from "./browse/browse-modal";
99
+ import { seedAssistantMessages } from "./panels/ai-panel";
100
+ import { seedPublishConnected } from "./publish/publish-panel";
101
+ import { openConnectorGrid } from "./grid/grid-open";
99
102
 
100
103
  import { getPlatform, hasPlatform, registerPlatform } from "./platform";
101
104
  import { parseMediaEntries } from "./utils/canvas-media";
@@ -145,7 +148,8 @@ import {
145
148
  renderBlockActionBar,
146
149
  } from "./panels/block-action-bar";
147
150
  import { initCssData } from "./panels/style-utils";
148
- import { initQuickSearch } from "./panels/quick-search";
151
+ import { initQuickSearch, openQuickSearch } from "./panels/quick-search";
152
+ import { openSettingsModal } from "./settings/settings-modal";
149
153
  import { hydrateAccountStatus } from "./account-status";
150
154
  import { hydrateProjectList } from "./project-list";
151
155
  import { addRecentProject, hydrateRecentProjects, removeRecentProject } from "./recent-projects";
@@ -391,9 +395,14 @@ if (!hasPlatform()) {
391
395
  installAutomationHook({
392
396
  getCanvasMode,
393
397
  openBrowseModal,
398
+ openConnectorGrid,
394
399
  openNewProjectModal,
400
+ openQuickSearchPalette: openQuickSearch,
401
+ openSettingsModal,
395
402
  render,
396
403
  renderActivityBar,
404
+ seedAssistantMessages,
405
+ seedPublishConnected,
397
406
  setCanvasMode,
398
407
  statusMessage,
399
408
  });
@@ -293,7 +293,10 @@ const OPERATOR_META: Record<string, OperatorMeta> = {
293
293
  export function operatorEntries(): FormulaCatalogEntry[] {
294
294
  const out: FormulaCatalogEntry[] = [];
295
295
  for (const op of BLESSED_OPERATORS) {
296
- const meta = OPERATOR_META[op];
296
+ // Own-property lookup only: a bare index would resolve inherited Object.prototype members for
297
+ // Ops like "toLocaleString", yielding a bogus meta whose undefined group/description breaks
298
+ // The palette's substring filter.
299
+ const meta = Object.hasOwn(OPERATOR_META, op) ? OPERATOR_META[op] : undefined;
297
300
  if (meta) {
298
301
  out.push({
299
302
  description: meta.description,