@jxsuite/studio 2.0.0 → 2.0.1

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 (43) hide show
  1. package/dist/chunks/{components-0hzentwq.js → components-799jpvhm.js} +4 -4
  2. package/dist/chunks/{extension-sections-qct0gt8f.js → extension-sections-qr3drkkw.js} +10 -10
  3. package/dist/chunks/{files-02zn5y24.js → files-j8a1t06k.js} +8 -8
  4. package/dist/chunks/{format-host-2c188n41.js → format-host-8jxwts2p.js} +2 -2
  5. package/dist/chunks/{git-panel-mbb0y0hz.js → git-panel-xtq5z81x.js} +6 -6
  6. package/dist/chunks/{layers-panel-9qekmqk1.js → layers-panel-ancn2wpq.js} +8 -8
  7. package/dist/chunks/{settings-modal-gj0hrah4.js → settings-modal-17ttr96g.js} +9 -9
  8. package/dist/chunks/{studio-asvf0j9e.js → studio-0xgs7xrs.js} +2 -2
  9. package/dist/chunks/{studio-d77qm3pk.js → studio-4d8hzcz5.js} +8 -8
  10. package/dist/chunks/{studio-hhj92gcr.js → studio-esrewxap.js} +2 -2
  11. package/dist/chunks/{studio-97qcgvdn.js → studio-gs6kqdt1.js} +2 -2
  12. package/dist/chunks/{studio-n9zmpxjg.js → studio-jm0z592w.js} +8 -8
  13. package/dist/chunks/{studio-j6gqyjf4.js → studio-m2t4mdg5.js} +8 -8
  14. package/dist/chunks/{studio-wd6ewdvd.js → studio-n4p11dfy.js} +6 -6
  15. package/dist/chunks/{studio-rq0vxb19.js → studio-sjh1wt1a.js} +2 -2
  16. package/dist/chunks/{studio-ygzav7z5.js → studio-v7jkjqng.js} +6 -6
  17. package/dist/chunks/{studio-zmy7241e.js → studio-yfcstdzy.js} +2 -2
  18. package/dist/chunks/{studio-568nx4t5.js → studio-zp7jncs8.js} +6 -6
  19. package/dist/chunks/{studio-568nx4t5.js.map → studio-zp7jncs8.js.map} +1 -1
  20. package/dist/studio.js +113 -82
  21. package/package.json +1 -1
  22. package/src/new-project/import-tab.ts +27 -9
  23. package/src/new-project/new-project-modal.ts +49 -17
  24. package/src/panels/ai-panel.ts +9 -76
  25. package/src/services/ai-models.ts +45 -1
  26. package/src/ui/ai-managed-connect.ts +107 -0
  27. /package/dist/chunks/{components-0hzentwq.js.map → components-799jpvhm.js.map} +0 -0
  28. /package/dist/chunks/{extension-sections-qct0gt8f.js.map → extension-sections-qr3drkkw.js.map} +0 -0
  29. /package/dist/chunks/{files-02zn5y24.js.map → files-j8a1t06k.js.map} +0 -0
  30. /package/dist/chunks/{format-host-2c188n41.js.map → format-host-8jxwts2p.js.map} +0 -0
  31. /package/dist/chunks/{git-panel-mbb0y0hz.js.map → git-panel-xtq5z81x.js.map} +0 -0
  32. /package/dist/chunks/{layers-panel-9qekmqk1.js.map → layers-panel-ancn2wpq.js.map} +0 -0
  33. /package/dist/chunks/{settings-modal-gj0hrah4.js.map → settings-modal-17ttr96g.js.map} +0 -0
  34. /package/dist/chunks/{studio-asvf0j9e.js.map → studio-0xgs7xrs.js.map} +0 -0
  35. /package/dist/chunks/{studio-d77qm3pk.js.map → studio-4d8hzcz5.js.map} +0 -0
  36. /package/dist/chunks/{studio-hhj92gcr.js.map → studio-esrewxap.js.map} +0 -0
  37. /package/dist/chunks/{studio-97qcgvdn.js.map → studio-gs6kqdt1.js.map} +0 -0
  38. /package/dist/chunks/{studio-n9zmpxjg.js.map → studio-jm0z592w.js.map} +0 -0
  39. /package/dist/chunks/{studio-j6gqyjf4.js.map → studio-m2t4mdg5.js.map} +0 -0
  40. /package/dist/chunks/{studio-wd6ewdvd.js.map → studio-n4p11dfy.js.map} +0 -0
  41. /package/dist/chunks/{studio-rq0vxb19.js.map → studio-sjh1wt1a.js.map} +0 -0
  42. /package/dist/chunks/{studio-ygzav7z5.js.map → studio-v7jkjqng.js.map} +0 -0
  43. /package/dist/chunks/{studio-zmy7241e.js.map → studio-yfcstdzy.js.map} +0 -0
