@jxsuite/studio 0.34.0 → 0.36.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 (41) hide show
  1. package/dist/iframe-entry.js +9 -1
  2. package/dist/iframe-entry.js.map +3 -3
  3. package/dist/studio.js +18382 -3686
  4. package/dist/studio.js.map +265 -26
  5. package/package.json +9 -6
  6. package/src/files/files.ts +3 -0
  7. package/src/new-project/design-fields.ts +260 -0
  8. package/src/new-project/import-tab.ts +322 -0
  9. package/src/new-project/new-project-modal.ts +472 -89
  10. package/src/new-project/templates.ts +61 -0
  11. package/src/packages/ensure-deps.ts +6 -0
  12. package/src/packages/pull-package-sync.ts +339 -0
  13. package/src/panels/ai-chat/attached-context.ts +49 -0
  14. package/src/panels/ai-chat/chat-markdown.ts +38 -0
  15. package/src/panels/ai-chat/chat-view.ts +250 -0
  16. package/src/panels/ai-chat/composer.ts +315 -0
  17. package/src/panels/ai-chat/sessions-view.ts +98 -0
  18. package/src/panels/ai-panel.ts +214 -458
  19. package/src/panels/drag-ghost.ts +1 -1
  20. package/src/panels/git-panel.ts +16 -1
  21. package/src/panels/right-panel.ts +21 -5
  22. package/src/panels/toolbar.ts +2 -0
  23. package/src/panels/welcome-screen.ts +26 -0
  24. package/src/platforms/cloud.ts +774 -0
  25. package/src/platforms/devserver.ts +100 -1
  26. package/src/project-list.ts +38 -0
  27. package/src/publish/pages-service.ts +186 -0
  28. package/src/publish/publish-panel.ts +360 -0
  29. package/src/services/agent-seed.ts +91 -0
  30. package/src/services/ai-models.ts +93 -0
  31. package/src/services/ai-session-store.ts +250 -0
  32. package/src/services/ai-settings.ts +5 -0
  33. package/src/services/automation.ts +140 -0
  34. package/src/services/cf-settings.ts +58 -0
  35. package/src/services/document-assistant.ts +98 -38
  36. package/src/services/import-client.ts +134 -0
  37. package/src/services/settings-store.ts +99 -0
  38. package/src/studio.ts +33 -0
  39. package/src/types.ts +130 -133
  40. package/src/ui/ai-credentials-form.ts +205 -0
  41. package/src/ui/spectrum.ts +8 -0
package/src/types.ts CHANGED
@@ -1,127 +1,58 @@
1
1
  /// <reference lib="dom" />
2
- import type {
3
- JsonValue as SchemaJsonValue,
4
- JxMutableNode,
5
- JxPath,
6
- ProjectConfig,
7
- } from "@jxsuite/schema/types";
8
-
9
- // ─── Git & Platform Types ───────────────────────────────────────────────────
10
-
11
- export interface GitFileStatus {
12
- status: string;
13
- path: string;
14
- staged?: boolean;
15
- }
16
-
17
- export interface GitStatusResult {
18
- branch: string;
19
- files: GitFileStatus[];
20
- ahead: number;
21
- behind: number;
22
- isRepo: boolean;
23
- remotes: string[];
24
- }
25
-
26
- export interface GitBranchesResult {
27
- current: string;
28
- branches: string[];
29
- }
30
-
31
- export interface GitLogEntry {
32
- hash: string;
33
- message: string;
34
- author: string;
35
- date: string;
36
- }
37
-
38
- export interface ComponentSlotMeta {
39
- name: string;
40
- description?: string;
41
- fallback?: (JxMutableNode | string)[];
42
- }
43
-
44
- export interface ComponentMeta {
45
- tagName: string;
46
- $id?: string | null;
47
- path: string;
48
- props?: { name: string; type?: string; default?: JsonValue; [k: string]: unknown }[];
49
- slots?: ComponentSlotMeta[];
50
- hasElements?: boolean;
51
- }
52
-
53
- export interface PackageInfo {
54
- name: string;
55
- version: string;
56
- /** True when the dependency lives in `devDependencies` rather than `dependencies`. */
57
- dev?: boolean;
58
- }
59
-
60
- /** A dependency with a newer version available, as reported by `bun outdated` / the npm registry. */
61
- export interface OutdatedInfo {
62
- name: string;
63
- /** The version range pinned in package.json (e.g. "^0.19.0"). */
64
- current: string;
65
- /** The newest published version. */
66
- latest: string;
67
- /** The newest version satisfying the current range, if known. */
68
- wanted?: string;
69
- dev?: boolean;
70
- }
71
-
72
- /** Result of a package mutation that runs `bun install` (install / set-versions). */
73
- export interface PackageOpResult {
74
- ok: boolean;
75
- /** Combined stdout/stderr from the bun invocation, surfaced to the user on failure. */
76
- log?: string;
77
- }
78
-
79
- /** Desktop app/build info surfaced in the About screen. */
80
- export interface AppInfo {
81
- version: string;
82
- channel: string;
83
- hash: string;
84
- /** Human-readable update status (e.g. "Up to date", "Update available"), if known. */
85
- updateStatus?: string;
86
- }
87
-
88
- export interface CodeServiceResult {
89
- code?: string;
90
- diagnostics?: unknown[];
91
- [key: string]: unknown;
92
- }
2
+ import type { JxMutableNode, JxPath, ProjectConfig } from "@jxsuite/schema/types";
93
3
 
