dsh-image-gen 0.10.0 → 0.10.2

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/lib/client.js CHANGED
@@ -122983,7 +122983,6 @@ img.ProseMirror-separator {
122983
122983
  };
122984
122984
  }, []);
122985
122985
  const [previewLoading, setPreviewLoading] = (0, react.useState)(false);
122986
- const blobCache = (0, react.useMemo)(() => /* @__PURE__ */ new Map(), []);
122987
122986
  const abortControllerRef = (0, react.useRef)(null);
122988
122987
  const filteredItems = (0, react.useMemo)(() => {
122989
122988
  return items.filter((item) => {
@@ -123039,7 +123038,6 @@ img.ProseMirror-separator {
123039
123038
  }
123040
123039
  setPreviewItem(item);
123041
123040
  if (blob) {
123042
- blobCache.set(item.id, blob);
123043
123041
  const url = URL.createObjectURL(blob);
123044
123042
  previewUrlRef.current = url;
123045
123043
  setPreviewBlob(blob);
@@ -123047,15 +123045,6 @@ img.ProseMirror-separator {
123047
123045
  setPreviewLoading(false);
123048
123046
  return;
123049
123047
  }
123050
- const cached = blobCache.get(item.id);
123051
- if (cached) {
123052
- const url = URL.createObjectURL(cached);
123053
- previewUrlRef.current = url;
123054
- setPreviewBlob(cached);
123055
- setPreviewUrl(url);
123056
- setPreviewLoading(false);
123057
- return;
123058
- }
123059
123048
  setPreviewLoading(true);
123060
123049
  setPreviewBlob(null);
123061
123050
  setPreviewUrl(null);
@@ -123063,7 +123052,6 @@ img.ProseMirror-separator {
123063
123052
  abortControllerRef.current = controller;
123064
123053
  fetchAttachmentBlob(item.attachment).then((fetchedBlob) => {
123065
123054
  if (controller.signal.aborted) return;
123066
- blobCache.set(item.id, fetchedBlob);
123067
123055
  const url = URL.createObjectURL(fetchedBlob);
123068
123056
  previewUrlRef.current = url;
123069
123057
  setPreviewBlob(fetchedBlob);
@@ -123327,11 +123315,7 @@ img.ProseMirror-separator {
123327
123315
  if (itemsToDownload.length === 1) {
123328
123316
  const single = itemsToDownload[0];
123329
123317
  if (!single) return;
123330
- let blob = blobCache.get(single.id);
123331
- if (!blob) {
123332
- blob = await fetchAttachmentBlob(single.attachment);
123333
- blobCache.set(single.id, blob);
123334
- }
123318
+ const blob = await fetchAttachmentBlob(single.attachment);
123335
123319
  const objectUrl = URL.createObjectURL(blob);
123336
123320
  downloadBlobUrl(objectUrl, `dsh-${single.provider}-${single.id}.png`);
123337
123321
  URL.revokeObjectURL(objectUrl);
@@ -123344,11 +123328,7 @@ img.ProseMirror-separator {
123344
123328
  const it = itemsToDownload[i];
123345
123329
  if (!it) continue;
123346
123330
  setBatchDownloadProgress(`${i + 1}/${itemsToDownload.length}`);
123347
- let blob = blobCache.get(it.id);
123348
- if (!blob) {
123349
- blob = await fetchAttachmentBlob(it.attachment);
123350
- blobCache.set(it.id, blob);
123351
- }
123331
+ const blob = await fetchAttachmentBlob(it.attachment);
123352
123332
  const arrayBuffer = await blob.arrayBuffer();
123353
123333
  const ext = blob.type === "image/jpeg" ? "jpg" : blob.type === "image/webp" ? "webp" : "png";
123354
123334
  let baseName = `dsh-${it.provider}-${it.id.slice(0, 8)}.${ext}`;
@@ -123752,7 +123732,6 @@ img.ProseMirror-separator {
123752
123732
  onToggleSelect: (e) => handleToggleSelect(item.id, idx, e),
123753
123733
  onRequestDelete: () => requestSingleDelete(item),
123754
123734
  onPreview: (blob) => openPreviewItem(item, blob),
123755
- onBlobLoaded: (b) => blobCache.set(item.id, b),
123756
123735
  onToast: showToast
123757
123736
  }, item.id))
123758
123737
  }) : activeTab === "inspiration" ? (0, react_jsx_runtime.jsx)(InspirationView, {
@@ -124127,39 +124106,37 @@ img.ProseMirror-separator {
124127
124106
  ]
124128
124107
  });
124129
124108
  };
124130
- const GalleryCard = ({ lang, item, index: _index, isManageMode, isSelected, t, onClick, onToggleSelect, onRequestDelete, onPreview: _onPreview, onBlobLoaded, onToast }) => {
124109
+ const GalleryCard = ({ lang, item, index: _index, isManageMode, isSelected, t, onClick, onToggleSelect, onRequestDelete, onPreview: _onPreview, onToast }) => {
124110
+ const [setCardRef, inView] = useInView();
124131
124111
  const [url, setUrl] = (0, react.useState)();
124132
124112
  const [blob, setBlob] = (0, react.useState)();
124133
124113
  const [loading, setLoading] = (0, react.useState)(true);
124134
124114
  const [error, setError] = (0, react.useState)();
124135
124115
  (0, react.useEffect)(() => {
124136
- const controller = new AbortController();
124116
+ if (!inView) return;
124117
+ let active = true;
124137
124118
  let objectUrl;
124138
- fetch(IMAGE_ROUTE, {
124139
- method: "POST",
124140
- signal: controller.signal,
124141
- headers: { "content-type": "application/json" },
124142
- body: JSON.stringify({ attachment: item.attachment })
124143
- }).then(async (response) => {
124144
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
124145
- const resBlob = await response.blob();
124146
- if (controller.signal.aborted) return;
124119
+ setUrl(void 0);
124120
+ setBlob(void 0);
124121
+ setError(void 0);
124122
+ setLoading(true);
124123
+ fetchAttachmentBlob(item.attachment).then((resBlob) => {
124124
+ if (!active) return;
124147
124125
  setBlob(resBlob);
124148
- onBlobLoaded?.(resBlob);
124149
124126
  objectUrl = URL.createObjectURL(resBlob);
124150
124127
  setUrl(objectUrl);
124151
124128
  setLoading(false);
124152
124129
  }).catch((err) => {
124153
- if (!controller.signal.aborted) {
124130
+ if (active) {
124154
124131
  setError(err instanceof Error ? err.message : String(err));
124155
124132
  setLoading(false);
124156
124133
  }
124157
124134
  });
124158
124135
  return () => {
124159
- controller.abort();
124136
+ active = false;
124160
124137
  if (objectUrl) URL.revokeObjectURL(objectUrl);
124161
124138
  };
124162
- }, [item.attachment]);
124139
+ }, [item.attachment.attachmentId, inView]);
124163
124140
  const copyPrompt = async (e) => {
124164
124141
  e.stopPropagation();
124165
124142
  try {
@@ -124190,6 +124167,7 @@ img.ProseMirror-separator {
124190
124167
  };
124191
124168
  const badgeLabel = item.model || item.provider;
124192
124169
  return (0, react_jsx_runtime.jsxs)("div", {
124170
+ ref: setCardRef,
124193
124171
  className: `dsh-ig-gallery-card ${isSelected ? "is-selected" : ""} ${isManageMode ? "is-manage-mode" : ""}`,
124194
124172
  onClick: (e) => onClick(e, blob),
124195
124173
  children: [(0, react_jsx_runtime.jsxs)("div", {
@@ -125642,7 +125620,7 @@ body[data-ds-dark-theme] .dsh-ig-workbench{--ig-danger:#f08c8c;--ig-warn:#e5b76a
125642
125620
  `;
125643
125621
  //#endregion
125644
125622
  //#region lib/types/client/index.js
125645
- console.info(`[dsh-image-gen] canvas bundle 2026-10-28T11:56:09.512Z`);
125623
+ console.info(`[dsh-image-gen] canvas bundle 2026-11-02T02:55:48.656Z`);
125646
125624
  /** Right-sidebar tab-type registration id (DSH 0.1.5 `sidebarRightTabs`). */
125647
125625
  const SIDEBAR_STUDIO_TAB_ID = "dsh-image-gen/studio";
125648
125626
  /** The tab kind: a page type opened by kind, recognizing no resource address. */
@@ -126062,23 +126040,26 @@ body[data-ds-dark-theme] .dsh-ig-workbench{--ig-danger:#f08c8c;--ig-warn:#e5b76a
126062
126040
  .dsh-ig-save:disabled{opacity:.4;cursor:default}
126063
126041
 
126064
126042
  /* Provider list: one expandable row per provider, each saving independently. */
126065
- .dsh-ig-providers{display:flex;flex-direction:column;margin-top:6px}
126066
- .dsh-ig-provider-row{border:0;background:none;padding:14px 0}
126067
- .dsh-ig-provider-row + .dsh-ig-provider-row{border-top:0.5px solid var(--dsw-alias-border-l2,#e8ebf0)}
126068
- .dsh-ig-provider-head-row{display:flex;align-items:center;gap:10px}
126069
- .dsh-ig-provider-head{flex:1;min-width:0;width:100%;appearance:none;border:0;background:none;font:inherit;color:inherit;text-align:left;cursor:pointer;display:flex;align-items:center;gap:10px;padding:0}
126070
- .dsh-ig-channel-add{flex:none;width:30px;padding:4px 0;font-size:16px;line-height:20px}
126043
+ .dsh-ig-providers{display:flex;flex-direction:column;margin-top:12px;overflow:hidden;border:1px solid var(--dsw-alias-border-l2,#e8ebf0);border-radius:10px;background:var(--dsw-alias-bg-layer-3,transparent)}
126044
+ .dsh-ig-provider-row{border:0;background:none;padding:0 14px}
126045
+ .dsh-ig-provider-row + .dsh-ig-provider-row{border-top:1px solid var(--dsw-alias-border-l2,#e8ebf0)}
126046
+ .dsh-ig-provider-head-row{display:flex;align-items:center;gap:8px;min-height:48px}
126047
+ .dsh-ig-provider-head{flex:1;min-width:0;width:100%;min-height:48px;appearance:none;border:0;background:none;font:inherit;color:inherit;text-align:left;cursor:pointer;display:flex;align-items:center;gap:10px;padding:0}
126048
+ .dsh-ig-channel-add.dsh-ig-btn-secondary{display:inline-flex;align-items:center;justify-content:center;flex:none;box-sizing:border-box;width:26px;height:26px;padding:0;border:0;border-radius:7px;background:var(--dsw-alias-bg-layer-2,#f3f4f6);color:var(--dsw-alias-label-tertiary,#7b818b);line-height:1;transition:background .15s,color .15s}
126049
+ .dsh-ig-channel-add.dsh-ig-btn-secondary:hover:not(:disabled){border:0;background:var(--dsw-alias-bg-layer-1,#e5e7eb);color:var(--dsw-alias-label-primary,inherit)}
126050
+ .dsh-ig-channel-add svg{width:14px;height:14px}
126071
126051
  .dsh-ig-provider-head:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#4c78ff);outline-offset:2px;border-radius:var(--dsw-radius-sm,4px)}
126072
- .dsh-ig-provider-head:hover .dsh-ig-provider-name{color:var(--dsw-alias-label-secondary,inherit)}
126073
- .dsh-ig-provider-name{flex:1;min-width:0;font-size:13.5px;font-weight:550;color:var(--dsw-alias-label-primary,inherit)}
126074
- .dsh-ig-provider-chevron{flex:none;color:var(--dsw-alias-label-tertiary,#7b818b);transition:transform .16s;display:inline-flex;align-items:center}
126052
+ .dsh-ig-provider-head:hover .dsh-ig-provider-name{color:var(--dsw-alias-label-primary,inherit)}
126053
+ .dsh-ig-provider-head:hover .dsh-ig-provider-chevron{color:var(--dsw-alias-label-primary,inherit)}
126054
+ .dsh-ig-provider-name{flex:1;min-width:0;font-size:13px;font-weight:550;line-height:20px;color:var(--dsw-alias-label-primary,inherit);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
126055
+ .dsh-ig-provider-chevron{display:inline-flex;align-items:center;justify-content:center;flex:none;width:20px;height:20px;color:var(--dsw-alias-label-tertiary,#7b818b);transition:transform .16s,color .15s}
126075
126056
  .dsh-ig-provider-chevron-open{transform:rotate(180deg)}
126076
- .dsh-ig-provider-body{border-top:0;padding:8px 0 2px}
126077
- .dsh-ig-badge{flex:none;display:inline-flex;align-items:center;gap:5px;font-size:11.5px;line-height:1.6;padding:2px 9px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2,#d7dbe0);color:var(--dsw-alias-label-secondary,inherit);white-space:nowrap;max-width:60%;overflow:hidden;text-overflow:ellipsis}
126078
- .dsh-ig-badge-dot{width:6px;height:6px;border-radius:50%;background:currentColor;flex:none}
126079
- .dsh-ig-badge-ok{border-color:rgba(34,197,94,.45);color:#15803d;background:rgba(34,197,94,.08)}
126080
- .dsh-ig-badge-missing{border-color:rgba(239,68,68,.4);color:#b91c1c;background:rgba(239,68,68,.06)}
126081
- .dsh-ig-badge-neutral{color:var(--dsw-alias-label-tertiary,#7b818b)}
126057
+ .dsh-ig-provider-body{border-top:0;padding:0 0 14px}
126058
+ .dsh-ig-badge{flex:none;display:inline-flex;align-items:center;gap:5px;box-sizing:border-box;min-height:22px;padding:1px 8px;border:1px solid transparent;border-radius:6px;font-size:11px;font-weight:500;line-height:18px;color:var(--dsw-alias-label-secondary,inherit);white-space:nowrap;max-width:60%;overflow:hidden;text-overflow:ellipsis}
126059
+ .dsh-ig-badge-dot{width:5px;height:5px;border-radius:50%;background:currentColor;flex:none}
126060
+ .dsh-ig-badge-ok{border-color:rgba(34,197,94,.2);color:#15803d;background:rgba(34,197,94,.08)}
126061
+ .dsh-ig-badge-missing{border-color:rgba(239,68,68,.18);color:#b91c1c;background:rgba(239,68,68,.06)}
126062
+ .dsh-ig-badge-neutral{border-color:var(--dsw-alias-border-l2,#e8ebf0);color:var(--dsw-alias-label-tertiary,#7b818b);background:var(--dsw-alias-bg-layer-2,transparent)}
126082
126063
  .dsh-ig-badge-checking .dsh-ig-badge-dot{animation:dsh-ig-pulse 1s ease-in-out infinite}
126083
126064
  @keyframes dsh-ig-pulse{50%{opacity:.25}}
126084
126065
 
@@ -128414,15 +128395,7 @@ body[data-ds-dark-theme] .dsh-ig-workbench{--ig-danger:#f08c8c;--ig-warn:#e5b76a
128414
128395
  className: "dsh-ig-provider-row",
128415
128396
  children: [(0, react_jsx_runtime.jsxs)("div", {
128416
128397
  className: "dsh-ig-provider-head-row",
128417
- children: [provider === "openai-compat" ? (0, react_jsx_runtime.jsx)("button", {
128418
- type: "button",
128419
- className: "dsh-ig-btn-secondary dsh-ig-channel-add",
128420
- title: t("addCompatChannel"),
128421
- "aria-label": t("addCompatChannel"),
128422
- onClick: addCompatChannel,
128423
- disabled: !settingsWritable,
128424
- children: "+"
128425
- }) : null, (0, react_jsx_runtime.jsxs)("button", {
128398
+ children: [(0, react_jsx_runtime.jsxs)("button", {
128426
128399
  type: "button",
128427
128400
  className: "dsh-ig-provider-head",
128428
128401
  "aria-expanded": row.expanded,
@@ -128458,7 +128431,23 @@ body[data-ds-dark-theme] .dsh-ig-workbench{--ig-danger:#f08c8c;--ig-warn:#e5b76a
128458
128431
  })
128459
128432
  })
128460
128433
  ]
128461
- })]
128434
+ }), provider === "openai-compat" ? (0, react_jsx_runtime.jsx)("button", {
128435
+ type: "button",
128436
+ className: "dsh-ig-btn-secondary dsh-ig-channel-add",
128437
+ title: t("addCompatChannel"),
128438
+ "aria-label": t("addCompatChannel"),
128439
+ onClick: addCompatChannel,
128440
+ disabled: !settingsWritable,
128441
+ children: (0, react_jsx_runtime.jsx)("svg", {
128442
+ viewBox: "0 0 16 16",
128443
+ fill: "none",
128444
+ stroke: "currentColor",
128445
+ strokeWidth: "1.6",
128446
+ strokeLinecap: "round",
128447
+ "aria-hidden": "true",
128448
+ children: (0, react_jsx_runtime.jsx)("path", { d: "M8 3v10M3 8h10" })
128449
+ })
128450
+ }) : null]
128462
128451
  }), row.expanded ? provider === "comfyui" ? renderComfyUIBody() : isSubscriptionProvider(provider) ? renderSubscriptionBody(provider) : renderCloudBody(provider) : null]
128463
128452
  }, provider);
128464
128453
  };
package/lib/index.js CHANGED
@@ -8614,12 +8614,13 @@ async function downloadImage$1(url, provider, input) {
8614
8614
  mediaType: detectImageMediaType(data) ?? imageMediaType$2(parsed.mediaType) ?? "image/png"
8615
8615
  };
8616
8616
  }
8617
+ const sameOrigin = new URL(url).origin === new URL(input.baseURL).origin;
8617
8618
  let response = await guardedFetch(`${provider} image download`, input.signal, () => fetch(url, {
8618
8619
  redirect: "follow",
8619
8620
  signal: input.signal,
8620
- ...input.apiKey === void 0 ? {} : { headers: { authorization: `Bearer ${input.apiKey}` } }
8621
+ ...input.apiKey === void 0 || !sameOrigin ? {} : { headers: { authorization: `Bearer ${input.apiKey}` } }
8621
8622
  }), input.apiKey);
8622
- if (!response.ok && input.apiKey !== void 0) response = await guardedFetch(`${provider} image download`, input.signal, () => fetch(url, {
8623
+ if (!response.ok && input.apiKey !== void 0 && sameOrigin) response = await guardedFetch(`${provider} image download`, input.signal, () => fetch(url, {
8623
8624
  redirect: "follow",
8624
8625
  signal: input.signal
8625
8626
  }), input.apiKey);
@@ -8875,6 +8876,7 @@ function emailFromToken(token) {
8875
8876
  async function formTokenRequest(url, params, fetchImpl) {
8876
8877
  const response = await fetchImpl(url, {
8877
8878
  method: "POST",
8879
+ redirect: "error",
8878
8880
  headers: {
8879
8881
  "Content-Type": "application/x-www-form-urlencoded",
8880
8882
  Accept: "application/json"
@@ -8883,8 +8885,15 @@ async function formTokenRequest(url, params, fetchImpl) {
8883
8885
  signal: AbortSignal.timeout(25e3)
8884
8886
  });
8885
8887
  if (!response.ok) {
8886
- const text = await response.text().catch(() => "");
8887
- throw new Error(`token endpoint HTTP ${String(response.status)}${text.length > 0 ? `: ${text.slice(0, 200)}` : ""}`);
8888
+ const detail = redactSecrets(await response.text().catch(() => ""), ...Object.entries(params).flatMap(([key, value]) => [
8889
+ "access_token",
8890
+ "refresh_token",
8891
+ "id_token",
8892
+ "code",
8893
+ "code_verifier",
8894
+ "client_secret"
8895
+ ].includes(key) ? [value] : [])).slice(0, 200);
8896
+ throw new Error(`token endpoint HTTP ${String(response.status)}${detail.length > 0 ? `: ${detail}` : ""}`);
8888
8897
  }
8889
8898
  try {
8890
8899
  const parsed = await response.json();
@@ -9928,7 +9937,8 @@ var SubscriptionManager = class {
9928
9937
  const payload = await response.json().catch(() => ({}));
9929
9938
  if (!response.ok) {
9930
9939
  const detail = extractErrorMessage(payload);
9931
- throw new Error(`${vendor} HTTP ${String(response.status)}${detail !== void 0 ? `: ${detail.slice(0, 200)}` : ""}`);
9940
+ const safeDetail = detail === void 0 ? void 0 : redactSecrets(detail, session.accessToken, session.refreshToken, session.accountId, session.email).slice(0, 200);
9941
+ throw new Error(`${vendor} HTTP ${String(response.status)}${safeDetail !== void 0 ? `: ${safeDetail}` : ""}`);
9932
9942
  }
9933
9943
  return parseImages(payload);
9934
9944
  }
@@ -10244,6 +10254,19 @@ const SUBSCRIPTION_CAPABILITIES = {
10244
10254
  }], "auto", "standard")
10245
10255
  };
10246
10256
  //#endregion
10257
+ //#region lib/types/web-route.js
10258
+ function withWebAdmission(route, admission) {
10259
+ return {
10260
+ ...route,
10261
+ handler: (req, res) => {
10262
+ const rejection = admission()?.(req);
10263
+ if (rejection === void 0) return route.handler(req, res);
10264
+ res.writeHead(rejection, { "content-type": "application/json; charset=utf-8" });
10265
+ res.end(JSON.stringify({ error: rejection === 401 ? "unauthorized" : "forbidden" }));
10266
+ }
10267
+ };
10268
+ }
10269
+ //#endregion
10247
10270
  //#region lib/types/subscription/subscription-route.js
10248
10271
  const MAX_BODY_BYTES$1 = 16384;
10249
10272
  /** The vendor each subscription provider logs in as. */
@@ -10253,21 +10276,21 @@ function vendorOfProvider(provider) {
10253
10276
  return "grok";
10254
10277
  }
10255
10278
  /** Register the login/status routes on the plugin's web server. */
10256
- function registerSubscriptionRoutes(ctx, manager) {
10257
- ctx.effect(() => ctx.webServer.register({
10279
+ function registerSubscriptionRoutes(ctx, manager, admission = () => void 0) {
10280
+ ctx.effect(() => ctx.webServer.register(withWebAdmission({
10258
10281
  kind: "exact",
10259
10282
  path: SUBSCRIPTION_LOGIN_ROUTE,
10260
10283
  handler: (req, res) => {
10261
10284
  serveLogin(req, res, manager);
10262
10285
  }
10263
- }), "dsh-image-gen: subscription login route");
10264
- ctx.effect(() => ctx.webServer.register({
10286
+ }, admission)), "dsh-image-gen: subscription login route");
10287
+ ctx.effect(() => ctx.webServer.register(withWebAdmission({
10265
10288
  kind: "exact",
10266
10289
  path: SUBSCRIPTION_STATUS_ROUTE,
10267
10290
  handler: (req, res) => {
10268
10291
  serveStatus(req, res, manager);
10269
10292
  }
10270
- }), "dsh-image-gen: subscription status route");
10293
+ }, admission)), "dsh-image-gen: subscription status route");
10271
10294
  }
10272
10295
  async function serveLogin(req, res, manager) {
10273
10296
  if (!sameOrigin$3(req)) return jsonError$3(res, 403, "origin-rejected");
@@ -28787,7 +28810,7 @@ async function fetchClassifiedJson(url, headers, apiKey, signal) {
28787
28810
  method: "GET",
28788
28811
  headers,
28789
28812
  signal: signal ?? AbortSignal.timeout(PROBE_TIMEOUT_MS),
28790
- redirect: "follow"
28813
+ redirect: "error"
28791
28814
  }).catch((error) => {
28792
28815
  throw new Error(error instanceof Error ? error.message : String(error));
28793
28816
  });
@@ -29021,8 +29044,10 @@ function serveSettingsHealth(req, res, health) {
29021
29044
  //#endregion
29022
29045
  //#region lib/types/image-tool-context.js
29023
29046
  /** Live, credential-free instructions for the conversation model's image tools. */
29024
- function imageToolContext(config) {
29047
+ function imageToolContext(config, configuredProviders) {
29025
29048
  const provider = config.provider ?? "google";
29049
+ const configured = configuredProviders === void 0 ? void 0 : [...configuredProviders];
29050
+ if (configured !== void 0 && !configured.includes("comfyui") && resolveComfyUIWorkflows(config).length > 0) configured.push("comfyui");
29026
29051
  let model = "not configured";
29027
29052
  let editModel = model;
29028
29053
  try {
@@ -29033,6 +29058,7 @@ function imageToolContext(config) {
29033
29058
  } catch {}
29034
29059
  return [
29035
29060
  `Image tools current default provider: ${provider}; generation model: ${JSON.stringify(model)}; editing model: ${JSON.stringify(editModel)}.`,
29061
+ ...configured === void 0 ? [] : [`Configured image providers (credential saved, subscription logged in, or ComfyUI workflow present): ${configured.length > 0 ? configured.join(", ") : "none"}. Do not select unlisted providers; configuration presence does not guarantee service reachability.`],
29036
29062
  "Unless the user explicitly requests a different image provider or model, OMIT provider and model from generate_image, generate_images and edit_image. Do not guess a channel or switch channels after an error.",
29037
29063
  "Omit unused optional arguments entirely; never fill them with empty strings. size and aspect_ratio are alternatives: provide at most one.",
29038
29064
  "Subscription parameters: chatgpt-sub accepts size OR aspect_ratio, but NOT image_size. grok-sub image_size: 1K or 2K. google-sub image_size: 1K or 4K. Do not copy parameters from another provider.",
@@ -29088,25 +29114,42 @@ function apply(ctx, config = {}) {
29088
29114
  const knownWorkspaceRoots = /* @__PURE__ */ new Set();
29089
29115
  const canvasMirror = new CanvasMirror();
29090
29116
  const subscriptionManager = new SubscriptionManager(ctx);
29117
+ let configuredImageProviders;
29118
+ let imageContextRevision = 0;
29119
+ const refreshImageToolContext = async () => {
29120
+ const revision = ++imageContextRevision;
29121
+ const config = current();
29122
+ try {
29123
+ const providers = (await describeStudio(ctx, config, subscriptionManager)).providers.filter((profile) => profile.configured).map((profile) => profile.provider === "openai-compat" && profile.channel !== void 0 ? `${profile.provider} (channel: ${profile.channel})` : profile.provider);
29124
+ if (revision === imageContextRevision) configuredImageProviders = providers;
29125
+ } catch {}
29126
+ };
29091
29127
  const settingsHealth = installImageSettings(ctx, current(), {
29092
29128
  setSource: (source) => {
29093
29129
  current = () => migrateOpenAICompatConfig(plainConfig(source()));
29094
29130
  },
29095
- onChange: () => {}
29131
+ onChange: () => {
29132
+ refreshImageToolContext();
29133
+ }
29134
+ });
29135
+ let webAdmission;
29136
+ ctx.inject(["connection"], (connectionCtx) => {
29137
+ webAdmission = (request) => connectionCtx.connection.requestRejection(request);
29096
29138
  });
29097
29139
  ctx.inject(["webServer"], (webCtx) => {
29098
- registerSubscriptionRoutes(webCtx, subscriptionManager);
29099
- webCtx.effect(() => webCtx.webServer.register({
29140
+ const register = (route) => webCtx.webServer.register(withWebAdmission(route, () => webAdmission));
29141
+ registerSubscriptionRoutes(webCtx, subscriptionManager, () => webAdmission);
29142
+ webCtx.effect(() => register({
29100
29143
  kind: "exact",
29101
29144
  path: SETTINGS_HEALTH_ROUTE,
29102
29145
  handler: (req, res) => serveSettingsHealth(req, res, settingsHealth())
29103
29146
  }), "dsh-image-gen: settings health");
29104
- webCtx.effect(() => webCtx.webServer.register({
29147
+ webCtx.effect(() => register({
29105
29148
  kind: "exact",
29106
29149
  path: IMAGE_ROUTE,
29107
29150
  handler: (req, res) => serveImage(req, res, { readImage: (ref) => ctx.attachments.readImage(ref) })
29108
29151
  }), "dsh-image-gen: image route");
29109
- webCtx.effect(() => webCtx.webServer.register({
29152
+ webCtx.effect(() => register({
29110
29153
  kind: "exact",
29111
29154
  path: IMPORT_ROUTE,
29112
29155
  handler: (req, res) => serveImport(req, res, {
@@ -29115,7 +29158,7 @@ function apply(ctx, config = {}) {
29115
29158
  mediaTypes: ctx.attachments.imageLimits.mediaTypes
29116
29159
  })
29117
29160
  }), "dsh-image-gen: import route");
29118
- webCtx.effect(() => webCtx.webServer.register({
29161
+ webCtx.effect(() => register({
29119
29162
  kind: "exact",
29120
29163
  path: DELETE_ROUTE,
29121
29164
  handler: (req, res) => serveDelete(req, res, { deleteWorkspaceImage: async (filePath) => {
@@ -29127,7 +29170,7 @@ function apply(ctx, config = {}) {
29127
29170
  ]));
29128
29171
  } })
29129
29172
  }), "dsh-image-gen: delete route");
29130
- webCtx.effect(() => webCtx.webServer.register({
29173
+ webCtx.effect(() => register({
29131
29174
  kind: "exact",
29132
29175
  path: SAVE_WORKSPACE_ROUTE,
29133
29176
  handler: (req, res) => serveSaveWorkspace(req, res, {
@@ -29154,7 +29197,7 @@ function apply(ctx, config = {}) {
29154
29197
  isSaveEnabled: () => current().saveToWorkspace !== false
29155
29198
  })
29156
29199
  }), "dsh-image-gen: save workspace route");
29157
- webCtx.effect(() => webCtx.webServer.register({
29200
+ webCtx.effect(() => register({
29158
29201
  kind: "exact",
29159
29202
  path: TEST_CONNECTION_ROUTE,
29160
29203
  handler: (req, res) => serveTestConnection(req, res, {
@@ -29163,7 +29206,7 @@ function apply(ctx, config = {}) {
29163
29206
  subscriptionManager
29164
29207
  })
29165
29208
  }), "dsh-image-gen: test connection route");
29166
- webCtx.effect(() => webCtx.webServer.register({
29209
+ webCtx.effect(() => register({
29167
29210
  kind: "exact",
29168
29211
  path: CANVAS_STATE_ROUTE,
29169
29212
  handler: (req, res) => serveCanvasState(req, res, {
@@ -29172,7 +29215,7 @@ function apply(ctx, config = {}) {
29172
29215
  maxImageBytes: ctx.attachments.imageLimits.maxImageBytes
29173
29216
  })
29174
29217
  }), "dsh-image-gen: canvas state route");
29175
- webCtx.effect(() => webCtx.webServer.register({
29218
+ webCtx.effect(() => register({
29176
29219
  kind: "exact",
29177
29220
  path: CANVAS_ASSET_ROUTE,
29178
29221
  handler: (req, res) => serveCanvasAsset(req, res, {
@@ -29182,10 +29225,19 @@ function apply(ctx, config = {}) {
29182
29225
  }), "dsh-image-gen: canvas asset route");
29183
29226
  });
29184
29227
  ctx.inject(["systemPrompt"], (promptCtx) => {
29228
+ refreshImageToolContext();
29229
+ ctx.inject(["credentials"], (credentialCtx) => {
29230
+ credentialCtx.on("credentials/reference-updated", () => {
29231
+ refreshImageToolContext();
29232
+ });
29233
+ credentialCtx.on("credentials/record-updated", () => {
29234
+ refreshImageToolContext();
29235
+ });
29236
+ });
29185
29237
  promptCtx.effect(() => promptCtx.systemPrompt.context({
29186
29238
  name: "dsh-image-gen:image-defaults",
29187
29239
  order: 59,
29188
- text: () => imageToolContext(current())
29240
+ text: () => imageToolContext(current(), configuredImageProviders)
29189
29241
  }), "dsh-image-gen: image tool context");
29190
29242
  promptCtx.effect(() => promptCtx.systemPrompt.context({
29191
29243
  name: "dsh-image-gen:canvas",
@@ -29199,7 +29251,8 @@ function apply(ctx, config = {}) {
29199
29251
  name: "canvas-selection"
29200
29252
  }) });
29201
29253
  ctx.inject(["webServer"], (webCtx) => {
29202
- webCtx.effect(() => webCtx.webServer.register({
29254
+ const register = (route) => webCtx.webServer.register(withWebAdmission(route, () => webAdmission));
29255
+ webCtx.effect(() => register({
29203
29256
  kind: "exact",
29204
29257
  path: STUDIO_ROUTE,
29205
29258
  handler: (req, res) => serveStudio(req, res, {
@@ -29222,7 +29275,7 @@ function apply(ctx, config = {}) {
29222
29275
  })
29223
29276
  }), "dsh-image-gen: studio route");
29224
29277
  const serveInspiration = createInspirationRoute();
29225
- webCtx.effect(() => webCtx.webServer.register({
29278
+ webCtx.effect(() => register({
29226
29279
  kind: "prefix",
29227
29280
  path: INSPIRATION_ROUTE,
29228
29281
  handler: (req, res) => {
@@ -29360,7 +29413,7 @@ function apply(ctx, config = {}) {
29360
29413
  },
29361
29414
  model: {
29362
29415
  type: "string",
29363
- description: "Optional model name for this call only, overriding the configured model. Not used by ComfyUI (use workflow instead) nor by the subscription providers (model fixed by the subscription)."
29416
+ description: "OMIT unless the user explicitly requests a specific model; omission uses the configured model. Optional override for this call only. Not used by ComfyUI (use workflow instead) nor by the subscription providers (model fixed by the subscription)."
29364
29417
  },
29365
29418
  channel: {
29366
29419
  type: "string",
@@ -29439,7 +29492,7 @@ function apply(ctx, config = {}) {
29439
29492
  },
29440
29493
  model: {
29441
29494
  type: "string",
29442
- description: "Optional model name for this call only, applied to every item."
29495
+ description: "OMIT unless the user explicitly requests a specific model for every item; omission uses the configured model."
29443
29496
  },
29444
29497
  channel: {
29445
29498
  type: "string",
@@ -29667,7 +29720,7 @@ function apply(ctx, config = {}) {
29667
29720
  },
29668
29721
  model: {
29669
29722
  type: "string",
29670
- description: "Optional model name for this call only, overriding the configured model. Not used by ComfyUI (use workflow instead) nor by the subscription providers (model fixed by the subscription)."
29723
+ description: "OMIT unless the user explicitly requests a specific model; omission uses the configured model. Optional override for this call only. Not used by ComfyUI (use workflow instead) nor by the subscription providers (model fixed by the subscription)."
29671
29724
  },
29672
29725
  channel: {
29673
29726
  type: "string",
@@ -40,6 +40,7 @@ export declare const StudioView: FC<{
40
40
  onInitialReferenceApplied?(): void;
41
41
  onOpenInspiration?(): void;
42
42
  }>;
43
+ export declare function useInView<T extends HTMLElement>(): [(node: T | null) => void, boolean];
43
44
  export declare function useAttachmentImage(attachment: ImageAttachmentRef | undefined, shouldLoad?: boolean): {
44
45
  url: string | null;
45
46
  blob: Blob | null;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Delivery of a drawing plan to the workbench canvases.
3
+ *
4
+ * The `draw_reference` tool cannot put path data inline: the plan is tens of
5
+ * kilobytes and the conversation is persisted, so the tool result carries only
6
+ * a `traceId` (see presentationMeta) and this bridge hands that id to whichever
7
+ * canvas is mounted. The canvas then fetches the plan from the trace-plan route
8
+ * and plays it.
9
+ *
10
+ * A trace that arrives while no canvas is mounted is held, not dropped: the
11
+ * canvas surfaces are tab-scoped and unmount when the user looks elsewhere, so
12
+ * dropping on absence would mean the drawing only ever plays if the user
13
+ * happened to be staring at the infinite canvas at that exact second. The
14
+ * queue holds the newest trace only — a drawing is a one-shot show, and an
15
+ * older plan is stale by the time a newer one exists.
16
+ */
17
+ /** The identifying part of a trace result, as it arrives on a tool event. */
18
+ export interface TlTraceItem {
19
+ traceId: string;
20
+ /** Source pixel size, for the overlay's initial aspect box. */
21
+ sourceWidth: number;
22
+ sourceHeight: number;
23
+ /** Region count, for the "N blocks" caption. */
24
+ steps: number;
25
+ }
26
+ type TlTraceConsumer = (item: TlTraceItem) => void;
27
+ /** Whether any canvas is mounted to play a plan. */
28
+ export declare function hasTlTraceConsumer(): boolean;
29
+ /** The trace waiting for a canvas, if any. */
30
+ export declare function pendingTlTrace(): TlTraceItem | null;
31
+ /**
32
+ * Register a canvas surface; returns its disposer. A trace that arrived while
33
+ * nothing was mounted plays immediately, so opening the canvas after asking
34
+ * for a drawing still shows it.
35
+ */
36
+ export declare function registerTlTraceConsumer(consumer: TlTraceConsumer): () => void;
37
+ /** Hand a plan to every mounted canvas, or hold the newest one until one is. */
38
+ export declare function pushTlTrace(item: TlTraceItem): void;
39
+ /** Test seam: clear every consumer and the pending slot. */
40
+ export declare function resetTlTraceBridge(): void;
41
+ /**
42
+ * Parse the plugin-owned trace metadata off a Tool result. Returns undefined
43
+ * for anything that is not a draw_reference result, so callers can share one
44
+ * event subscription with the image landings.
45
+ */
46
+ export declare function traceResultFromMeta(value: unknown): TlTraceItem | undefined;
47
+ /**
48
+ * Recover a trace from a `tool/ptc-dispatch` event, when the model called
49
+ * draw_reference through run_code instead of directly.
50
+ */
51
+ export declare function traceResultFromPtcDispatch(event: {
52
+ readonly type: string;
53
+ readonly data: Record<string, unknown>;
54
+ }): TlTraceItem | undefined;
55
+ export {};