@jxsuite/studio 2.4.3 → 3.0.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/chunks/{cf-settings-g5fbf2zp.js → cf-settings-zj2cj1ar.js} +6 -3
- package/dist/chunks/{content-source-k9724vcv.js → content-source-7fy9qrm0.js} +10 -9
- package/dist/chunks/{convert-to-repeater-k10dvcc7.js → convert-to-repeater-6vvx96n2.js} +9 -8
- package/dist/chunks/{extension-sections-0ada0xcw.js → extension-sections-kwzvpemr.js} +13 -12
- package/dist/chunks/{files-yew27sn9.js → files-xcybanxe.js} +14 -13
- package/dist/chunks/{format-host-wewba4nq.js → format-host-8fr3545e.js} +2 -2
- package/dist/chunks/{git-panel-2dh1aw50.js → git-panel-g01d93vc.js} +9 -8
- package/dist/chunks/{layers-panel-qnsghwwd.js → layers-panel-27essgkc.js} +12 -11
- package/dist/chunks/{publish-panel-a0pdft5w.js → publish-panel-09sy2bab.js} +8 -7
- package/dist/chunks/{quick-search-9avyc4pc.js → quick-search-sefb61v8.js} +15 -14
- package/dist/chunks/{right-panel-kcnhtc5j.js → right-panel-9r32jd5j.js} +11 -10
- package/dist/chunks/{studio-aweyr4v1.js → studio-2x9zbxmx.js} +2 -2
- package/dist/chunks/{studio-ef8t53vc.js → studio-3dkcd4c5.js} +3 -3
- package/dist/chunks/{studio-hb7c46jy.js → studio-5efrtn1m.js} +9 -7
- package/dist/chunks/{studio-v1va0vhd.js → studio-6z5eyjwn.js} +24 -25
- package/dist/chunks/{studio-ke244pam.js → studio-7p35254k.js} +33 -11
- package/dist/chunks/{studio-f5yyvqxv.js → studio-8ezdegcx.js} +13 -11
- package/dist/chunks/{studio-cz2j1c0g.js → studio-a0yp7syn.js} +55 -18
- package/dist/chunks/{studio-26sv44br.js → studio-dc8s8heb.js} +34 -23
- package/dist/chunks/{studio-jnakhg87.js → studio-g5gdqnyd.js} +4 -4
- package/dist/chunks/{studio-rqemfhz6.js → studio-hcxzyk3x.js} +2 -2
- package/dist/chunks/{studio-ws6ef6js.js → studio-hj2se583.js} +12 -10
- package/dist/chunks/{studio-pyrw8h0w.js → studio-j595e748.js} +407 -167
- package/dist/chunks/{studio-dgj7gq44.js → studio-k4jvjcfx.js} +4 -4
- package/dist/chunks/studio-kfpek536.js +28 -0
- package/dist/chunks/{studio-sccge2wf.js → studio-p5dja96s.js} +2 -2
- package/dist/chunks/{studio-0b6fs05q.js → studio-pkp61mh8.js} +5 -5
- package/dist/chunks/studio-qpfq7xhm.js +1802 -0
- package/dist/chunks/{studio-ra7ar50f.js → studio-r5xbtctf.js} +51 -1565
- package/dist/chunks/{studio-v2c5pmjj.js → studio-rh1cnamv.js} +17 -15
- package/dist/chunks/{studio-28xc8ws4.js → studio-ry1vz5sv.js} +10 -8
- package/dist/chunks/{studio-hyn3vhwr.js → studio-xntfdy1x.js} +5 -5
- package/dist/iframe-entry.js +344 -26
- package/dist/studio.js +274 -247
- package/package.json +9 -9
- package/src/browse/library-layouts.ts +3 -3
- package/src/browse/library-pane.ts +2 -2
- package/src/browse/library-preview.ts +19 -3
- package/src/canvas/asset-refs.ts +119 -0
- package/src/canvas/asset-resolve.ts +256 -0
- package/src/canvas/canvas-live-render.ts +23 -15
- package/src/canvas/iframe-entry.ts +1 -0
- package/src/canvas/iframe-host.ts +4 -0
- package/src/canvas/iframe-protocol.ts +9 -0
- package/src/canvas/iframe-render.ts +28 -10
- package/src/canvas/site-style-css.ts +2 -6
- package/src/editor/file-drop-action.ts +2 -1
- package/src/files/files.ts +2 -2
- package/src/files/media-paths.ts +37 -14
- package/src/files/media-upload.ts +47 -6
- package/src/grid/grid-idle.ts +45 -0
- package/src/grid/grid-view.ts +21 -0
- package/src/grid/schema-columns.ts +2 -1
- package/src/grid/sources/content-source.ts +24 -3
- package/src/new-project/import-tab.ts +3 -2
- package/src/panels/ai-chat/composer.ts +30 -13
- package/src/panels/frontmatter-fields.ts +6 -2
- package/src/panels/properties-panel.ts +8 -2
- package/src/panels/seo-modal.ts +5 -6
- package/src/panels/signals-panel.ts +2 -1
- package/src/platforms/cloud.ts +14 -3
- package/src/platforms/devserver.ts +5 -1
- package/src/services/ai-models.ts +101 -11
- package/src/services/ai-settings.ts +69 -71
- package/src/services/cf-settings.ts +19 -37
- package/src/services/document-assistant.ts +4 -3
- package/src/services/idle.ts +6 -1
- package/src/services/settings/definitions.ts +164 -0
- package/src/services/settings/kernel.ts +335 -0
- package/src/services/settings/write-queue.ts +116 -0
- package/src/settings/preferences-accounts.ts +6 -19
- package/src/settings/preferences-keymap.ts +8 -18
- package/src/settings/schema-field-ui.ts +2 -2
- package/src/shell.ts +6 -14
- package/src/studio.ts +32 -1
- package/src/types.ts +61 -3
- package/src/ui/ai-credentials-form.ts +52 -27
- package/src/ui/media-picker.ts +6 -11
- package/src/utils/studio-utils.ts +45 -3
- package/dist/chunks/studio-a7pzveq1.js +0 -110
- package/src/canvas/content-assets.ts +0 -263
- package/src/services/settings-store.ts +0 -99
|
@@ -58,6 +58,7 @@ import { getExtensions, loadExtensions } from "../format/format-host";
|
|
|
58
58
|
import { optionalStringArg, stringProperty } from "../commands/command-args";
|
|
59
59
|
import type { TemplateResult } from "lit-html";
|
|
60
60
|
import type { AnyCommand, CommandRegistry } from "../commands/registry";
|
|
61
|
+
import { isMediaFormat } from "../utils/studio-utils";
|
|
61
62
|
|
|
62
63
|
interface SignalsPanelState {
|
|
63
64
|
document: JxMutableNode;
|
|
@@ -915,7 +916,7 @@ function renderSignalEditorTemplate(
|
|
|
915
916
|
: nothing
|
|
916
917
|
}
|
|
917
918
|
${
|
|
918
|
-
def.format
|
|
919
|
+
isMediaFormat(def.format)
|
|
919
920
|
? renderFieldRow({
|
|
920
921
|
hasValue: false,
|
|
921
922
|
label: "Default",
|
package/src/platforms/cloud.ts
CHANGED
|
@@ -36,6 +36,7 @@ import type {
|
|
|
36
36
|
StudioPlatform,
|
|
37
37
|
} from "../types";
|
|
38
38
|
import { problemDetail, problemSlug } from "@jxsuite/protocol";
|
|
39
|
+
import type { UploadResult } from "@jxsuite/protocol";
|
|
39
40
|
|
|
40
41
|
export interface CloudProject {
|
|
41
42
|
owner: string;
|
|
@@ -232,8 +233,15 @@ export function createCloudPlatform(project: CloudProject | null): StudioPlatfor
|
|
|
232
233
|
The session serves the project tree at /raw.
|
|
233
234
|
|
|
234
235
|
Conditional because `base` is "" until a project is bound, and "/raw/" would be a confident
|
|
235
|
-
wrong answer — worse than the default, which at least fails visibly.
|
|
236
|
-
|
|
236
|
+
wrong answer — worse than the default, which at least fails visibly.
|
|
237
|
+
|
|
238
|
+
`assetSpace` rides in the SAME spread, and that is the whole of its correctness: studio.jx-
|
|
239
|
+
suite.com is a multi-tenant SPA origin, so `/hero.jpg` misses Workers Static Assets and the
|
|
240
|
+
single-page-app fallback answers index.html at HTTP 200 — the <img> gets HTML, renders
|
|
241
|
+
broken, and logs nothing. Declaring "repo" tells Studio to resolve each reference to the
|
|
242
|
+
project file it names and address it under /raw. Until a project is bound there is no /raw to
|
|
243
|
+
address, so the project-less hub declares neither and stays exactly as it was. */
|
|
244
|
+
...(base ? { assetSpace: "repo" as const, documentBaseUrl: `${base}/raw/` } : {}),
|
|
237
245
|
/* Open Project routes through Studio's repo picker (write-access repos via listRepos +
|
|
238
246
|
importProject) — sessions are URL-bound, so openProject() below never opens a dialog. */
|
|
239
247
|
openProjectPicker: "repo-list",
|
|
@@ -314,7 +322,10 @@ export function createCloudPlatform(project: CloudProject | null): StudioPlatfor
|
|
|
314
322
|
method: "POST",
|
|
315
323
|
body: data,
|
|
316
324
|
});
|
|
317
|
-
|
|
325
|
+
const body = await okJson<Partial<UploadResult>>(res, `Upload failed: ${path}`);
|
|
326
|
+
// The session echoes the path today; falling back to the request keeps a backend that says
|
|
327
|
+
// Nothing working rather than writing `undefined` into a document.
|
|
328
|
+
return { path: body.path ?? path, ...(body.size === undefined ? {} : { size: body.size }) };
|
|
318
329
|
},
|
|
319
330
|
|
|
320
331
|
async deleteFile(path: string) {
|
|
@@ -38,6 +38,7 @@ import type {
|
|
|
38
38
|
StarterInfo,
|
|
39
39
|
} from "../types";
|
|
40
40
|
import { problemDetail, problemMessage } from "@jxsuite/protocol";
|
|
41
|
+
import type { UploadResult } from "@jxsuite/protocol";
|
|
41
42
|
|
|
42
43
|
/** A directory entry from the server, tolerating extra wire fields. */
|
|
43
44
|
type WireDirEntry = DirEntry & Record<string, unknown>;
|
|
@@ -391,7 +392,10 @@ export function createDevServerPlatform() {
|
|
|
391
392
|
if (!res.ok) {
|
|
392
393
|
throw new Error(`Upload failed: ${path}`);
|
|
393
394
|
}
|
|
394
|
-
|
|
395
|
+
const body = (await res.json()) as Partial<UploadResult>;
|
|
396
|
+
// The dev server echoes the path today; falling back to the request keeps a backend that says
|
|
397
|
+
// Nothing working rather than writing `undefined` into a document.
|
|
398
|
+
return { path: body.path ?? path, ...(body.size === undefined ? {} : { size: body.size }) };
|
|
395
399
|
},
|
|
396
400
|
|
|
397
401
|
/** @param {string} path */
|
|
@@ -13,14 +13,54 @@
|
|
|
13
13
|
|
|
14
14
|
import type { AiModelsResponse } from "@jxsuite/protocol";
|
|
15
15
|
import { getPlatform } from "../platform";
|
|
16
|
-
import { getBaseUrl, getOpenAiKey, hasOpenAiKey } from "./ai-settings";
|
|
16
|
+
import { getBaseUrl, getOpenAiKey, hasOpenAiKey, storedModel } from "./ai-settings";
|
|
17
|
+
import { SETTINGS } from "./settings/definitions";
|
|
18
|
+
import { onSettingsChanged } from "./settings/kernel";
|
|
17
19
|
|
|
18
20
|
export interface AiModel {
|
|
19
21
|
id: string;
|
|
20
22
|
name: string;
|
|
21
23
|
}
|
|
22
24
|
|
|
25
|
+
/**
|
|
26
|
+
* The credentials a model list belongs to.
|
|
27
|
+
*
|
|
28
|
+
* Two fields, taken together or not at all. The per-field overrides this replaces let a caller pass
|
|
29
|
+
* a draft key with a stored base URL, which is a pair that was never configured anywhere.
|
|
30
|
+
*/
|
|
31
|
+
export interface AiCredentials {
|
|
32
|
+
apiKey: string;
|
|
33
|
+
baseUrl: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The credentials the app is configured with right now. */
|
|
37
|
+
export function aiConnection(): AiCredentials {
|
|
38
|
+
return { apiKey: getOpenAiKey(), baseUrl: getBaseUrl() };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* An opaque identity for a credential pair.
|
|
43
|
+
*
|
|
44
|
+
* NUL-joined rather than concatenated: a key ending in the base URL's first characters would
|
|
45
|
+
* otherwise be indistinguishable from a shorter key and a longer URL.
|
|
46
|
+
*/
|
|
47
|
+
function fingerprint(credentials: AiCredentials): string {
|
|
48
|
+
return `${credentials.apiKey}\u0000${credentials.baseUrl}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
23
51
|
let cache: AiModel[] | null = null;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Which credentials {@link cache} was listed under.
|
|
55
|
+
*
|
|
56
|
+
* The cache used to be unkeyed, and two callers with different credentials wrote to it: the
|
|
57
|
+
* credentials form fetches with the drafts being edited, while the capability probe fetches with
|
|
58
|
+
* whatever is stored. Whichever landed last won, so the chat composer's picker could list a
|
|
59
|
+
* different provider's models than the one actually configured — which is exactly what a user saw
|
|
60
|
+
* after configuring a provider and finding the shell offering someone else's catalogue.
|
|
61
|
+
*/
|
|
62
|
+
let cacheKey = "";
|
|
63
|
+
|
|
24
64
|
let proxyConfigured = false;
|
|
25
65
|
let proxyManaged = false;
|
|
26
66
|
let proxyDefaultModel = "";
|
|
@@ -34,9 +74,17 @@ let proxyCode: AiModelsResponse["code"];
|
|
|
34
74
|
let proxyProbe: Promise<void> | null = null;
|
|
35
75
|
const probeListeners = new Set<() => void>();
|
|
36
76
|
|
|
37
|
-
/**
|
|
38
|
-
|
|
77
|
+
/**
|
|
78
|
+
* Drop the cached list and the capability reading.
|
|
79
|
+
*
|
|
80
|
+
* No surface calls this: it runs off the settings subscription below, because "the credentials
|
|
81
|
+
* changed" is a fact about the store rather than something each form has to remember to announce —
|
|
82
|
+
* and a form that forgot left every gate reading a stale capability probe. It stays exported only
|
|
83
|
+
* so a test can start from a cold module.
|
|
84
|
+
*/
|
|
85
|
+
export function resetModelCache() {
|
|
39
86
|
cache = null;
|
|
87
|
+
cacheKey = "";
|
|
40
88
|
proxyConfigured = false;
|
|
41
89
|
proxyManaged = false;
|
|
42
90
|
proxyDefaultModel = "";
|
|
@@ -47,6 +95,28 @@ export function invalidateModelCache() {
|
|
|
47
95
|
proxyProbe = null;
|
|
48
96
|
}
|
|
49
97
|
|
|
98
|
+
/** The keys whose value changes what a provider would answer with. */
|
|
99
|
+
const CONNECTION_KEYS = new Set<string>([
|
|
100
|
+
SETTINGS.aiOpenAiKey.key,
|
|
101
|
+
SETTINGS.aiBaseUrl.key,
|
|
102
|
+
SETTINGS.cfToken.key,
|
|
103
|
+
SETTINGS.cfAccountId.key,
|
|
104
|
+
]);
|
|
105
|
+
|
|
106
|
+
onSettingsChanged((keys) => {
|
|
107
|
+
if (keys.some((key) => CONNECTION_KEYS.has(key))) {
|
|
108
|
+
resetModelCache();
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The cached list, if it was listed under `credentials`. Null when it was not, or when there is
|
|
114
|
+
* none — a caller must not show one provider's catalogue for another's key.
|
|
115
|
+
*/
|
|
116
|
+
export function cachedModels(credentials: AiCredentials): AiModel[] | null {
|
|
117
|
+
return cache && cacheKey === fingerprint(credentials) ? cache : null;
|
|
118
|
+
}
|
|
119
|
+
|
|
50
120
|
/**
|
|
51
121
|
* Run the capability probe once, repainting `onSettled` when it lands. Idempotent: concurrent and
|
|
52
122
|
* repeated callers share the single in-flight fetch, and every registered host is notified. Probe
|
|
@@ -108,6 +178,23 @@ export function getProxyDefaultModel(): string {
|
|
|
108
178
|
return proxyDefaultModel;
|
|
109
179
|
}
|
|
110
180
|
|
|
181
|
+
/**
|
|
182
|
+
* The model to SEND: what the user chose, else what the backend says it prefers, else the built-in
|
|
183
|
+
* default.
|
|
184
|
+
*
|
|
185
|
+
* The middle term is the point. A managed Workers AI backend declares its own `defaultModel`, and
|
|
186
|
+
* that value was fetched, stored and never read — so a user who had chosen nothing got the
|
|
187
|
+
* hardcoded `"gpt-4o"`, which Workers AI does not serve. Asking the backend what it prefers before
|
|
188
|
+
* falling back to a name invented here is the difference between working and a 404 on the first
|
|
189
|
+
* message.
|
|
190
|
+
*
|
|
191
|
+
* It lives here rather than in `ai-settings.ts` because only this module knows what the proxy said,
|
|
192
|
+
* and `ai-settings` must not import it — this module already imports `ai-settings`.
|
|
193
|
+
*/
|
|
194
|
+
export function preferredModel(): string {
|
|
195
|
+
return storedModel() || getProxyDefaultModel() || SETTINGS.aiModel.default;
|
|
196
|
+
}
|
|
197
|
+
|
|
111
198
|
/**
|
|
112
199
|
* The sibling of the chat route, replacing the last path segment and keeping everything else.
|
|
113
200
|
*
|
|
@@ -146,9 +233,13 @@ export function siblingRoute(chatUrl: string, segment: string): string {
|
|
|
146
233
|
* @returns {Promise<AiModel[]>}
|
|
147
234
|
*/
|
|
148
235
|
export async function fetchAvailableModels(
|
|
149
|
-
opts: {
|
|
236
|
+
opts: { credentials?: AiCredentials; force?: boolean } = {},
|
|
150
237
|
): Promise<AiModel[]> {
|
|
151
|
-
|
|
238
|
+
/* Resolve the credentials BEFORE consulting the cache. Checking the cache first is what let a
|
|
239
|
+
list fetched under one provider be handed to a caller asking about another. */
|
|
240
|
+
const credentials = opts.credentials ?? aiConnection();
|
|
241
|
+
const key = fingerprint(credentials);
|
|
242
|
+
if (cache && cacheKey === key && !opts.force) {
|
|
152
243
|
return cache;
|
|
153
244
|
}
|
|
154
245
|
const plat = getPlatform();
|
|
@@ -156,13 +247,11 @@ export async function fetchAvailableModels(
|
|
|
156
247
|
const modelsUrl = siblingRoute(chatUrl, "models");
|
|
157
248
|
|
|
158
249
|
const headers: Record<string, string> = {};
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
headers["X-Api-Key"] = apiKey;
|
|
250
|
+
if (credentials.apiKey) {
|
|
251
|
+
headers["X-Api-Key"] = credentials.apiKey;
|
|
162
252
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
headers["X-Api-Base-URL"] = baseUrl;
|
|
253
|
+
if (credentials.baseUrl) {
|
|
254
|
+
headers["X-Api-Base-URL"] = credentials.baseUrl;
|
|
166
255
|
}
|
|
167
256
|
|
|
168
257
|
const resp = await fetch(modelsUrl, { headers });
|
|
@@ -175,5 +264,6 @@ export async function fetchAvailableModels(
|
|
|
175
264
|
proxyDefaultModel = data.defaultModel ?? "";
|
|
176
265
|
proxyCode = data.code;
|
|
177
266
|
cache = (data.models || []).map((m) => ({ id: m.id, name: m.name || m.id }));
|
|
267
|
+
cacheKey = key;
|
|
178
268
|
return cache;
|
|
179
269
|
}
|
|
@@ -1,52 +1,51 @@
|
|
|
1
1
|
/// <reference lib="dom" />
|
|
2
2
|
/**
|
|
3
|
-
* Ai-settings.js —
|
|
3
|
+
* Ai-settings.js — the AI assistant's provider settings, over the settings kernel.
|
|
4
4
|
*
|
|
5
5
|
* The Stack B proxy reads the OpenAI key from the `X-Api-Key` header (falling back to the server's
|
|
6
|
-
* `OPENAI_API_KEY` env var). Studio stores a user-supplied key
|
|
7
|
-
*
|
|
6
|
+
* `OPENAI_API_KEY` env var). Studio stores a user-supplied key so the browser/dev build works
|
|
7
|
+
* without an env var. The key never leaves the machine except to the same-origin proxy.
|
|
8
|
+
*
|
|
9
|
+
* This module is now a typed façade over `settings/kernel.ts` — the storage keys, defaults and
|
|
10
|
+
* normalisation live in `settings/definitions.ts`, and the kernel owns the cache, the write queue
|
|
11
|
+
* and change notification. What it keeps is the vocabulary: callers say `getOpenAiKey()`, not
|
|
12
|
+
* `readSetting(SETTINGS.aiOpenAiKey)`.
|
|
13
|
+
*
|
|
14
|
+
* **Blank no longer deletes.** `setOpenAiKey("")` stores an empty string; {@link clearAiProvider}
|
|
15
|
+
* is the revoke path. The old conflation read as a convenience right up until a form that blanked
|
|
16
|
+
* its own drafts on Save called these setters with them, and a second Save revoked the credentials
|
|
17
|
+
* the first had stored.
|
|
8
18
|
*
|
|
9
19
|
* @license MIT
|
|
10
20
|
*/
|
|
11
21
|
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
22
|
+
import { SETTINGS } from "./settings/definitions";
|
|
23
|
+
import {
|
|
24
|
+
clearSettings,
|
|
25
|
+
hasSetting,
|
|
26
|
+
readStoredSetting,
|
|
27
|
+
setSetting,
|
|
28
|
+
setSettings,
|
|
29
|
+
} from "./settings/kernel";
|
|
18
30
|
|
|
19
|
-
/** @returns {string} The stored OpenAI key, or "" if none
|
|
31
|
+
/** @returns {string} The stored OpenAI key, or "" if none. */
|
|
20
32
|
export function getOpenAiKey() {
|
|
21
|
-
|
|
22
|
-
return globalThis.localStorage?.getItem(KEY_STORAGE) ?? "";
|
|
23
|
-
} catch {
|
|
24
|
-
return "";
|
|
25
|
-
}
|
|
33
|
+
return readStoredSetting(SETTINGS.aiOpenAiKey);
|
|
26
34
|
}
|
|
27
35
|
|
|
28
36
|
/**
|
|
29
|
-
* Persist
|
|
37
|
+
* Persist the OpenAI key.
|
|
30
38
|
*
|
|
31
|
-
* @param {string} key - The key to store
|
|
39
|
+
* @param {string} key - The key to store. A blank value stores blank; it does NOT clear — see
|
|
40
|
+
* {@link clearAiProvider}.
|
|
32
41
|
*/
|
|
33
42
|
export function setOpenAiKey(key: string) {
|
|
34
|
-
|
|
35
|
-
const trimmed = (key || "").trim();
|
|
36
|
-
if (trimmed) {
|
|
37
|
-
globalThis.localStorage?.setItem(KEY_STORAGE, trimmed);
|
|
38
|
-
} else {
|
|
39
|
-
globalThis.localStorage?.removeItem(KEY_STORAGE);
|
|
40
|
-
}
|
|
41
|
-
persistSettings();
|
|
42
|
-
} catch {
|
|
43
|
-
/* LocalStorage unavailable — settings are not persisted. */
|
|
44
|
-
}
|
|
43
|
+
setSetting(SETTINGS.aiOpenAiKey, key || "");
|
|
45
44
|
}
|
|
46
45
|
|
|
47
46
|
/** @returns {boolean} Whether a non-empty key is stored. */
|
|
48
47
|
export function hasOpenAiKey() {
|
|
49
|
-
return
|
|
48
|
+
return hasSetting(SETTINGS.aiOpenAiKey);
|
|
50
49
|
}
|
|
51
50
|
|
|
52
51
|
/**
|
|
@@ -54,59 +53,58 @@ export function hasOpenAiKey() {
|
|
|
54
53
|
* or "" to use the proxy's default (`https://api.openai.com/v1` / server `OPENAI_BASE_URL`).
|
|
55
54
|
*/
|
|
56
55
|
export function getBaseUrl() {
|
|
57
|
-
|
|
58
|
-
return globalThis.localStorage?.getItem(BASE_URL_STORAGE) ?? "";
|
|
59
|
-
} catch {
|
|
60
|
-
return "";
|
|
61
|
-
}
|
|
56
|
+
return readStoredSetting(SETTINGS.aiBaseUrl);
|
|
62
57
|
}
|
|
63
58
|
|
|
59
|
+
// ─── Model selection ────────────────────────────────────────────────────────
|
|
60
|
+
|
|
64
61
|
/**
|
|
65
|
-
*
|
|
62
|
+
* The model the user actually chose, with no default standing in for silence.
|
|
66
63
|
*
|
|
67
|
-
*
|
|
64
|
+
* The getter this replaced masked _unset_ as `"gpt-4o"`, which is right for a sender and wrong for
|
|
65
|
+
* anything that writes back: a form prefilled from it and then saved persists a choice nobody made,
|
|
66
|
+
* which is how `jx.ai.model: "gpt-4o"` came to be the only surviving key in a settings file whose
|
|
67
|
+
* owner had configured a different provider entirely. Senders want
|
|
68
|
+
* {@link ../services/ai-models!preferredModel} instead.
|
|
69
|
+
*
|
|
70
|
+
* @returns {string} The stored model ID, or "" when none has been chosen.
|
|
68
71
|
*/
|
|
69
|
-
export function
|
|
70
|
-
|
|
71
|
-
const trimmed = (url || "").trim().replace(/\/+$/, "");
|
|
72
|
-
if (trimmed) {
|
|
73
|
-
globalThis.localStorage?.setItem(BASE_URL_STORAGE, trimmed);
|
|
74
|
-
} else {
|
|
75
|
-
globalThis.localStorage?.removeItem(BASE_URL_STORAGE);
|
|
76
|
-
}
|
|
77
|
-
persistSettings();
|
|
78
|
-
} catch {
|
|
79
|
-
/* LocalStorage unavailable — settings are not persisted. */
|
|
80
|
-
}
|
|
72
|
+
export function storedModel() {
|
|
73
|
+
return readStoredSetting(SETTINGS.aiModel);
|
|
81
74
|
}
|
|
82
75
|
|
|
83
|
-
|
|
76
|
+
/**
|
|
77
|
+
* Persist the selected model ID.
|
|
78
|
+
*
|
|
79
|
+
* @param {string} modelId - The model ID to store. A blank value stores blank; it does NOT clear.
|
|
80
|
+
*/
|
|
81
|
+
export function setModel(modelId: string) {
|
|
82
|
+
setSetting(SETTINGS.aiModel, modelId || "");
|
|
83
|
+
}
|
|
84
84
|
|
|
85
|
-
/**
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
85
|
+
/**
|
|
86
|
+
* Store the provider's key, endpoint and model as ONE change.
|
|
87
|
+
*
|
|
88
|
+
* What a credentials form's Save should call. Three separate setters coalesce into one write
|
|
89
|
+
* anyway, but they announce three times, and each intermediate announcement describes a provider
|
|
90
|
+
* the user never asked for — a new key against the old endpoint, then against no model.
|
|
91
|
+
*
|
|
92
|
+
* @param {{ apiKey: string; baseUrl: string; model: string }} provider
|
|
93
|
+
*/
|
|
94
|
+
export function saveAiProvider(provider: { apiKey: string; baseUrl: string; model: string }) {
|
|
95
|
+
setSettings([
|
|
96
|
+
[SETTINGS.aiOpenAiKey, provider.apiKey],
|
|
97
|
+
[SETTINGS.aiBaseUrl, provider.baseUrl],
|
|
98
|
+
[SETTINGS.aiModel, provider.model],
|
|
99
|
+
]);
|
|
92
100
|
}
|
|
93
101
|
|
|
94
102
|
/**
|
|
95
|
-
*
|
|
103
|
+
* Forget the provider entirely — key, endpoint and model.
|
|
96
104
|
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
105
|
+
* The one deletion path, and the one the Accounts list's Disconnect uses. Deliberately all three
|
|
106
|
+
* together: a key with an orphaned endpoint is a state no surface knows how to describe.
|
|
99
107
|
*/
|
|
100
|
-
export function
|
|
101
|
-
|
|
102
|
-
const trimmed = (modelId || "").trim();
|
|
103
|
-
if (trimmed) {
|
|
104
|
-
globalThis.localStorage?.setItem(MODEL_STORAGE, trimmed);
|
|
105
|
-
} else {
|
|
106
|
-
globalThis.localStorage?.removeItem(MODEL_STORAGE);
|
|
107
|
-
}
|
|
108
|
-
persistSettings();
|
|
109
|
-
} catch {
|
|
110
|
-
/* LocalStorage unavailable — settings are not persisted. */
|
|
111
|
-
}
|
|
108
|
+
export function clearAiProvider() {
|
|
109
|
+
clearSettings([SETTINGS.aiOpenAiKey, SETTINGS.aiBaseUrl, SETTINGS.aiModel]);
|
|
112
110
|
}
|
|
@@ -1,58 +1,40 @@
|
|
|
1
1
|
/// <reference lib="dom" />
|
|
2
2
|
/**
|
|
3
|
-
* Cf-settings —
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* platform settings backend where one exists. It only ever leaves the machine
|
|
7
|
-
* to the same-origin `/__studio/cf/proxy`, which forwards it as a bearer to
|
|
3
|
+
* Cf-settings — the Cloudflare publish connection on platforms without a hosted OAuth broker (dev
|
|
4
|
+
* server, desktop). The API token is stored like the AI key, over `settings/kernel.ts`. It only ever
|
|
5
|
+
* leaves the machine to the same-origin `/__studio/cf/proxy`, which forwards it as a bearer to
|
|
8
6
|
* api.cloudflare.com (not CORS-enabled, hence the proxy).
|
|
9
7
|
*
|
|
8
|
+
* As with the AI provider, a blank value is a VALUE: {@link clearCfConnection} is the deletion path,
|
|
9
|
+
* so nothing can revoke a connection by rendering an empty field into a setter.
|
|
10
|
+
*
|
|
10
11
|
* @license MIT
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
const TOKEN_STORAGE = "jx.cf.token";
|
|
16
|
-
const ACCOUNT_STORAGE = "jx.cf.accountId";
|
|
17
|
-
|
|
18
|
-
function read(key: string): string {
|
|
19
|
-
try {
|
|
20
|
-
return globalThis.localStorage?.getItem(key) ?? "";
|
|
21
|
-
} catch {
|
|
22
|
-
return "";
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function write(key: string, value: string): void {
|
|
27
|
-
try {
|
|
28
|
-
const trimmed = (value || "").trim();
|
|
29
|
-
if (trimmed) {
|
|
30
|
-
globalThis.localStorage?.setItem(key, trimmed);
|
|
31
|
-
} else {
|
|
32
|
-
globalThis.localStorage?.removeItem(key);
|
|
33
|
-
}
|
|
34
|
-
} catch {
|
|
35
|
-
// Storage unavailable (private mode) — the connection just won't persist.
|
|
36
|
-
}
|
|
37
|
-
persistSettings();
|
|
38
|
-
}
|
|
14
|
+
import { SETTINGS } from "./settings/definitions";
|
|
15
|
+
import { clearSettings, readStoredSetting, setSetting } from "./settings/kernel";
|
|
39
16
|
|
|
40
17
|
/** The stored Cloudflare API token, or "" when not connected. */
|
|
41
18
|
export function getCfToken(): string {
|
|
42
|
-
return
|
|
19
|
+
return readStoredSetting(SETTINGS.cfToken);
|
|
43
20
|
}
|
|
44
21
|
|
|
45
|
-
/** Persist
|
|
22
|
+
/** Persist the Cloudflare API token. Blank stores blank — see {@link clearCfConnection}. */
|
|
46
23
|
export function setCfToken(token: string): void {
|
|
47
|
-
|
|
24
|
+
setSetting(SETTINGS.cfToken, token || "");
|
|
48
25
|
}
|
|
49
26
|
|
|
50
27
|
/** The selected Cloudflare account id, or "" when none chosen yet. */
|
|
51
28
|
export function getCfAccountId(): string {
|
|
52
|
-
return
|
|
29
|
+
return readStoredSetting(SETTINGS.cfAccountId);
|
|
53
30
|
}
|
|
54
31
|
|
|
55
|
-
/** Persist
|
|
32
|
+
/** Persist the selected Cloudflare account id. Blank stores blank. */
|
|
56
33
|
export function setCfAccountId(accountId: string): void {
|
|
57
|
-
|
|
34
|
+
setSetting(SETTINGS.cfAccountId, accountId || "");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Forget the connection — token and account together. What Disconnect calls. */
|
|
38
|
+
export function clearCfConnection(): void {
|
|
39
|
+
clearSettings([SETTINGS.cfToken, SETTINGS.cfAccountId]);
|
|
58
40
|
}
|
|
@@ -26,7 +26,8 @@ import type { ToolAvailability } from "./gated-registry";
|
|
|
26
26
|
import { adoptProject } from "./project-adoption";
|
|
27
27
|
import { runAgentLoop } from "./tool-executor";
|
|
28
28
|
import { AI_TOOL_TIERS, buildSystemPrompt, tierActive } from "./ai-system-prompt";
|
|
29
|
-
import { getBaseUrl,
|
|
29
|
+
import { getBaseUrl, getOpenAiKey } from "./ai-settings";
|
|
30
|
+
import { preferredModel } from "./ai-models";
|
|
30
31
|
import { trimContext } from "./context-manager";
|
|
31
32
|
import { renderCheck } from "./render-critic";
|
|
32
33
|
import { openFileInTab, reloadFileInTab } from "../files/files";
|
|
@@ -58,7 +59,7 @@ function projectRoot() {
|
|
|
58
59
|
* }}
|
|
59
60
|
*/
|
|
60
61
|
export function createDocumentAssistant() {
|
|
61
|
-
const chatState = createChatState({ model:
|
|
62
|
+
const chatState = createChatState({ model: preferredModel() });
|
|
62
63
|
|
|
63
64
|
/** The persisted session backing the live chat; null = fresh unsaved chat. */
|
|
64
65
|
let sessionId: string | null = null;
|
|
@@ -212,7 +213,7 @@ export function createDocumentAssistant() {
|
|
|
212
213
|
const chatUrl = await Promise.resolve(plat.aiChatUrl());
|
|
213
214
|
// Re-read the persisted model each send: the session is constructed once at module load
|
|
214
215
|
// (before the user sets a key/model), so the picker's choice must be picked up here.
|
|
215
|
-
chatState.setModel(
|
|
216
|
+
chatState.setModel(preferredModel());
|
|
216
217
|
const streamingClient = createProxyStreamingClient({
|
|
217
218
|
chatUrl,
|
|
218
219
|
model: chatState.model,
|
package/src/services/idle.ts
CHANGED
|
@@ -27,6 +27,9 @@
|
|
|
27
27
|
* duplicate of `platform`: an activity spans a whole operation (an install, a deploy, a clone)
|
|
28
28
|
* across many PAL calls, so the gaps between them look quiet at the seam while the operation is
|
|
29
29
|
* plainly mid-flight. A finished entry never blocks — the list stays on screen forever.
|
|
30
|
+
* 7. `grid-idle.gridIdleBlockers()` — tables still building, or built but not yet showing the
|
|
31
|
+
* selection range `selectableRange: 1` gives them. Nothing else covered Tabulator: a grid
|
|
32
|
+
* command resolves when the panel mounts, which is several frames before the table is drawn.
|
|
30
33
|
*
|
|
31
34
|
* The consumers are the `packages/studio:verify` skill, the screenshot runner and P4.2's Activity
|
|
32
35
|
* tracker (this is a read-only projection of the same in-flight set that dock renders).
|
|
@@ -38,6 +41,7 @@ import { pendingSchedulers } from "../panels/panel-scheduler";
|
|
|
38
41
|
import { canvasIdleBlockers } from "../canvas/iframe-host";
|
|
39
42
|
import { platformInFlight } from "../platform";
|
|
40
43
|
import { overlayIdleBlockers } from "../ui/layers";
|
|
44
|
+
import { gridIdleBlockers } from "../grid/grid-idle";
|
|
41
45
|
|
|
42
46
|
/** One subsystem that can be outstanding, and its account of why. */
|
|
43
47
|
export interface IdleSource {
|
|
@@ -69,7 +73,7 @@ export class NotIdleError extends Error {
|
|
|
69
73
|
}
|
|
70
74
|
|
|
71
75
|
/**
|
|
72
|
-
* The
|
|
76
|
+
* The seven live sources.
|
|
73
77
|
*
|
|
74
78
|
* Built per call rather than held in a module constant so a test can substitute one without
|
|
75
79
|
* unpicking the others, and so a second window's hosts are never read through the first window's
|
|
@@ -101,6 +105,7 @@ export function defaultIdleSources(): IdleSource[] {
|
|
|
101
105
|
},
|
|
102
106
|
{ blockers: overlayIdleBlockers, name: "overlay" },
|
|
103
107
|
{ blockers: activityIdleBlockers, name: "activity" },
|
|
108
|
+
{ blockers: gridIdleBlockers, name: "grid" },
|
|
104
109
|
];
|
|
105
110
|
}
|
|
106
111
|
|