@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.
- package/dist/iframe-entry.js +9 -1
- package/dist/iframe-entry.js.map +3 -3
- package/dist/studio.js +18382 -3686
- package/dist/studio.js.map +265 -26
- package/package.json +9 -6
- package/src/files/files.ts +3 -0
- package/src/new-project/design-fields.ts +260 -0
- package/src/new-project/import-tab.ts +322 -0
- package/src/new-project/new-project-modal.ts +472 -89
- package/src/new-project/templates.ts +61 -0
- package/src/packages/ensure-deps.ts +6 -0
- package/src/packages/pull-package-sync.ts +339 -0
- package/src/panels/ai-chat/attached-context.ts +49 -0
- package/src/panels/ai-chat/chat-markdown.ts +38 -0
- package/src/panels/ai-chat/chat-view.ts +250 -0
- package/src/panels/ai-chat/composer.ts +315 -0
- package/src/panels/ai-chat/sessions-view.ts +98 -0
- package/src/panels/ai-panel.ts +214 -458
- package/src/panels/drag-ghost.ts +1 -1
- package/src/panels/git-panel.ts +16 -1
- package/src/panels/right-panel.ts +21 -5
- package/src/panels/toolbar.ts +2 -0
- package/src/panels/welcome-screen.ts +26 -0
- package/src/platforms/cloud.ts +774 -0
- package/src/platforms/devserver.ts +100 -1
- package/src/project-list.ts +38 -0
- package/src/publish/pages-service.ts +186 -0
- package/src/publish/publish-panel.ts +360 -0
- package/src/services/agent-seed.ts +91 -0
- package/src/services/ai-models.ts +93 -0
- package/src/services/ai-session-store.ts +250 -0
- package/src/services/ai-settings.ts +5 -0
- package/src/services/automation.ts +140 -0
- package/src/services/cf-settings.ts +58 -0
- package/src/services/document-assistant.ts +98 -38
- package/src/services/import-client.ts +134 -0
- package/src/services/settings-store.ts +99 -0
- package/src/studio.ts +33 -0
- package/src/types.ts +130 -133
- package/src/ui/ai-credentials-form.ts +205 -0
- package/src/ui/spectrum.ts +8 -0
|
@@ -22,20 +22,16 @@ import { getBaseUrl, getModel, getOpenAiKey } from "./ai-settings";
|
|
|
22
22
|
import { trimContext } from "./context-manager";
|
|
23
23
|
import { renderCheck } from "./render-critic";
|
|
24
24
|
import { openFileInTab } from "../files/files";
|
|
25
|
-
|
|
26
|
-
const PERSIST_KEY_PREFIX = "jx-ai-chat-history";
|
|
27
|
-
const MAX_PERSIST_MESSAGES = 50;
|
|
25
|
+
import * as sessionStore from "./ai-session-store";
|
|
28
26
|
|
|
29
27
|
/**
|
|
30
|
-
* Project
|
|
31
|
-
* projects
|
|
28
|
+
* Project root scoping the session store (ADR §11.5 / §14.2 — conversations don't bleed across
|
|
29
|
+
* projects). Falls back to a shared unscoped store when no project is open.
|
|
32
30
|
*
|
|
33
31
|
* @returns {string}
|
|
34
32
|
*/
|
|
35
|
-
function
|
|
36
|
-
return workspace.projectRoot
|
|
37
|
-
? `${PERSIST_KEY_PREFIX}:${workspace.projectRoot}`
|
|
38
|
-
: PERSIST_KEY_PREFIX;
|
|
33
|
+
function projectRoot() {
|
|
34
|
+
return workspace.projectRoot || "";
|
|
39
35
|
}
|
|
40
36
|
|
|
41
37
|
/**
|
|
@@ -46,6 +42,10 @@ function persistKey() {
|
|
|
46
42
|
* sendMessage: (text: string) => Promise<void>;
|
|
47
43
|
* stop: () => void;
|
|
48
44
|
* newChat: () => void;
|
|
45
|
+
* listSessions: () => import("./ai-session-store").SessionMeta[];
|
|
46
|
+
* openSession: (id: string) => void;
|
|
47
|
+
* deleteSession: (id: string) => void;
|
|
48
|
+
* activeSessionId: () => string | null;
|
|
49
49
|
* }}
|
|
50
50
|
*/
|
|
51
51
|
export function createDocumentAssistant() {
|
|
@@ -69,6 +69,9 @@ export function createDocumentAssistant() {
|
|
|
69
69
|
|
|
70
70
|
let controller: AbortController | null = null;
|
|
71
71
|
|
|
72
|
+
/** The persisted session backing the live chat; null = fresh unsaved chat. */
|
|
73
|
+
let sessionId: string | null = null;
|
|
74
|
+
|
|
72
75
|
function buildPrompt() {
|
|
73
76
|
const tab = activeTab.value;
|
|
74
77
|
return buildSystemPrompt({
|
|
@@ -84,6 +87,12 @@ export function createDocumentAssistant() {
|
|
|
84
87
|
return;
|
|
85
88
|
}
|
|
86
89
|
|
|
90
|
+
// Lazily create the backing session on the first message so empty "New Chat"
|
|
91
|
+
// Clicks never pollute the session list.
|
|
92
|
+
if (!sessionId) {
|
|
93
|
+
sessionId = sessionStore.createSession(projectRoot(), text).id;
|
|
94
|
+
}
|
|
95
|
+
|
|
87
96
|
chatState.sendMessage(text);
|
|
88
97
|
|
|
89
98
|
// Trim context before streaming to keep the conversation within token limits.
|
|
@@ -93,7 +102,7 @@ export function createDocumentAssistant() {
|
|
|
93
102
|
}
|
|
94
103
|
|
|
95
104
|
// Persist after trimming so the saved history reflects what's actually sent.
|
|
96
|
-
persistChat(
|
|
105
|
+
persistChat();
|
|
97
106
|
|
|
98
107
|
try {
|
|
99
108
|
const plat = getPlatform();
|
|
@@ -127,6 +136,9 @@ export function createDocumentAssistant() {
|
|
|
127
136
|
chatState.setError(error instanceof Error ? error.message : String(error));
|
|
128
137
|
} finally {
|
|
129
138
|
controller = null;
|
|
139
|
+
// Persist again once the stream settled so the completed reply (or the state
|
|
140
|
+
// After an error/abort cleanup) survives a reload without another send.
|
|
141
|
+
persistChat();
|
|
130
142
|
}
|
|
131
143
|
}
|
|
132
144
|
|
|
@@ -138,46 +150,94 @@ export function createDocumentAssistant() {
|
|
|
138
150
|
function newChat() {
|
|
139
151
|
stop();
|
|
140
152
|
chatState.clearChat();
|
|
141
|
-
|
|
153
|
+
sessionId = null;
|
|
154
|
+
sessionStore.setActiveSession(projectRoot(), null);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ── Sessions ──────────────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
/** The project's persisted sessions, most recently updated first. */
|
|
160
|
+
function listSessions() {
|
|
161
|
+
return sessionStore.listSessions(projectRoot());
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Replace the live chat with a persisted session's messages. */
|
|
165
|
+
function openSession(id: string) {
|
|
166
|
+
const msgs = sessionStore.loadSession(projectRoot(), id);
|
|
167
|
+
if (!msgs) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
stop();
|
|
171
|
+
chatState.clearChat();
|
|
172
|
+
pushRestoredMessages(msgs);
|
|
173
|
+
sessionId = id;
|
|
174
|
+
sessionStore.setActiveSession(projectRoot(), id);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Delete a persisted session; deleting the open one leaves a fresh unsaved chat. */
|
|
178
|
+
function deleteSession(id: string) {
|
|
179
|
+
sessionStore.deleteSession(projectRoot(), id);
|
|
180
|
+
if (sessionId === id) {
|
|
181
|
+
stop();
|
|
182
|
+
chatState.clearChat();
|
|
183
|
+
sessionId = null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function activeSessionId() {
|
|
188
|
+
return sessionId;
|
|
142
189
|
}
|
|
143
190
|
|
|
144
191
|
// ── Persistence ───────────────────────────────────────────────────────
|
|
145
192
|
|
|
146
|
-
/** Persist
|
|
147
|
-
function persistChat(
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
localStorage.setItem(persistKey(), JSON.stringify(msgs));
|
|
151
|
-
} catch {
|
|
152
|
-
// Storage full or unavailable — not critical.
|
|
193
|
+
/** Persist the live conversation into its backing session (non-blocking). */
|
|
194
|
+
function persistChat() {
|
|
195
|
+
if (!sessionId) {
|
|
196
|
+
return;
|
|
153
197
|
}
|
|
198
|
+
// Skip a still-empty streaming placeholder so reloads don't restore blank bubbles.
|
|
199
|
+
const msgs = chatState.messages.filter(
|
|
200
|
+
(m) => m.role !== "assistant" || m.content || (m.toolCalls?.length ?? 0) > 0,
|
|
201
|
+
);
|
|
202
|
+
sessionStore.saveSession(projectRoot(), sessionId, msgs);
|
|
154
203
|
}
|
|
155
204
|
|
|
156
|
-
/**
|
|
205
|
+
/** Push persisted messages into chat state, synthesizing ids where missing. */
|
|
206
|
+
function pushRestoredMessages(msgs: sessionStore.PersistedMessage[]) {
|
|
207
|
+
for (const m of msgs) {
|
|
208
|
+
chatState.messages.push({
|
|
209
|
+
...m,
|
|
210
|
+
id: m.id || `restored_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`,
|
|
211
|
+
} as (typeof chatState.messages)[number]);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Restore the last-active session once on creation, if any. */
|
|
157
216
|
function restoreChat() {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
// Push into chat state (skips streaming state)
|
|
168
|
-
for (const m of msgs) {
|
|
169
|
-
chatState.messages.push({
|
|
170
|
-
...m,
|
|
171
|
-
id: m.id || `restored_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`,
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
} catch {
|
|
175
|
-
// Corrupt or empty — ignore.
|
|
217
|
+
const root = projectRoot();
|
|
218
|
+
const activeId = sessionStore.getActiveSessionId(root);
|
|
219
|
+
if (!activeId) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const msgs = sessionStore.loadSession(root, activeId);
|
|
223
|
+
if (!msgs || msgs.length === 0) {
|
|
224
|
+
return;
|
|
176
225
|
}
|
|
226
|
+
pushRestoredMessages(msgs);
|
|
227
|
+
sessionId = activeId;
|
|
177
228
|
}
|
|
178
229
|
|
|
179
230
|
// Restore once on creation.
|
|
180
231
|
restoreChat();
|
|
181
232
|
|
|
182
|
-
return {
|
|
233
|
+
return {
|
|
234
|
+
chatState,
|
|
235
|
+
sendMessage,
|
|
236
|
+
stop,
|
|
237
|
+
newChat,
|
|
238
|
+
listSessions,
|
|
239
|
+
openSession,
|
|
240
|
+
deleteSession,
|
|
241
|
+
activeSessionId,
|
|
242
|
+
};
|
|
183
243
|
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared client for the /__studio/import-site NDJSON stream. Every platform's importSite is "a
|
|
3
|
+
* streaming fetch to some URL" — the dev server posts to its own origin, the desktop platforms post
|
|
4
|
+
* to their token-gated loopback servers — so the request/parse/settle logic lives here once.
|
|
5
|
+
*
|
|
6
|
+
* The endpoint emits one JSON object per line: `progress` lines (forwarded to onProgress),
|
|
7
|
+
* `heartbeat` keep-alives (ignored), and a terminal `done` ({root, config}) or `error` line.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ImportProgressEvent, ImportSiteOptions } from "../types";
|
|
11
|
+
import type { ProjectConfig } from "@jxsuite/schema/types";
|
|
12
|
+
|
|
13
|
+
interface StreamLine {
|
|
14
|
+
type: string;
|
|
15
|
+
phase?: string;
|
|
16
|
+
message?: string;
|
|
17
|
+
current?: number;
|
|
18
|
+
total?: number;
|
|
19
|
+
root?: string;
|
|
20
|
+
config?: ProjectConfig;
|
|
21
|
+
error?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* POST an import request and consume its NDJSON progress stream.
|
|
26
|
+
*
|
|
27
|
+
* @param {string} endpoint — the platform's import-site URL (may carry an auth token)
|
|
28
|
+
* @param {ImportSiteOptions} opts — the import request; `apiKey`/`baseUrl` travel as headers
|
|
29
|
+
* @param {(evt: ImportProgressEvent) => void} onProgress
|
|
30
|
+
* @param {AbortSignal} [signal]
|
|
31
|
+
*/
|
|
32
|
+
export async function streamImport(
|
|
33
|
+
endpoint: string,
|
|
34
|
+
opts: ImportSiteOptions,
|
|
35
|
+
onProgress: (evt: ImportProgressEvent) => void,
|
|
36
|
+
signal?: AbortSignal,
|
|
37
|
+
): Promise<{ root: string; config: ProjectConfig }> {
|
|
38
|
+
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
39
|
+
if (opts.apiKey) {
|
|
40
|
+
headers["X-Api-Key"] = opts.apiKey;
|
|
41
|
+
}
|
|
42
|
+
if (opts.baseUrl) {
|
|
43
|
+
headers["X-Api-Base-URL"] = opts.baseUrl;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const res = await fetch(endpoint, {
|
|
47
|
+
body: JSON.stringify({
|
|
48
|
+
url: opts.url,
|
|
49
|
+
directory: opts.directory,
|
|
50
|
+
depth: opts.depth,
|
|
51
|
+
maxPages: opts.maxPages,
|
|
52
|
+
aiComponents: opts.aiComponents,
|
|
53
|
+
...(opts.model === undefined ? {} : { aiModel: opts.model }),
|
|
54
|
+
}),
|
|
55
|
+
headers,
|
|
56
|
+
method: "POST",
|
|
57
|
+
...(signal === undefined ? {} : { signal }),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (!res.ok) {
|
|
61
|
+
let message = `Import failed (${res.status})`;
|
|
62
|
+
try {
|
|
63
|
+
const body = (await res.json()) as { error?: string };
|
|
64
|
+
if (body.error) {
|
|
65
|
+
message = body.error;
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
/* Non-JSON error body — keep the status message. */
|
|
69
|
+
}
|
|
70
|
+
throw new Error(message);
|
|
71
|
+
}
|
|
72
|
+
if (!res.body) {
|
|
73
|
+
throw new Error("Import stream had no response body");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const reader = res.body.getReader();
|
|
77
|
+
const decoder = new TextDecoder();
|
|
78
|
+
let buffer = "";
|
|
79
|
+
let result: { root: string; config: ProjectConfig } | null = null;
|
|
80
|
+
|
|
81
|
+
const handleLine = (line: string) => {
|
|
82
|
+
const trimmed = line.trim();
|
|
83
|
+
if (!trimmed) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
let parsed: StreamLine;
|
|
87
|
+
try {
|
|
88
|
+
parsed = JSON.parse(trimmed) as StreamLine;
|
|
89
|
+
} catch {
|
|
90
|
+
return; // Tolerate a mangled line rather than killing the whole import.
|
|
91
|
+
}
|
|
92
|
+
if (parsed.type === "progress") {
|
|
93
|
+
onProgress({
|
|
94
|
+
phase: parsed.phase ?? "",
|
|
95
|
+
message: parsed.message ?? "",
|
|
96
|
+
...(parsed.current === undefined ? {} : { current: parsed.current }),
|
|
97
|
+
...(parsed.total === undefined ? {} : { total: parsed.total }),
|
|
98
|
+
});
|
|
99
|
+
} else if (parsed.type === "done" && parsed.root !== undefined && parsed.config) {
|
|
100
|
+
result = { root: parsed.root, config: parsed.config };
|
|
101
|
+
} else if (parsed.type === "error") {
|
|
102
|
+
throw new Error(parsed.error || "Import failed");
|
|
103
|
+
}
|
|
104
|
+
// Heartbeats and unknown line types are ignored.
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
for (;;) {
|
|
109
|
+
const { done, value } = await reader.read();
|
|
110
|
+
if (done) {
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
buffer += decoder.decode(value, { stream: true });
|
|
114
|
+
// Lines can split across chunks: keep the trailing partial in the buffer.
|
|
115
|
+
const lines = buffer.split("\n");
|
|
116
|
+
buffer = lines.pop() ?? "";
|
|
117
|
+
for (const line of lines) {
|
|
118
|
+
handleLine(line);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
handleLine(buffer);
|
|
122
|
+
} finally {
|
|
123
|
+
try {
|
|
124
|
+
await reader.cancel();
|
|
125
|
+
} catch {
|
|
126
|
+
/* The stream is already closed (e.g. after an abort). */
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!result) {
|
|
131
|
+
throw new Error("Import stream ended without a result");
|
|
132
|
+
}
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/**
|
|
3
|
+
* Settings-store.js — sync of user settings between localStorage and the platform backend.
|
|
4
|
+
*
|
|
5
|
+
* LocalStorage stays the synchronous source of truth for every consumer (ai-settings getters read
|
|
6
|
+
* it directly). On platforms with a backend user-settings store (desktop/chromium — where
|
|
7
|
+
* localStorage does not roam across browser profiles), the persisted keys are hydrated from the
|
|
8
|
+
* backend at boot and written through on every change. The dev server omits the platform methods
|
|
9
|
+
* and settings remain localStorage-only.
|
|
10
|
+
*
|
|
11
|
+
* @license MIT
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { getPlatform, hasPlatform } from "../platform";
|
|
15
|
+
|
|
16
|
+
/** The localStorage keys mirrored into the platform's user-settings store. */
|
|
17
|
+
export const PERSISTED_SETTINGS_KEYS = [
|
|
18
|
+
"jx.ai.openaiKey",
|
|
19
|
+
"jx.ai.baseUrl",
|
|
20
|
+
"jx.ai.model",
|
|
21
|
+
"jx.cf.token",
|
|
22
|
+
"jx.cf.accountId",
|
|
23
|
+
] as const;
|
|
24
|
+
|
|
25
|
+
/** Read a localStorage value defensively, treating unavailable/throwing storage as empty. */
|
|
26
|
+
function readLocal(key: string): string {
|
|
27
|
+
try {
|
|
28
|
+
return globalThis.localStorage?.getItem(key) ?? "";
|
|
29
|
+
} catch {
|
|
30
|
+
return "";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Pull the persisted settings from the backend store into localStorage. Call once after the
|
|
36
|
+
* platform is registered; a no-op on platforms without a settings store (dev server).
|
|
37
|
+
*
|
|
38
|
+
* Migration: keys the backend does not know yet but localStorage does (an existing user upgrading
|
|
39
|
+
* from localStorage-only builds) are pushed back to the backend once, fire-and-forget.
|
|
40
|
+
*/
|
|
41
|
+
export async function hydrateSettings(): Promise<void> {
|
|
42
|
+
if (!hasPlatform()) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const platform = getPlatform();
|
|
46
|
+
if (!platform.getSettings) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
let stored: Record<string, string>;
|
|
50
|
+
try {
|
|
51
|
+
stored = await platform.getSettings();
|
|
52
|
+
} catch {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const merged: Record<string, string> = { ...stored };
|
|
56
|
+
let needsMigration = false;
|
|
57
|
+
for (const key of PERSISTED_SETTINGS_KEYS) {
|
|
58
|
+
const backendValue = stored[key];
|
|
59
|
+
if (backendValue) {
|
|
60
|
+
try {
|
|
61
|
+
globalThis.localStorage?.setItem(key, backendValue);
|
|
62
|
+
} catch {
|
|
63
|
+
/* Storage unavailable — consumers fall back to defaults. */
|
|
64
|
+
}
|
|
65
|
+
} else {
|
|
66
|
+
const localValue = readLocal(key);
|
|
67
|
+
if (localValue) {
|
|
68
|
+
merged[key] = localValue;
|
|
69
|
+
needsMigration = true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (needsMigration && platform.saveSettings) {
|
|
74
|
+
void platform.saveSettings(merged).catch(() => {});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Write the current values of the persisted keys through to the backend store, fire-and-forget.
|
|
80
|
+
* Call after mutating any of {@link PERSISTED_SETTINGS_KEYS} in localStorage; a no-op on platforms
|
|
81
|
+
* without a settings store (dev server).
|
|
82
|
+
*/
|
|
83
|
+
export function persistSettings(): void {
|
|
84
|
+
if (!hasPlatform()) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const platform = getPlatform();
|
|
88
|
+
if (!platform.saveSettings) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const settings: Record<string, string> = {};
|
|
92
|
+
for (const key of PERSISTED_SETTINGS_KEYS) {
|
|
93
|
+
const value = readLocal(key);
|
|
94
|
+
if (value) {
|
|
95
|
+
settings[key] = value;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
void platform.saveSettings(settings).catch(() => {});
|
|
99
|
+
}
|
package/src/studio.ts
CHANGED
|
@@ -80,6 +80,8 @@ import { startFsSync } from "./files/fs-events";
|
|
|
80
80
|
import { renderImportsTemplate } from "./panels/imports-panel";
|
|
81
81
|
import { renderHeadTemplate } from "./panels/head-panel";
|
|
82
82
|
import { exportCemManifest as _exportCemManifest } from "./services/cem-export";
|
|
83
|
+
import { installAutomationHook } from "./services/automation";
|
|
84
|
+
import { openBrowseModal } from "./browse/browse-modal";
|
|
83
85
|
|
|
84
86
|
import { getPlatform, hasPlatform, registerPlatform } from "./platform";
|
|
85
87
|
import { parseMediaEntries } from "./utils/canvas-media";
|
|
@@ -90,6 +92,7 @@ import { defBadgeLabel, defCategory, renderSignalsTemplate } from "./panels/sign
|
|
|
90
92
|
import { loadComponentRegistry } from "./files/components";
|
|
91
93
|
import { ensureDependenciesInstalled } from "./packages/ensure-deps";
|
|
92
94
|
import { maybePromptJxsuiteUpdate } from "./packages/jxsuite-update";
|
|
95
|
+
import { autoSyncProjectOnOpen } from "./packages/pull-package-sync";
|
|
93
96
|
|
|
94
97
|
import { html, render as litRender } from "lit-html";
|
|
95
98
|
|
|
@@ -123,7 +126,9 @@ import {
|
|
|
123
126
|
} from "./panels/block-action-bar";
|
|
124
127
|
import { initCssData } from "./panels/style-utils";
|
|
125
128
|
import { initQuickSearch } from "./panels/quick-search";
|
|
129
|
+
import { hydrateProjectList } from "./project-list";
|
|
126
130
|
import { addRecentProject, hydrateRecentProjects, removeRecentProject } from "./recent-projects";
|
|
131
|
+
import { hydrateSettings } from "./services/settings-store";
|
|
127
132
|
import { initWelcome } from "./panels/welcome-screen";
|
|
128
133
|
import { openNewProjectModal } from "./new-project/new-project-modal";
|
|
129
134
|
import type { DocumentStackEntry, GitDiffState } from "./types";
|
|
@@ -342,6 +347,18 @@ if (!hasPlatform()) {
|
|
|
342
347
|
registerPlatform(createDevServerPlatform());
|
|
343
348
|
}
|
|
344
349
|
|
|
350
|
+
// Screenshot/automation runners (scripts/screenshots/) await window.__jxAutomation right after
|
|
351
|
+
// Navigation, so the gated hook must install before the async deep-link project load below.
|
|
352
|
+
installAutomationHook({
|
|
353
|
+
getCanvasMode,
|
|
354
|
+
openBrowseModal,
|
|
355
|
+
openNewProjectModal,
|
|
356
|
+
render,
|
|
357
|
+
renderActivityBar,
|
|
358
|
+
setCanvasMode,
|
|
359
|
+
statusMessage,
|
|
360
|
+
});
|
|
361
|
+
|
|
345
362
|
mountResizeEdges();
|
|
346
363
|
|
|
347
364
|
// ─── Render loop ──────────────────────────────────────────────────────────────
|
|
@@ -619,6 +636,7 @@ if (_projectParam) {
|
|
|
619
636
|
selectedPath: siteCtx.fileRelPath || null,
|
|
620
637
|
});
|
|
621
638
|
|
|
639
|
+
await autoSyncProjectOnOpen();
|
|
622
640
|
await ensureDependenciesInstalled();
|
|
623
641
|
await loadComponentRegistry();
|
|
624
642
|
|
|
@@ -727,6 +745,20 @@ void hydrateRecentProjects().then(() => {
|
|
|
727
745
|
render();
|
|
728
746
|
});
|
|
729
747
|
|
|
748
|
+
// Hydrate the platform's project catalogue (dev server sites, cloud projects), then refresh the
|
|
749
|
+
// Welcome screen, which reads it synchronously. No-op on platforms without listProjects.
|
|
750
|
+
// oxlint-disable-next-line unicorn/prefer-top-level-await -- deliberate fire-and-forget: hydration must not block initial render
|
|
751
|
+
void hydrateProjectList().then(() => {
|
|
752
|
+
render();
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
// Hydrate user settings (AI connection parameters) from the backend store, then re-render so
|
|
756
|
+
// Key-gated surfaces (assistant gate, New Project Import/Agent tabs) see the stored key.
|
|
757
|
+
// oxlint-disable-next-line unicorn/prefer-top-level-await -- deliberate fire-and-forget: hydration must not block initial render
|
|
758
|
+
void hydrateSettings().then(() => {
|
|
759
|
+
render();
|
|
760
|
+
});
|
|
761
|
+
|
|
730
762
|
// ─── Left panel: delegated to panels/left-panel.js ───────────────────────────
|
|
731
763
|
|
|
732
764
|
function renderLeftPanel() {
|
|
@@ -787,6 +819,7 @@ async function openRecentProject(root: string) {
|
|
|
787
819
|
selectedPath: null,
|
|
788
820
|
});
|
|
789
821
|
|
|
822
|
+
await autoSyncProjectOnOpen();
|
|
790
823
|
await ensureDependenciesInstalled();
|
|
791
824
|
await loadDirectory(".");
|
|
792
825
|
await loadComponentRegistry();
|