94
- export interface DirEntry {
95
- name: string;
96
- path: string;
97
- type: "file" | "directory";
98
- size?: number;
99
- modified?: string;
100
- }
4
+ // ─── Wire types (the Studio Backend Protocol) ───────────────────────────────
5
+ /* The request/response shapes every backend serves live in @jxsuite/protocol;
6
+ re-exported here so existing `../types` imports keep working. */
101
7
 
102
- /** A filesystem change pushed from the backend (project-relative, forward-slashed path). */
103
- export interface FsEvent {
104
- type: "add" | "change" | "unlink" | "addDir" | "unlinkDir";
105
- path: string;
106
- isDir: boolean;
107
- }
8
+ import type {
9
+ AppInfo,
10
+ CfConnection,
11
+ CodeServiceResult,
12
+ ComponentMeta,
13
+ DirEntry,
14
+ FsEvent,
15
+ GitBranchesResult,
16
+ GitLogEntry,
17
+ GitStatusResult,
18
+ ImportProgressEvent,
19
+ ImportSiteOptions,
20
+ OutdatedInfo,
21
+ PackageInfo,
22
+ PackageOpResult,
23
+ ProjectListEntry,
24
+ RecentProjectEntry,
25
+ RenameResult,
26
+ StarterInfo,
27
+ } from "@jxsuite/protocol";
108
28
 
109
- /** Result of a rename, including the references rewritten across the project (refactor report). */
110
- export interface RenameResult {
111
- ok: boolean;
112
- from: string;
113
- to: string;
114
- isDir?: boolean;
115
- references?: {
116
- filesChanged: number;
117
- refsUpdated: number;
118
- files: { path: string; count: number }[];
119
- };
120
- errors?: { path: string; error: string }[];
121
- tag?: { from: string; to: string; filesChanged: number; refsUpdated: number };
122
- tagSkipped?: string;
123
- error?: string;
124
- }
29
+ export type {
30
+ AiModelInfo,
31
+ AiModelsResponse,
32
+ AppInfo,
33
+ CfConnection,
34
+ CodeServiceResult,
35
+ ComponentMeta,
36
+ ComponentSlotMeta,
37
+ DirEntry,
38
+ ErrorBody,
39
+ FsEvent,
40
+ GitBranchesResult,
41
+ GitFileStatus,
42
+ GitLogEntry,
43
+ GitStatusResult,
44
+ ImportProgressEvent,
45
+ ImportSiteOptions,
46
+ JsonValue,
47
+ OutdatedInfo,
48
+ PackageInfo,
49
+ PackageOpResult,
50
+ ProjectListEntry,
51
+ PullRequestInfo,
52
+ RecentProjectEntry,
53
+ RenameResult,
54
+ StarterInfo,
55
+ } from "@jxsuite/protocol";
125
56
 