@@ -16,14 +16,14 @@
16
16
  * @license MIT
17
17
  */
18
18
 
19
- import { html, render as litRender, nothing } from "lit-html";
19
+ import { html, render as litRender } from "lit-html";
20
20
  import type { TemplateResult } from "lit-html";
21
- import { getPlatform } from "../platform";
22
21
  import { effect, effectScope } from "../reactivity";
23
22
  import { createDocumentAssistant } from "../services/document-assistant";
24
- import { hasOpenAiKey, setOpenAiKey } from "../services/ai-settings";
25
- import { fetchAvailableModels, isManagedProxy, isProxyConfigured } from "../services/ai-models";
23
+ import { setOpenAiKey } from "../services/ai-settings";
24
+ import { hasAiCredentials } from "../services/ai-models";
26
25
  import { createAiCredentialsForm } from "../ui/ai-credentials-form";
26
+ import { createManagedConnect } from "../ui/ai-managed-connect";
27
27
  import { clearMarkdownCache } from "./ai-chat/chat-markdown";
28
28
  import { renderChatHeader, renderMessageList } from "./ai-chat/chat-view";
29
29
  import { createComposer } from "./ai-chat/composer";
@@ -38,23 +38,6 @@ let mounted = false;
38
38
  /** Whether the OpenAI key form is showing (gate when no key, or re-edit via the gear). */
39
39
  let keyEditing = false;
40
40
 
41
- /**
42
- * One-time proxy probe: managed platforms (cloud Workers AI) and env-keyed dev servers report
43
- * `configured` from /models, unlocking the assistant without a locally stored key. Fired lazily on
44
- * the first gated render.
45
- */
46
- let proxyProbe: Promise<void> | null = null;
47
-
48
- function ensureProxyProbe() {
49
- proxyProbe ??= fetchAvailableModels({ force: true })
50
- .catch(() => {
51
- // Unreachable proxy — the key gate stays up.
52
- })
53
- .then(() => {
54
- scheduleAiRender();
55
- });
56
- }
57
-
58
41
  /** Which pane the panel shows once the key gate is passed. */
59
42
  let view: "chat" | "sessions" = "chat";
60
43
 
