@jxsuite/studio 0.33.0 → 0.35.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 +6238 -0
- package/dist/iframe-entry.js.map +35 -0
- package/dist/studio.js +30972 -17222
- package/dist/studio.js.map +328 -83
- package/package.json +9 -9
- package/src/browse/browse.ts +11 -4
- package/src/canvas/canvas-helpers.ts +2 -56
- package/src/canvas/canvas-live-render.ts +102 -435
- package/src/canvas/canvas-origin.ts +66 -0
- package/src/canvas/canvas-patcher.ts +63 -403
- package/src/canvas/canvas-render.ts +70 -212
- package/src/canvas/canvas-utils.ts +37 -65
- package/src/canvas/iframe-channel.ts +154 -0
- package/src/canvas/iframe-drop.ts +484 -0
- package/src/canvas/iframe-entry.ts +600 -0
- package/src/canvas/iframe-host.ts +1373 -0
- package/src/canvas/iframe-inline-edit.ts +367 -0
- package/src/canvas/iframe-insert.ts +164 -0
- package/src/canvas/iframe-interaction.ts +176 -0
- package/src/canvas/iframe-keys.ts +85 -0
- package/src/canvas/iframe-overlay.ts +218 -0
- package/src/canvas/iframe-patch.ts +363 -0
- package/src/canvas/iframe-protocol.ts +361 -0
- package/src/canvas/iframe-render.ts +458 -0
- package/src/canvas/iframe-slash.ts +114 -0
- package/src/canvas/iframe-subtree.ts +113 -0
- package/src/canvas/path-mapping.ts +86 -0
- package/src/canvas/serialize-scope.ts +65 -0
- package/src/editor/canvas-context-menu.ts +40 -0
- package/src/editor/canvas-slash-bridge.ts +21 -0
- package/src/editor/context-menu.ts +2 -1
- package/src/editor/inline-edit-apply.ts +183 -0
- package/src/editor/inline-edit.ts +99 -21
- package/src/editor/inline-link.ts +89 -0
- package/src/editor/insert-zone-action.ts +35 -0
- package/src/editor/merge-tags.ts +26 -2
- package/src/editor/repeater-scope.ts +144 -0
- package/src/editor/shortcuts.ts +14 -28
- package/src/editor/slash-menu.ts +73 -42
- package/src/files/files.ts +5 -1
- 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/page-params.ts +383 -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 +189 -457
- package/src/panels/block-action-bar.ts +296 -138
- package/src/panels/canvas-dnd-bridge.ts +397 -0
- package/src/panels/component-preview.ts +56 -0
- package/src/panels/dnd.ts +41 -17
- package/src/panels/drag-ghost.ts +62 -0
- package/src/panels/editors.ts +1 -1
- package/src/panels/git-panel.ts +16 -1
- package/src/panels/overlays.ts +10 -125
- package/src/panels/properties-panel.ts +210 -0
- package/src/panels/right-panel.ts +21 -7
- package/src/panels/signals-panel.ts +136 -22
- package/src/panels/stylebook-doc.ts +373 -0
- package/src/panels/stylebook-panel.ts +46 -689
- package/src/panels/tab-bar.ts +159 -13
- package/src/panels/toolbar.ts +3 -2
- package/src/platforms/devserver.ts +45 -1
- package/src/services/agent-seed.ts +91 -0
- package/src/services/ai-models.ts +64 -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/document-assistant.ts +98 -38
- package/src/services/import-client.ts +134 -0
- package/src/services/monaco-setup.ts +12 -0
- package/src/services/render-critic.ts +9 -9
- package/src/services/settings-store.ts +93 -0
- package/src/settings/css-vars-editor.ts +2 -2
- package/src/store.ts +4 -62
- package/src/studio.ts +115 -40
- package/src/tabs/doc-op-apply.ts +89 -0
- package/src/tabs/patch-ops.ts +6 -2
- package/src/tabs/tab.ts +23 -4
- package/src/tabs/transact.ts +2 -74
- package/src/types.ts +98 -18
- package/src/ui/ai-credentials-form.ts +205 -0
- package/src/ui/jx-theme.ts +63 -0
- package/src/ui/media-picker.ts +6 -4
- package/src/ui/spectrum.ts +13 -0
- package/src/utils/canvas-media.ts +0 -137
- package/src/utils/edit-display.ts +23 -3
- package/src/utils/geometry.ts +43 -0
- package/src/utils/link-target.ts +93 -0
- package/src/utils/strip-events.ts +54 -0
- package/src/view.ts +0 -23
- package/src/workspace/workspace.ts +14 -1
- package/src/canvas/canvas-subtree-render.ts +0 -113
- package/src/editor/component-inline-edit.ts +0 -349
- package/src/editor/content-inline-edit.ts +0 -207
- package/src/editor/insertion-helper.ts +0 -308
- package/src/panels/canvas-dnd.ts +0 -329
- package/src/panels/panel-events.ts +0 -306
- package/src/panels/preview-render.ts +0 -132
- package/src/panels/pseudo-preview.ts +0 -75
package/src/panels/tab-bar.ts
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
/// <reference lib="dom" />
|
|
2
2
|
/**
|
|
3
|
-
* Tab bar — a per-tab
|
|
4
|
-
* Standardizes Back/breadcrumb navigation,
|
|
5
|
-
*
|
|
3
|
+
* Tab bar — a persistent per-tab settings bar rendered between the tab strip and the edit content.
|
|
4
|
+
* Standardizes Back/breadcrumb navigation, the view settings cluster (preview toggle, layout
|
|
5
|
+
* visibility, dynamic route-param pickers — shown in the edit/design base modes), media feature
|
|
6
|
+
* toggles, and mode actions (Code-mode Export) into a single bar shared by every edit mode.
|
|
6
7
|
*
|
|
7
8
|
* Follows the same module shape as tab-strip.ts: mount(host, ctx) → effectScope/effect → render().
|
|
8
|
-
* The bar
|
|
9
|
-
* contextual to show.
|
|
9
|
+
* The bar renders `nothing` (so `#tab-bar:empty` hides the row) only when no tab is active.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { html, render as litRender, nothing } from "lit-html";
|
|
13
|
-
import { updateUi } from "../store";
|
|
13
|
+
import { projectState, updateUi } from "../store";
|
|
14
14
|
import { effect, effectScope } from "../reactivity";
|
|
15
15
|
import { activeTab } from "../workspace/workspace";
|
|
16
|
-
import { getEffectiveMedia } from "../site-context";
|
|
16
|
+
import { getEffectiveLayoutPath, getEffectiveMedia } from "../site-context";
|
|
17
|
+
import { dynamicRouteParams, loadParamValues, pagePathsDef } from "../page-params";
|
|
17
18
|
import { mediaDisplayName } from "./shared";
|
|
19
|
+
import type { ParamValues } from "../page-params";
|
|
20
|
+
import type { Tab } from "../tabs/tab";
|
|
18
21
|
import type { DocumentStackEntry, FunctionEditDef } from "../types";
|
|
19
22
|
import type { EffectScope } from "@vue/reactivity";
|
|
20
23
|
import type { TemplateResult } from "lit-html";
|
|
@@ -59,12 +62,16 @@ export function mount(host: HTMLElement, ctx: TabBarCtx) {
|
|
|
59
62
|
if (tab) {
|
|
60
63
|
// Read reactive properties to establish tracking — mirrors the toolbar's subset
|
|
61
64
|
void tab.doc.document;
|
|
65
|
+
void tab.doc.document?.$layout;
|
|
62
66
|
void tab.doc.mode;
|
|
63
67
|
void tab.documentPath;
|
|
64
68
|
void tab.session.documentStack.length;
|
|
65
69
|
void tab.session.ui.canvasMode;
|
|
66
70
|
void tab.session.ui.editingFunction;
|
|
67
71
|
void tab.session.ui.featureToggles;
|
|
72
|
+
void tab.session.ui.preview;
|
|
73
|
+
void tab.session.ui.previewParams;
|
|
74
|
+
void tab.session.ui.showLayout;
|
|
68
75
|
}
|
|
69
76
|
render();
|
|
70
77
|
});
|
|
@@ -143,6 +150,55 @@ function tabBarTemplate(ctx: TabBarCtx): TemplateResult | typeof nothing {
|
|
|
143
150
|
`;
|
|
144
151
|
}
|
|
145
152
|
|
|
153
|
+
// ── Right region: view settings cluster (edit/design base modes only) ──
|
|
154
|
+
// Gates on the BASE mode (not the effective mode): the cluster stays visible while the preview
|
|
155
|
+
// Toggle is on so it can be toggled back off.
|
|
156
|
+
const baseMode = S.ui.canvasMode;
|
|
157
|
+
const isPage = Boolean(
|
|
158
|
+
S.documentPath &&
|
|
159
|
+
projectState?.isSiteProject &&
|
|
160
|
+
(S.documentPath.startsWith("pages/") || S.documentPath.startsWith("./pages/")),
|
|
161
|
+
);
|
|
162
|
+
let settingsTpl: TemplateResult | typeof nothing = nothing;
|
|
163
|
+
if (!editing && (baseMode === "edit" || baseMode === "design")) {
|
|
164
|
+
const canPreview = tab.capabilities.modes.includes("preview");
|
|
165
|
+
const hasLayout = isPage && Boolean(getEffectiveLayoutPath(S.document?.$layout));
|
|
166
|
+
const pickersTpl = isPage ? paramPickersTpl(tab) : nothing;
|
|
167
|
+
const previewTpl = canPreview
|
|
168
|
+
? html`
|
|
169
|
+
<sp-action-button
|
|
170
|
+
toggles
|
|
171
|
+
size="s"
|
|
172
|
+
title="Preview resolved values"
|
|
173
|
+
?selected=${Boolean(S.ui.preview)}
|
|
174
|
+
@click=${() => updateUi("preview", !S.ui.preview)}
|
|
175
|
+
>
|
|
176
|
+
<sp-icon-preview slot="icon"></sp-icon-preview>
|
|
177
|
+
Preview
|
|
178
|
+
</sp-action-button>
|
|
179
|
+
`
|
|
180
|
+
: nothing;
|
|
181
|
+
const layoutTpl = hasLayout
|
|
182
|
+
? html`
|
|
183
|
+
<sp-action-button
|
|
184
|
+
toggles
|
|
185
|
+
size="s"
|
|
186
|
+
title="Show layout elements"
|
|
187
|
+
?selected=${S.ui.showLayout !== false}
|
|
188
|
+
@click=${() => updateUi("showLayout", S.ui.showLayout === false)}
|
|
189
|
+
>
|
|
190
|
+
Layout
|
|
191
|
+
</sp-action-button>
|
|
192
|
+
`
|
|
193
|
+
: nothing;
|
|
194
|
+
if (pickersTpl !== nothing || previewTpl !== nothing || layoutTpl !== nothing) {
|
|
195
|
+
settingsTpl = html`
|
|
196
|
+
${pickersTpl}
|
|
197
|
+
<sp-action-group compact size="s">${layoutTpl} ${previewTpl}</sp-action-group>
|
|
198
|
+
`;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
146
202
|
// ── Right region: media feature toggles ──
|
|
147
203
|
const { featureQueries } = ctx.parseMediaEntries(getEffectiveMedia(S.document?.$media));
|
|
148
204
|
const togglesTpl =
|
|
@@ -183,16 +239,106 @@ function tabBarTemplate(ctx: TabBarCtx): TemplateResult | typeof nothing {
|
|
|
183
239
|
`
|
|
184
240
|
: nothing;
|
|
185
241
|
|
|
186
|
-
// Collapse the bar entirely when there is nothing contextual to show.
|
|
187
|
-
if (navTpl === nothing && togglesTpl === nothing && exportTpl === nothing) {
|
|
188
|
-
return nothing;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
242
|
return html`
|
|
192
243
|
<div class="tab-bar">
|
|
193
244
|
${navTpl}
|
|
194
245
|
<div class="tb-spacer"></div>
|
|
195
|
-
${togglesTpl} ${exportTpl}
|
|
246
|
+
${settingsTpl} ${togglesTpl} ${exportTpl}
|
|
196
247
|
</div>
|
|
197
248
|
`;
|
|
198
249
|
}
|
|
250
|
+
|
|
251
|
+
// ── Dynamic route-param pickers ──────────────────────────────────────────────
|
|
252
|
+
// Candidate values load asynchronously (ContentCollection resolution / data-file read); the module
|
|
253
|
+
// Caches the last result per (documentPath, $paths) and re-renders when it lands — the same lazy
|
|
254
|
+
// Fill pattern as head-panel's loadLayoutEntries. When values arrive, any param without a chosen
|
|
255
|
+
// Value auto-selects the first candidate (matching the compiler, whose first expanded route is the
|
|
256
|
+
// First path entry).
|
|
257
|
+
|
|
258
|
+
let _paramValues: ParamValues | null = null;
|
|
259
|
+
|
|
260
|
+
let _paramValuesKey: string | null = null;
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* @param {Tab} tab
|
|
264
|
+
* @returns {ParamValues | null} — null while loading (or when the doc declares no params)
|
|
265
|
+
*/
|
|
266
|
+
function paramValuesFor(tab: Tab): ParamValues | null {
|
|
267
|
+
const pathsDef = pagePathsDef({
|
|
268
|
+
document: tab.doc.document,
|
|
269
|
+
frontmatter: tab.doc.content.frontmatter,
|
|
270
|
+
});
|
|
271
|
+
if (!pathsDef && dynamicRouteParams(tab.documentPath).length === 0) {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
const key = `${tab.documentPath ?? ""}::${JSON.stringify(pathsDef)}`;
|
|
275
|
+
if (_paramValuesKey === key) {
|
|
276
|
+
return _paramValues;
|
|
277
|
+
}
|
|
278
|
+
_paramValuesKey = key;
|
|
279
|
+
_paramValues = null;
|
|
280
|
+
void loadParamValues(tab.documentPath, pathsDef).then((values) => {
|
|
281
|
+
if (_paramValuesKey !== key || activeTab.value !== tab) {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
_paramValues = values;
|
|
285
|
+
autoSelectParams(tab, values);
|
|
286
|
+
render();
|
|
287
|
+
});
|
|
288
|
+
return _paramValues;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* @param {Tab} tab
|
|
293
|
+
* @param {ParamValues} values
|
|
294
|
+
*/
|
|
295
|
+
function autoSelectParams(tab: Tab, values: ParamValues) {
|
|
296
|
+
const current = tab.session.ui.previewParams ?? {};
|
|
297
|
+
const additions: Record<string, string> = {};
|
|
298
|
+
for (const [name, list] of Object.entries(values)) {
|
|
299
|
+
if (!current[name] && list.length > 0) {
|
|
300
|
+
additions[name] = list[0]!;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (Object.keys(additions).length > 0) {
|
|
304
|
+
updateUi("previewParams", { ...current, ...additions });
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* @param {Tab} tab
|
|
310
|
+
* @returns {TemplateResult | typeof nothing}
|
|
311
|
+
*/
|
|
312
|
+
function paramPickersTpl(tab: Tab): TemplateResult | typeof nothing {
|
|
313
|
+
const values = paramValuesFor(tab);
|
|
314
|
+
const names = new Set(dynamicRouteParams(tab.documentPath));
|
|
315
|
+
for (const name of Object.keys(values ?? {})) {
|
|
316
|
+
names.add(name);
|
|
317
|
+
}
|
|
318
|
+
if (names.size === 0) {
|
|
319
|
+
return nothing;
|
|
320
|
+
}
|
|
321
|
+
const { previewParams } = tab.session.ui;
|
|
322
|
+
return html`
|
|
323
|
+
${[...names].map(
|
|
324
|
+
(name) => html`
|
|
325
|
+
<sp-picker
|
|
326
|
+
size="s"
|
|
327
|
+
quiet
|
|
328
|
+
class="tab-bar-param"
|
|
329
|
+
label=${name}
|
|
330
|
+
title=${`Preview value for [${name}]`}
|
|
331
|
+
value=${previewParams?.[name] ?? ""}
|
|
332
|
+
@change=${(e: Event) => {
|
|
333
|
+
const { value } = e.target as HTMLInputElement;
|
|
334
|
+
updateUi("previewParams", { ...tab.session.ui.previewParams, [name]: value });
|
|
335
|
+
}}
|
|
336
|
+
>
|
|
337
|
+
${(values?.[name] ?? []).map(
|
|
338
|
+
(v: string) => html`<sp-menu-item value=${v}>${v}</sp-menu-item>`,
|
|
339
|
+
)}
|
|
340
|
+
</sp-picker>
|
|
341
|
+
`,
|
|
342
|
+
)}
|
|
343
|
+
`;
|
|
344
|
+
}
|
package/src/panels/toolbar.ts
CHANGED
|
@@ -305,7 +305,6 @@ function minimalToolbarTemplate(ctx: ToolbarCtx) {
|
|
|
305
305
|
const modes = [
|
|
306
306
|
{ iconTag: "sp-icon-edit", key: "edit", label: "Edit" },
|
|
307
307
|
{ iconTag: "sp-icon-artboard", key: "design", label: "Design" },
|
|
308
|
-
{ iconTag: "sp-icon-preview", key: "preview", label: "Preview" },
|
|
309
308
|
{ iconTag: "sp-icon-code", key: "source", label: "Code" },
|
|
310
309
|
{ iconTag: "sp-icon-brush", key: "stylebook", label: "Stylebook" },
|
|
311
310
|
];
|
|
@@ -333,7 +332,9 @@ function toolbarTemplate() {
|
|
|
333
332
|
selection: tab.session.selection,
|
|
334
333
|
ui: tab.session.ui,
|
|
335
334
|
};
|
|
336
|
-
|
|
335
|
+
// Base mode, not the effective mode: the switcher keeps Edit/Design highlighted while the
|
|
336
|
+
// Tab-bar preview toggle is on (preview is no longer a switchable mode).
|
|
337
|
+
const { canvasMode } = tab.session.ui;
|
|
337
338
|
|
|
338
339
|
const modeSwitcherTpl = html`
|
|
339
340
|
<sp-action-group selects="single" size="s" compact>
|
|
@@ -8,8 +8,16 @@
|
|
|
8
8
|
* See spec/desktop.md §8 for the full specification.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { streamImport } from "../services/import-client";
|
|
11
12
|
import type { ProjectConfig } from "@jxsuite/schema/types";
|
|
12
|
-
import type {
|
|
13
|
+
import type {
|
|
14
|
+
DirEntry,
|
|
15
|
+
FsEvent,
|
|
16
|
+
ImportProgressEvent,
|
|
17
|
+
ImportSiteOptions,
|
|
18
|
+
RenameResult,
|
|
19
|
+
StarterInfo,
|
|
20
|
+
} from "../types";
|
|
13
21
|
|
|
14
22
|
/** A directory entry from the server, tolerating extra wire fields. */
|
|
15
23
|
type WireDirEntry = DirEntry & Record<string, unknown>;
|
|
@@ -215,6 +223,9 @@ export function createDevServerPlatform() {
|
|
|
215
223
|
url?: string;
|
|
216
224
|
adapter?: string;
|
|
217
225
|
directory: string;
|
|
226
|
+
starter?: string;
|
|
227
|
+
template?: string;
|
|
228
|
+
design?: Record<string, unknown>;
|
|
218
229
|
}) {
|
|
219
230
|
const res = await fetch("/__studio/create-project", {
|
|
220
231
|
body: JSON.stringify(opts),
|
|
@@ -228,6 +239,24 @@ export function createDevServerPlatform() {
|
|
|
228
239
|
return await res.json();
|
|
229
240
|
},
|
|
230
241
|
|
|
242
|
+
/** List starter templates from the dev server. */
|
|
243
|
+
async listStarters(): Promise<StarterInfo[]> {
|
|
244
|
+
const res = await fetch("/__studio/starters");
|
|
245
|
+
if (!res.ok) {
|
|
246
|
+
throw new Error("Failed to load starters");
|
|
247
|
+
}
|
|
248
|
+
return await readJson<StarterInfo[]>(res);
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
/** AI-guided site import: NDJSON progress stream from the dev server's import endpoint. */
|
|
252
|
+
async importSite(
|
|
253
|
+
opts: ImportSiteOptions,
|
|
254
|
+
onProgress: (evt: ImportProgressEvent) => void,
|
|
255
|
+
signal?: AbortSignal,
|
|
256
|
+
) {
|
|
257
|
+
return await streamImport("/__studio/import-site", opts, onProgress, signal);
|
|
258
|
+
},
|
|
259
|
+
|
|
231
260
|
// ─── File operations ──────────────────────────────────────────────────
|
|
232
261
|
|
|
233
262
|
/** @param {string} dir */
|
|
@@ -588,6 +617,21 @@ export function createDevServerPlatform() {
|
|
|
588
617
|
return schema;
|
|
589
618
|
},
|
|
590
619
|
|
|
620
|
+
// ─── Class resolution (dev-proxy) ─────────────────────────────────────
|
|
621
|
+
|
|
622
|
+
/** @param {Record<string, unknown>} body */
|
|
623
|
+
async resolveClass(body: Record<string, unknown>) {
|
|
624
|
+
const res = await fetch("/__jx_resolve__", {
|
|
625
|
+
body: JSON.stringify(body),
|
|
626
|
+
headers: { "content-type": "application/json" },
|
|
627
|
+
method: "POST",
|
|
628
|
+
});
|
|
629
|
+
if (!res.ok) {
|
|
630
|
+
throw new Error(`Class resolution failed: ${res.status}`);
|
|
631
|
+
}
|
|
632
|
+
return await readJson<unknown>(res);
|
|
633
|
+
},
|
|
634
|
+
|
|
591
635
|
// ─── Git operations ──────────────────────────────────────────────────
|
|
592
636
|
|
|
593
637
|
async gitStatus() {
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/**
|
|
3
|
+
* Agent-seed.ts — cross-window handoff of a pending AI assistant prompt.
|
|
4
|
+
*
|
|
5
|
+
* The New Project flow stores the user's project brief here; the Studio window that opens the
|
|
6
|
+
* project consumes it and seeds the Assistant tab. localStorage (not module state) is deliberate:
|
|
7
|
+
* on desktop the new project opens in a NEW window, and the per-root localStorage key is the
|
|
8
|
+
* cross-window channel (precedent: the chat-history key `jx-ai-chat-history:<root>`).
|
|
9
|
+
*
|
|
10
|
+
* @license MIT
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const PENDING_PROMPT_PREFIX = "jx.ai.pendingAgentPrompt";
|
|
14
|
+
|
|
15
|
+
/** Entries older than this are considered stale and dropped on read. */
|
|
16
|
+
const MAX_AGE_MS = 15 * 60 * 1000;
|
|
17
|
+
|
|
18
|
+
function storageKey(root: string) {
|
|
19
|
+
return `${PENDING_PROMPT_PREFIX}:${root}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Read the pending entry for a project root, dropping stale or malformed entries.
|
|
24
|
+
*
|
|
25
|
+
* @param {string} root
|
|
26
|
+
* @returns {{ prompt: string; ts: number } | null}
|
|
27
|
+
*/
|
|
28
|
+
function readEntry(root: string): { prompt: string; ts: number } | null {
|
|
29
|
+
try {
|
|
30
|
+
const raw = globalThis.localStorage?.getItem(storageKey(root));
|
|
31
|
+
if (!raw) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const parsed = JSON.parse(raw) as { prompt?: unknown; ts?: unknown };
|
|
35
|
+
const valid =
|
|
36
|
+
typeof parsed.prompt === "string" &&
|
|
37
|
+
typeof parsed.ts === "number" &&
|
|
38
|
+
Date.now() - parsed.ts <= MAX_AGE_MS;
|
|
39
|
+
if (!valid) {
|
|
40
|
+
// Stale or malformed — remove so it never seeds a later session.
|
|
41
|
+
globalThis.localStorage?.removeItem(storageKey(root));
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
return { prompt: parsed.prompt as string, ts: parsed.ts as number };
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Store a pending assistant prompt for the project at `root`.
|
|
52
|
+
*
|
|
53
|
+
* @param {string} root
|
|
54
|
+
* @param {string} prompt
|
|
55
|
+
*/
|
|
56
|
+
export function setPendingAgentPrompt(root: string, prompt: string): void {
|
|
57
|
+
try {
|
|
58
|
+
globalThis.localStorage?.setItem(storageKey(root), JSON.stringify({ prompt, ts: Date.now() }));
|
|
59
|
+
} catch {
|
|
60
|
+
/* LocalStorage unavailable — the prompt is simply not handed off. */
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Whether a fresh (younger than 15 min) pending prompt exists for `root`.
|
|
66
|
+
*
|
|
67
|
+
* @param {string} root
|
|
68
|
+
*/
|
|
69
|
+
export function hasPendingAgentPrompt(root: string): boolean {
|
|
70
|
+
return readEntry(root) !== null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Read AND delete the pending prompt for `root` (consume-on-read keeps repeated renders
|
|
75
|
+
* idempotent). Returns null when there is no fresh entry.
|
|
76
|
+
*
|
|
77
|
+
* @param {string} root
|
|
78
|
+
* @returns {string | null}
|
|
79
|
+
*/
|
|
80
|
+
export function consumePendingAgentPrompt(root: string): string | null {
|
|
81
|
+
const entry = readEntry(root);
|
|
82
|
+
if (!entry) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
globalThis.localStorage?.removeItem(storageKey(root));
|
|
87
|
+
} catch {
|
|
88
|
+
/* LocalStorage unavailable — nothing to delete. */
|
|
89
|
+
}
|
|
90
|
+
return entry.prompt;
|
|
91
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/**
|
|
3
|
+
* Ai-models.js — available-model listing from the studio AI proxy.
|
|
4
|
+
*
|
|
5
|
+
* Fetches the proxy's /models endpoint (the chat endpoint's sibling), forwarding the
|
|
6
|
+
* stored API key as X-Api-Key and the endpoint override as X-Api-Base-URL so the proxy
|
|
7
|
+
* lists models from the user's chosen provider. Shared by the credentials form and the
|
|
8
|
+
* chat composer's model picker; results are cached module-wide until invalidated (e.g.
|
|
9
|
+
* after credentials change).
|
|
10
|
+
*
|
|
11
|
+
* @license MIT
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { getPlatform } from "../platform";
|
|
15
|
+
import { getBaseUrl, getOpenAiKey } from "./ai-settings";
|
|
16
|
+
|
|
17
|
+
export interface AiModel {
|
|
18
|
+
id: string;
|
|
19
|
+
name: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let cache: AiModel[] | null = null;
|
|
23
|
+
|
|
24
|
+
/** Drop the cached model list (call after credentials/endpoint changes). */
|
|
25
|
+
export function invalidateModelCache() {
|
|
26
|
+
cache = null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Fetch the models available through the AI proxy. Returns the cached list when present unless
|
|
31
|
+
* `force` is set; throws on HTTP/network failure (callers surface the error).
|
|
32
|
+
*
|
|
33
|
+
* @param {{ apiKey?: string; baseUrl?: string; force?: boolean }} [opts] - Credential overrides for
|
|
34
|
+
* drafts not yet saved to ai-settings.
|
|
35
|
+
* @returns {Promise<AiModel[]>}
|
|
36
|
+
*/
|
|
37
|
+
export async function fetchAvailableModels(
|
|
38
|
+
opts: { apiKey?: string; baseUrl?: string; force?: boolean } = {},
|
|
39
|
+
): Promise<AiModel[]> {
|
|
40
|
+
if (cache && !opts.force) {
|
|
41
|
+
return cache;
|
|
42
|
+
}
|
|
43
|
+
const plat = getPlatform();
|
|
44
|
+
const chatUrl = await Promise.resolve(plat.aiChatUrl());
|
|
45
|
+
const modelsUrl = chatUrl.replace(/\/chat$/, "/models");
|
|
46
|
+
|
|
47
|
+
const headers: Record<string, string> = {};
|
|
48
|
+
const apiKey = opts.apiKey || getOpenAiKey();
|
|
49
|
+
if (apiKey) {
|
|
50
|
+
headers["X-Api-Key"] = apiKey;
|
|
51
|
+
}
|
|
52
|
+
const baseUrl = opts.baseUrl || getBaseUrl();
|
|
53
|
+
if (baseUrl) {
|
|
54
|
+
headers["X-Api-Base-URL"] = baseUrl;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const resp = await fetch(modelsUrl, { headers });
|
|
58
|
+
if (!resp.ok) {
|
|
59
|
+
throw new Error(`HTTP ${resp.status}`);
|
|
60
|
+
}
|
|
61
|
+
const data = (await resp.json()) as { models?: { id: string; name?: string }[] };
|
|
62
|
+
cache = (data.models || []).map((m) => ({ id: m.id, name: m.name || m.id }));
|
|
63
|
+
return cache;
|
|
64
|
+
}
|