126
57
  export interface StudioPlatform {
127
58
  id: string;
@@ -227,7 +158,41 @@ export interface StudioPlatform {
227
158
  url?: string;
228
159
  adapter?: string;
229
160
  directory: string;
161
+ /** Id of a starter template to clone, or "blank"/undefined for the minimal template. */
162
+ starter?: string;
163
+ /** Id of a built-in template variant (from `@jxsuite/create/templates`); undefined = blank. */
164
+ template?: string;
165
+ /** Design quickstart (colors, fonts, logo, breakpoints) applied on top of the scaffold. */
166
+ design?: {
167
+ accent?: string;
168
+ background?: string;
169
+ text?: string;
170
+ bodyFont?: string;
171
+ headingFont?: string;
172
+ media?: Record<string, string>;
173
+ logo?: { name: string; base64: string };
174
+ };
230
175
  }) => Promise<{ root: string; config: ProjectConfig }>;
176
+ /**
177
+ * List the starter templates available in the New Project picker. Absent on platforms that don't
178
+ * ship starters (the picker then offers only a blank project).
179
+ */
180
+ listStarters?: () => Promise<StarterInfo[]>;
181
+ /**
182
+ * AI-guided import of an existing site into a new project. Runs in the backend (headless Chrome +
183
+ * fs), streaming progress until the project is written. Absent on platforms without a backend
184
+ * import pipeline — the New Project modal hides its Import tab then.
185
+ */
186
+ importSite?: (
187
+ opts: ImportSiteOptions,
188
+ onProgress: (evt: ImportProgressEvent) => void,
189
+ signal?: AbortSignal,
190
+ ) => Promise<{ root: string; config: ProjectConfig }>;
191
+ /**
192
+ * Open a native directory picker and return the chosen absolute path (null when cancelled).
193
+ * Desktop only; the dev server interprets directories relative to its root instead.
194
+ */
195
+ pickDirectory?: () => Promise<string | null>;
231
196
  /** Stack B AI assistant: URL of the OpenAI-compatible SSE chat proxy (`/__studio/ai/chat`). */
232
197
  aiChatUrl: () => string | Promise<string>;
233
198
  // ─── Multi-window (desktop only; undefined on dev-server) ───────────────────
@@ -251,13 +216,51 @@ export interface StudioPlatform {
251
216
  getRecentProjects?: () => Promise<RecentProjectEntry[]>;
252
217
  /** Persist the full recent-projects list to the backend store. */
253
218
  saveRecentProjects?: (projects: RecentProjectEntry[]) => Promise<void>;
254
- }
255
-
256
- /** A recently-opened project, keyed by its re-openable `root` (platform-specific). */
257
- export interface RecentProjectEntry {
258
- name: string;
259
- root: string;
260
- timestamp: number;
219
+ // ─── Project catalogue (platforms that can enumerate openable projects) ─────
220
+ /**
221
+ * Enumerate every project this platform can open — the dev server's sites under its root, a cloud
222
+ * platform's remote projects. Entry `root` values re-open through the same paths as recent
223
+ * projects (openRecentProject). Absent on desktop, where the OS file system is the catalogue and
224
+ * projects are found via the native picker instead.
225
+ */
226
+ listProjects?: () => Promise<ProjectListEntry[]>;
227
+ // ─── Identity & hosting connections (publish surface) ───────────────────────
228
+ /** The signed-in user's identity, when the platform has one (cloud). */
229
+ getUser?: () => Promise<{ login: string; name?: string; avatarUrl?: string } | null>;
230
+ /**
231
+ * Open a pull request for the current branch. Cloud platforms implement it against their session;
232
+ * local platforms omit it and Studio falls back to a direct GitHub API call with the user's
233
+ * stored token.
234
+ */
235
+ createPullRequest?: (opts: {
236
+ title: string;
237
+ body?: string;
238
+ head?: string;
239
+ base?: string;
240
+ }) => Promise<{ url: string; number: number }>;
241
+ /** Current Cloudflare connection state, when the platform can broker one. */
242
+ cfConnection?: () => Promise<CfConnection | null>;
243
+ /**
244
+ * Interactively connect a Cloudflare account (hosted OAuth on the cloud platform). Local
245
+ * platforms omit it — the publish UI collects an API token instead and verifies via
246
+ * cfConnection.
247
+ */
248
+ cfConnect?: () => Promise<CfConnection | null>;
249
+ /**
250
+ * Allowlisted Cloudflare API passthrough (accounts, Pages projects and deployments). The backend
251
+ * injects credentials — an OAuth token on the cloud platform, the user's pasted API token locally
252
+ * — so no secret ever rides in the request body.
253
+ */
254
+ cfApi?: (path: string, init?: { method?: string; body?: unknown }) => Promise<unknown>;
255
+ // ─── User settings (backend-persisted; undefined on dev-server) ─────────────
256
+ /**
257
+ * Read the user-level settings map (e.g. the AI connection parameters) from a backend store
258
+ * shared across all projects/windows. Platforms without a native backend (dev server) omit it and
259
+ * settings live in localStorage only.
260
+ */
261
+ getSettings?: () => Promise<Record<string, string>>;
262
+ /** Persist the full user-level settings map to the backend store. */
263
+ saveSettings?: (settings: Record<string, string>) => Promise<void>;
261
264
  }