@@ -171,56 +154,8 @@ function startEditApiKey() {
171
154
 
172
155
  // ─── Managed Cloudflare connect (Workers AI) ─────────────────────────────────
173
156
 
174
- let cfConnectBusy = false;
175
- let cfConnectError = "";
176
-
177
- /**
178
- * Managed platforms broker Workers AI on the user's own Cloudflare account: offer connecting it as
179
- * the keyless alternative whenever the proxy says managed-but-unconfigured and the platform can run
180
- * the hosted OAuth flow (the PAL seam — desktop shells can implement cfConnect later).
181
- */
182
- function canOfferManagedConnect(): boolean {
183
- return isManagedProxy() && !isProxyConfigured() && Boolean(getPlatform().cfConnect);
184
- }
185
-
186
- async function connectCloudflareForAi() {
187
- if (cfConnectBusy) {
188
- return;
189
- }
190
- cfConnectBusy = true;
191
- cfConnectError = "";
192
- scheduleAiRender();
193
- try {
194
- const connection = await getPlatform().cfConnect?.();
195
- if (connection) {
196
- // Re-probe: /models flips to configured once the connection lands, opening the gate.
197
- await fetchAvailableModels({ force: true });
198
- } else {
199
- cfConnectError = "Cloudflare connection was not completed.";
200
- }
201
- } catch (error) {
202
- cfConnectError = error instanceof Error ? error.message : String(error);
203
- }
204
- cfConnectBusy = false;
205
- scheduleAiRender();
206
- }
207
-
208
- function renderManagedConnect() {
209
- return html`
210
- <div class="ai-managed-connect">
211
- <div>Use Workers AI on your own Cloudflare account — no API key needed.</div>
212
- <sp-button size="s" ?disabled=${cfConnectBusy} @click=${() => void connectCloudflareForAi()}>
213
- ${cfConnectBusy ? "Connecting…" : "Connect Cloudflare"}
214
- </sp-button>
215
- ${
216
- cfConnectError
217
- ? html`<div class="ai-managed-connect-error">${cfConnectError}</div>`
218
- : nothing
219
- }
220
- <div class="ai-managed-connect-divider">— or bring your own key —</div>
221
- </div>
222
- `;
223
- }
157
+ /** Shared with the New Project modal's gates — see ui/ai-managed-connect.ts. */
158
+ const managedConnect = createManagedConnect({ requestRender: scheduleAiRender });
224
159
 
225
160
  /**
226
161
  * The credentials gate: on managed platforms a keyless "Connect Cloudflare" (Workers AI) option
@@ -229,9 +164,7 @@ function renderManagedConnect() {
229
164
  function renderKeyGate() {
230
165
  return html`
231
166
  <div class="ai-tab-body">
232
- <div class="ai-status-center">
233
- ${canOfferManagedConnect() ? renderManagedConnect() : nothing} ${credsForm.render()}
234
- </div>
167
+ <div class="ai-status-center">${managedConnect.render()} ${credsForm.render()}</div>
235
168
  </div>
236
169
  `;
237
170
  }
@@ -377,9 +310,9 @@ export function renderAiPanelTemplate(): TemplateResult {
377
310
  /* The document assistant authenticates via the AI proxy. Gate the chat
378
311
  behind the key form until a key is stored locally OR the proxy reports
379
312
  itself configured (managed platforms, env-keyed dev servers). */
380
- if ((!hasOpenAiKey() && !isProxyConfigured()) || keyEditing) {
313
+ if (!hasAiCredentials() || keyEditing) {
381
314
  if (!keyEditing) {
382
- ensureProxyProbe();
315
+ managedConnect.ensureProbe();
383
316
  }
384
317
  return renderKeyGate();
385
318
  }
@@ -13,7 +13,7 @@
13
13
 
14
14
  import type { AiModelsResponse } from "@jxsuite/protocol";
15
15
  import { getPlatform } from "../platform";
16
- import { getBaseUrl, getOpenAiKey } from "./ai-settings";
16
+ import { getBaseUrl, getOpenAiKey, hasOpenAiKey } from "./ai-settings";
17
17
 
18
18
  export interface AiModel {
19
19
  id: string;
@@ -25,12 +25,56 @@ let proxyConfigured = false;
25
25
  let proxyManaged = false;
26
26
  let proxyDefaultModel = "";
27
27
 
28
+ /**
29
+ * One-shot capability probe shared by every credentials gate, plus the hosts to repaint when it
30
+ * settles. Managed platforms (cloud Workers AI) and env-keyed dev servers report their state from
31
+ * /models, so the gate cannot know which paths to offer until this has run once.
32
+ */
33
+ let proxyProbe: Promise<void> | null = null;
34
+ const probeListeners = new Set<() => void>();
35
+
28
36
  /** Drop the cached model list (call after credentials/endpoint changes). */
29
37
  export function invalidateModelCache() {
30
38
  cache = null;
31
39
  proxyConfigured = false;
32
40
  proxyManaged = false;
33
41
  proxyDefaultModel = "";
42
+ /* The probe's result IS the three flags above. Clearing them while keeping the settled promise
43
+ would strand every gate on a permanent "unconfigured, unmanaged" reading — ensureProxyProbe
44
+ would no-op forever and the managed option would vanish until a full reload. */
45
+ proxyProbe = null;
46
+ }
47
+
48
+ /**
49
+ * Run the capability probe once, repainting `onSettled` when it lands. Idempotent: concurrent and
50
+ * repeated callers share the single in-flight fetch, and every registered host is notified. Probe
51
+ * failure is not surfaced — an unreachable proxy simply leaves the gate showing the key form.
52
+ *
53
+ * @param {() => void} [onSettled] - Host re-render scheduler, registered for this and later probes.
54
+ */
55
+ export function ensureProxyProbe(onSettled?: () => void) {
56
+ if (onSettled) {
57
+ probeListeners.add(onSettled);
58
+ }
59
+ proxyProbe ??= fetchAvailableModels({ force: true })
60
+ .catch(() => {
61
+ // Unreachable proxy — the key gate stays up.
62
+ })
63
+ .then(() => {
64
+ for (const notify of probeListeners) {
65
+ notify();
66
+ }
67
+ });
68
+ }
69
+
70
+ /**
71
+ * Whether the assistant can run at all: a key stored in this browser, or a backend that holds
72
+ * credentials itself (managed cloud platforms, env-keyed dev servers). Every credentials gate must
73
+ * use this rather than `hasOpenAiKey()` alone — gating on the local key locks managed-platform
74
+ * users out of features their own Cloudflare account is already paying for.
75
+ */
76
+ export function hasAiCredentials(): boolean {
77
+ return hasOpenAiKey() || isProxyConfigured();
34
78
  }
35
79
 
36
80
  /**
@@ -0,0 +1,107 @@
1
+ /// <reference lib="dom" />
2
+ /**
3
+ * Ai-managed-connect.ts — the keyless "Connect Cloudflare" (Workers AI) option for credentials
4
+ * gates.
5
+ *
6
+ * Companion to ai-credentials-form.ts: managed platforms broker Workers AI on the user's OWN
7
+ * Cloudflare account, so a gate that renders only the key form strands them — they have no key, and
8
+ * nothing on screen offers the hosted OAuth flow that would give them working AI. Both hosts that
9
+ * gate on credentials (the assistant sidebar and the New Project modal's Import/Agent tabs) embed
10
+ * this alongside the form so both real paths are always on offer.
11
+ *
12
+ * State is per-instance (closure-scoped) like the credentials form; the capability probe behind
13
+ * `ensureProbe` is shared module-wide by services/ai-models.ts.
14
+ *
15
+ * @docs studio/ai
16
+ * @license MIT
17
+ */
18
+
19
+ import { html, nothing } from "lit-html";
20
+ import type { TemplateResult } from "lit-html";
21
+ import { getPlatform, hasPlatform } from "../platform";
22
+ import {
23
+ ensureProxyProbe,
24
+ fetchAvailableModels,
25
+ isManagedProxy,
26
+ isProxyConfigured,
27
+ } from "../services/ai-models";
28
+
29
+ export interface ManagedConnectOptions {
30
+ /** Host re-render scheduler — called on connect start/finish and when the probe settles. */
31
+ requestRender: () => void;
32
+ }
33
+
34
+ export interface ManagedConnect {
35
+ /** Whether the keyless path is available and worth offering right now. */
36
+ canOffer: () => boolean;
37
+ /** Fire the shared capability probe, repainting the host when it settles. */
38
+ ensureProbe: () => void;
39
+ /** The CTA block, or `nothing` when the platform cannot broker AI. */
40
+ render: () => TemplateResult | typeof nothing;
41
+ }
42
+
43
+ /**
44
+ * Create a managed-connect controller bound to a host's render scheduler.
45
+ *
46
+ * @param {ManagedConnectOptions} opts
47
+ * @returns {ManagedConnect}
48
+ */
49
+ export function createManagedConnect(opts: ManagedConnectOptions): ManagedConnect {
50
+ let busy = false;
51
+ let connectError = "";
52
+
53
+ /*
54
+ * Offer the keyless path when the proxy says managed-but-unconfigured and the platform can run
55
+ * the hosted OAuth flow (the PAL seam — desktop shells can implement cfConnect later). Guarded on
56
+ * hasPlatform() because gates can render from modules that load before registration.
57
+ */
58
+ function canOffer(): boolean {
59
+ return (
60
+ isManagedProxy() && !isProxyConfigured() && hasPlatform() && Boolean(getPlatform().cfConnect)
61
+ );
62
+ }
63
+
64
+ function ensureProbe() {
65
+ ensureProxyProbe(opts.requestRender);
66
+ }
67
+
68
+ async function connect() {
69
+ if (busy) {
70
+ return;
71
+ }
72
+ busy = true;
73
+ connectError = "";
74
+ opts.requestRender();
75
+ try {
76
+ const connection = await getPlatform().cfConnect?.();
77
+ if (connection) {
78
+ // Re-probe: /models flips to configured once the connection lands, opening the gate.
79
+ await fetchAvailableModels({ force: true });
80
+ } else {
81
+ connectError = "Cloudflare connection was not completed.";
82
+ }
83
+ } catch (error) {
84
+ connectError = error instanceof Error ? error.message : String(error);
85
+ }
86
+ busy = false;
87
+ opts.requestRender();
88
+ }
89
+
90
+ function render(): TemplateResult | typeof nothing {
91
+ if (!canOffer()) {
92
+ return nothing;
93
+ }
94
+ return html`
95
+ <div class="ai-managed-connect">
96
+ <div>Use Workers AI on your own Cloudflare account — no API key needed.</div>
97
+ <sp-button size="s" ?disabled=${busy} @click=${() => void connect()}>
98
+ ${busy ? "Connecting…" : "Connect Cloudflare"}
99
+ </sp-button>
100
+ ${connectError ? html`<div class="ai-managed-connect-error">${connectError}</div>` : nothing}
101
+ <div class="ai-managed-connect-divider">— or bring your own key —</div>
102
+ </div>
103
+ `;
104
+ }
105
+
106
+ return { canOffer, ensureProbe, render };
107
+ }