262
265
 
263
266
  // ─── Studio Types ───────────────────────────────────────────────────────────
@@ -325,9 +328,3 @@ export interface ProjectState {
325
328
  projectDirs?: string[];
326
329
  [key: string]: unknown;
327
330
  }
328
-
329
- /**
330
- * A JSON document value, or `undefined` to signal property removal in the mutators. Re-uses the
331
- * schema's precise recursive JSON model.
332
- */
333
- export type JsonValue = SchemaJsonValue | undefined;
@@ -0,0 +1,205 @@
1
+ /// <reference lib="dom" />
2
+ /**
3
+ * Ai-credentials-form.ts — reusable AI provider credentials form (key / model / endpoint).
4
+ *
5
+ * Extracted from the ai-panel key gate so other hosts (e.g. the New Project modal) can embed the
6
+ * same form. All draft/model-list state lives inside the closure, so multiple instances never
7
+ * share state. Persists via src/services/ai-settings.ts on Save.
8
+ *
9
+ * @license MIT
10
+ */
11
+
12
+ import { html, nothing } from "lit-html";
13
+ import type { TemplateResult } from "lit-html";
14
+ import { fetchAvailableModels, invalidateModelCache } from "../services/ai-models";
15
+ import {
16
+ getBaseUrl,
17
+ getModel,
18
+ getOpenAiKey,
19
+ hasOpenAiKey,
20
+ setBaseUrl,
21
+ setModel,
22
+ setOpenAiKey,
23
+ } from "../services/ai-settings";
24
+
25
+ export interface AiCredentialsFormOptions {
26
+ /** Host re-render scheduler — called whenever the form's internal state changes. */
27
+ requestRender: () => void;
28
+ /** Called after Save persists the credentials. */
29
+ onSaved?: () => void;
30
+ /** Called when Cancel dismisses the form (Cancel is only offered when a key already exists). */
31
+ onCancel?: () => void;
32
+ /** Optional context line (TemplateResult | string) replacing the default blurb. */
33
+ intro?: unknown;
34
+ }
35
+
36
+ export interface AiCredentialsForm {
37
+ render: () => TemplateResult;
38
+ /** Preload drafts from the stored ai-settings and auto-fetch the model list. */
39
+ startEdit: () => void;
40
+ }
41
+
42
+ /**
43
+ * Create an AI credentials form instance bound to a host's render scheduler.
44
+ *
45
+ * @param {AiCredentialsFormOptions} opts
46
+ * @returns {AiCredentialsForm}
47
+ */
48
+ export function createAiCredentialsForm(opts: AiCredentialsFormOptions): AiCredentialsForm {
49
+ let keyDraft = "";
50
+ let baseUrlDraft = "";
51
+ let modelDraft = "";
52
+
53
+ /** Fetched from the proxy's /models endpoint (sibling of the chat endpoint). */
54
+ let availableModels: { id: string; name: string }[] = [];
55
+ let modelsLoading = false;
56
+ let modelsError = "";
57
+
58
+ /** Open the form pre-filled with the current settings, and load the model list. */
59
+ function startEdit() {
60
+ keyDraft = getOpenAiKey();
61
+ baseUrlDraft = getBaseUrl();
62
+ modelDraft = getModel();
63
+ opts.requestRender();
64
+ // Auto-fetch available models if not already loaded.
65
+ if (availableModels.length === 0 && !modelsLoading) {
66
+ void fetchModels();
67
+ }
68
+ }
69
+
70
+ /** Persist the drafted key + endpoint + model and notify the host. */
71
+ function save() {
72
+ setOpenAiKey(keyDraft);
73
+ setBaseUrl(baseUrlDraft);
74
+ setModel(modelDraft);
75
+ keyDraft = "";
76
+ baseUrlDraft = "";
77
+ modelDraft = "";
78
+ // Clear fetched models so they're re-fetched with the new credentials next time.
79
+ availableModels = [];
80
+ invalidateModelCache();
81
+ opts.onSaved?.();
82
+ opts.requestRender();
83
+ }
84
+
85
+ /** Dismiss the form without saving (only offered when a key already exists). */
86
+ function cancel() {
87
+ keyDraft = "";
88
+ baseUrlDraft = "";
89
+ modelDraft = "";
90
+ opts.onCancel?.();
91
+ opts.requestRender();
92
+ }
93
+
94
+ /**
95
+ * Fetch available models via src/services/ai-models.ts, preferring the in-form drafts over the
96
+ * stored settings so the list reflects the credentials being edited. Always forces past the
97
+ * module cache — this runs on explicit user action (or first open) with possibly-new drafts.
98
+ */
99
+ async function fetchModels() {
100
+ modelsLoading = true;
101
+ modelsError = "";
102
+ opts.requestRender();
103
+ try {
104
+ availableModels = await fetchAvailableModels({
105
+ apiKey: getOpenAiKey() || keyDraft,
106
+ baseUrl: baseUrlDraft || getBaseUrl(),
107
+ force: true,
108
+ });
109
+ } catch (error: unknown) {
110
+ modelsError = (error as Error).message || "Failed to fetch models";
111
+ } finally {
112
+ modelsLoading = false;
113
+ opts.requestRender();
114
+ }
115
+ }
116
+
117
+ /** The key + model + endpoint form column. */
118
+ function render(): TemplateResult {
119
+ const haveKey = hasOpenAiKey();
120
+ return html`
121
+ <div
122
+ class="ai-creds-form"
123
+ style="display:flex;flex-direction:column;gap:10px;max-width:320px;text-align:left"
124
+ >
125
+ <div style="font-weight:600;align-self:center">AI provider key</div>
126
+ ${opts.intro === undefined
127
+ ? html`
128
+ <div style="font-size:11px;color:var(--fg-dim)">
129
+ Any OpenAI-compatible key works. Stored locally in this browser; sent only to the
130
+ Studio proxy (never to a third party except your chosen endpoint).
131
+ </div>
132
+ `
133
+ : html`<div style="font-size:11px;color:var(--fg-dim)">${opts.intro}</div>`}
134
+ <input
135
+ type="password"
136
+ style="width:100%;box-sizing:border-box;padding:6px 8px;border-radius:4px;border:1px solid var(--border);background:var(--bg-input);color:var(--fg);font-size:12px"
137
+ placeholder="sk-… or any compatible key"
138
+ .value=${keyDraft}
139
+ @input=${(e: Event) => {
140
+ keyDraft = (e.target as HTMLInputElement).value;
141
+ }}
142
+ />
143
+ <div style="font-weight:500;font-size:11px;margin-top:4px">Model</div>
144
+ ${availableModels.length > 0
145
+ ? html`
146
+ <sp-combobox
147
+ size="s"
148
+ allows-custom-value
149
+ .value=${modelDraft}
150
+ @change=${(e: Event) => {
151
+ modelDraft = (e.target as HTMLInputElement).value;
152
+ }}
153
+ @input=${(e: Event) => {
154
+ modelDraft = (e.target as HTMLInputElement).value;
155
+ }}
156
+ >
157
+ ${availableModels.map(
158
+ (m) => html`<sp-menu-item value=${m.id}>${m.name}</sp-menu-item>`,
159
+ )}
160
+ </sp-combobox>
161
+ `
162
+ : html`
163
+ <input
164
+ type="text"
165
+ style="width:100%;box-sizing:border-box;padding:6px 8px;border-radius:4px;border:1px solid var(--border);background:var(--bg-input);color:var(--fg);font-size:12px"
166
+ placeholder="Model ID (e.g. gpt-4o, claude-sonnet-4-20250514, etc.)"
167
+ .value=${modelDraft}
168
+ @input=${(e: Event) => {
169
+ modelDraft = (e.target as HTMLInputElement).value;
170
+ }}
171
+ />
172
+ `}
173
+ <div style="display:flex;gap:8px;align-items:center">
174
+ <sp-button size="s" variant="secondary" ?disabled=${modelsLoading} @click=${fetchModels}>
175
+ ${modelsLoading
176
+ ? "Fetching…"
177
+ : availableModels.length > 0
178
+ ? "Refresh models"
179
+ : "Fetch models"}
180
+ </sp-button>
181
+ ${modelsError
182
+ ? html`<span style="font-size:10px;color:var(--danger)">${modelsError}</span>`
183
+ : nothing}
184
+ </div>
185
+ <input
186
+ type="text"
187
+ style="width:100%;box-sizing:border-box;padding:6px 8px;border-radius:4px;border:1px solid var(--border);background:var(--bg-input);color:var(--fg);font-size:12px"
188
+ placeholder="Endpoint (optional, e.g. http://localhost:11434/v1)"
189
+ .value=${baseUrlDraft}
190
+ @input=${(e: Event) => {
191
+ baseUrlDraft = (e.target as HTMLInputElement).value;
192
+ }}
193
+ />
194
+ <div style="display:flex;gap:8px;align-self:flex-end">
195
+ ${haveKey
196
+ ? html`<sp-button size="s" variant="secondary" @click=${cancel}>Cancel</sp-button>`
197
+ : nothing}
198
+ <sp-button size="s" variant="primary" @click=${save}>Save</sp-button>
199
+ </div>
200
+ </div>
201
+ `;
202
+ }
203
+
204
+ return { render, startEdit };
205
+ }
@@ -127,6 +127,10 @@ import { IconVisibility } from "@spectrum-web-components/icons-workflow/src/elem
127
127
  import { IconVisibilityOff } from "@spectrum-web-components/icons-workflow/src/elements/IconVisibilityOff.js";
128
128
  import { IconArtboard } from "@spectrum-web-components/icons-workflow/src/elements/IconArtboard.js";
129
129
  import { IconChat } from "@spectrum-web-components/icons-workflow/src/elements/IconChat.js";
130
+ import { IconSend } from "@spectrum-web-components/icons-workflow/src/elements/IconSend.js";
131
+ import { IconStop } from "@spectrum-web-components/icons-workflow/src/elements/IconStop.js";
132
+ import { IconHistory } from "@spectrum-web-components/icons-workflow/src/elements/IconHistory.js";
133
+ import { IconAttach } from "@spectrum-web-components/icons-workflow/src/elements/IconAttach.js";
130
134
  import { IconViewList } from "@spectrum-web-components/icons-workflow/src/elements/IconViewList.js";
131
135
  import { IconRailRightClose } from "@spectrum-web-components/icons-workflow/src/elements/IconRailRightClose.js";
132
136
  import { IconRailRightOpen } from "@spectrum-web-components/icons-workflow/src/elements/IconRailRightOpen.js";
@@ -272,6 +276,10 @@ const components = [
272
276
  ["sp-icon-visibility-off", IconVisibilityOff],
273
277
  ["sp-icon-artboard", IconArtboard],
274
278
  ["sp-icon-chat", IconChat],
279
+ ["sp-icon-send", IconSend],
280
+ ["sp-icon-stop", IconStop],
281
+ ["sp-icon-history", IconHistory],
282
+ ["sp-icon-attach", IconAttach],
275
283
  ["sp-icon-view-list", IconViewList],
276
284
  ["sp-icon-text-bold", IconTextBold],
277
285
  ["sp-icon-text-italic", IconTextItalic],