dsh-image-gen 0.6.1 → 0.6.7

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 (47) hide show
  1. package/README.en.md +381 -345
  2. package/README.md +381 -345
  3. package/README.zh-CN.md +3 -3
  4. package/docs/assets/readme/canvas-edit.webp +0 -0
  5. package/docs/assets/readme/canvas-generate.webp +0 -0
  6. package/docs/assets/readme/hero-en.webp +0 -0
  7. package/docs/assets/readme/hero-zh.webp +0 -0
  8. package/docs/assets/readme/workflow-overview-en.webp +0 -0
  9. package/docs/assets/readme/workflow-overview.webp +0 -0
  10. package/lib/client.js +1046 -374
  11. package/lib/client.js.map +1 -1
  12. package/lib/index.js +1421 -103
  13. package/lib/types/client/index.d.ts +4 -0
  14. package/lib/types/client/inspiration-api.d.ts +10 -0
  15. package/lib/types/client/inspiration-detail.d.ts +14 -0
  16. package/lib/types/client/provider-pill.d.ts +1 -1
  17. package/lib/types/client/tl/canvas-sync.d.ts +3 -3
  18. package/lib/types/client/tl/conversation-landings.d.ts +33 -0
  19. package/lib/types/client/tl/studio-tl-canvas.d.ts +2 -0
  20. package/lib/types/client/tl/tl-canvas-bridge.d.ts +12 -32
  21. package/lib/types/client/tldraw/generated/tldraw-assets.d.ts +12 -0
  22. package/lib/types/client/tldraw/generated/tldraw-css.d.ts +1 -0
  23. package/lib/types/client/tldraw/tldraw-style.d.ts +1 -0
  24. package/lib/types/client/tldraw/tldraw-workspace.d.ts +1 -0
  25. package/lib/types/config.d.ts +24 -0
  26. package/lib/types/inspiration/cache.d.ts +20 -0
  27. package/lib/types/inspiration/downloader.d.ts +27 -0
  28. package/lib/types/inspiration/routes.d.ts +13 -0
  29. package/lib/types/inspiration/sources.d.ts +33 -0
  30. package/lib/types/inspiration/types.d.ts +65 -0
  31. package/lib/types/inspiration/updater.d.ts +7 -0
  32. package/lib/types/openai-compatible.d.ts +15 -0
  33. package/lib/types/shared.d.ts +16 -1
  34. package/lib/types/subscription/blob.d.ts +23 -0
  35. package/lib/types/subscription/loopback.d.ts +11 -0
  36. package/lib/types/subscription/manager.d.ts +94 -0
  37. package/lib/types/subscription/oauth.d.ts +25 -0
  38. package/lib/types/subscription/subscription-route.d.ts +5 -0
  39. package/lib/types/subscription/vendors/antigravity.d.ts +66 -0
  40. package/lib/types/subscription/vendors/codex.d.ts +30 -0
  41. package/lib/types/subscription/vendors/grok.d.ts +32 -0
  42. package/lib/types/subscription.d.ts +42 -0
  43. package/lib/types/test-route.d.ts +9 -0
  44. package/package.json +8 -10
  45. package/skills/install-dsh-image-gen/SKILL.md +22 -22
  46. package/lib/types/client/studio-chat-panel.d.ts +0 -23
  47. package/lib/types/studio-chat.d.ts +0 -48
package/lib/client.js CHANGED
@@ -87,6 +87,8 @@ window.__ModuleLoader__.load({
87
87
  const TEST_CONNECTION_ROUTE = "/plugins/dsh-image-gen/test";
88
88
  /** Browser route the workbench infinite canvas pushes live state through. */
89
89
  const CANVAS_STATE_ROUTE = "/plugins/dsh-image-gen/canvas-state";
90
+ const SUBSCRIPTION_LOGIN_ROUTE = "/plugins/dsh-image-gen/subscription-login";
91
+ const SUBSCRIPTION_STATUS_ROUTE = "/plugins/dsh-image-gen/subscription-status";
90
92
  /** Namespace persisted through DSH Settings. */
91
93
  const IMAGE_GENERATION_NAMESPACE = "image-generation";
92
94
  /** Supported providers. */
@@ -98,8 +100,25 @@ window.__ModuleLoader__.load({
98
100
  "dashscope",
99
101
  "xai",
100
102
  "zhipu",
101
- "comfyui"
103
+ "comfyui",
104
+ "chatgpt-sub",
105
+ "grok-sub",
106
+ "google-sub"
102
107
  ];
108
+ /**
109
+ * Providers that generate through a logged-in subscription account instead
110
+ * of an API key. They use no credential reference and never join the
111
+ * cloud/BYOK sets.
112
+ */
113
+ const SUBSCRIPTION_PROVIDERS = [
114
+ "chatgpt-sub",
115
+ "grok-sub",
116
+ "google-sub"
117
+ ];
118
+ /** True when the provider generates through a logged-in subscription account. */
119
+ function isSubscriptionProvider(provider) {
120
+ return SUBSCRIPTION_PROVIDERS.includes(provider);
121
+ }
103
122
  /** Providers supported by the first browser workbench release. */
104
123
  const CLOUD_IMAGE_PROVIDERS = [
105
124
  "google",
@@ -184,6 +203,12 @@ window.__ModuleLoader__.load({
184
203
  if (!existing.includes(candidate)) return candidate;
185
204
  }
186
205
  }
206
+ /** Default models for the subscription channels; fixed by the bridge protocol. */
207
+ const DEFAULT_SUBSCRIPTION_MODELS = {
208
+ "chatgpt-sub": "gpt-image-2.5-flare",
209
+ "grok-sub": "grok-imagine-image-2.0",
210
+ "google-sub": "gemini-3-pro-image"
211
+ };
187
212
  const DEFAULT_MODELS = {
188
213
  google: DEFAULT_GOOGLE_MODEL,
189
214
  openai: DEFAULT_OPENAI_MODEL,
@@ -192,7 +217,10 @@ window.__ModuleLoader__.load({
192
217
  dashscope: DEFAULT_DASHSCOPE_MODEL,
193
218
  xai: DEFAULT_XAI_MODEL,
194
219
  zhipu: DEFAULT_ZHIPU_MODEL,
195
- comfyui: DEFAULT_COMFYUI_WORKFLOW_LABEL
220
+ comfyui: DEFAULT_COMFYUI_WORKFLOW_LABEL,
221
+ "chatgpt-sub": DEFAULT_SUBSCRIPTION_MODELS["chatgpt-sub"],
222
+ "grok-sub": DEFAULT_SUBSCRIPTION_MODELS["grok-sub"],
223
+ "google-sub": DEFAULT_SUBSCRIPTION_MODELS["google-sub"]
196
224
  };
197
225
  const DEFAULT_BASE_URLS = {
198
226
  google: DEFAULT_GOOGLE_ENDPOINT,
@@ -202,7 +230,10 @@ window.__ModuleLoader__.load({
202
230
  dashscope: DEFAULT_DASHSCOPE_ENDPOINT,
203
231
  xai: DEFAULT_XAI_BASE_URL,
204
232
  zhipu: DEFAULT_ZHIPU_BASE_URL,
205
- comfyui: DEFAULT_COMFYUI_BASE_URL
233
+ comfyui: DEFAULT_COMFYUI_BASE_URL,
234
+ "chatgpt-sub": "",
235
+ "grok-sub": "",
236
+ "google-sub": ""
206
237
  };
207
238
  //#endregion
208
239
  //#region lib/types/comfyui-workflow.js
@@ -312,9 +343,9 @@ window.__ModuleLoader__.load({
312
343
  }
313
344
  });
314
345
  }
315
- const listeners$1 = /* @__PURE__ */ new Set();
346
+ const listeners = /* @__PURE__ */ new Set();
316
347
  function notifyListeners() {
317
- for (const listener of listeners$1) try {
348
+ for (const listener of listeners) try {
318
349
  listener();
319
350
  } catch (err) {
320
351
  console.error("[dsh-image-gen] Gallery listener error:", err);
@@ -324,9 +355,9 @@ window.__ModuleLoader__.load({
324
355
  * Subscribe to gallery mutations (insert/delete/clear).
325
356
  */
326
357
  function subscribeGallery(listener) {
327
- listeners$1.add(listener);
358
+ listeners.add(listener);
328
359
  return () => {
329
- listeners$1.delete(listener);
360
+ listeners.delete(listener);
330
361
  };
331
362
  }
332
363
  /**
@@ -1409,6 +1440,11 @@ window.__ModuleLoader__.load({
1409
1440
  cache.delete(key);
1410
1441
  }
1411
1442
  }
1443
+ /** Clear all in-memory image caches. */
1444
+ function clearAttachmentCache() {
1445
+ cache.clear();
1446
+ currentCacheBytes = 0;
1447
+ }
1412
1448
  function enforceLimits() {
1413
1449
  while (cache.size > 30 || currentCacheBytes > 134217728) {
1414
1450
  const oldestKey = cache.keys().next().value;
@@ -76011,14 +76047,14 @@ img.ProseMirror-separator {
76011
76047
  });
76012
76048
  var useReactId = react[" useId ".trim().toString()] || (() => void 0);
76013
76049
  var count$1 = 0;
76014
- function useId(deterministicId) {
76050
+ function useId$1(deterministicId) {
76015
76051
  const [id, setId] = react.useState(useReactId());
76016
76052
  useLayoutEffect2(() => {
76017
76053
  if (!deterministicId) setId((reactId) => reactId ?? String(count$1++));
76018
76054
  }, [deterministicId]);
76019
76055
  return deterministicId || (id ? `radix-${id}` : "");
76020
76056
  }
76021
- __name$25(useId, "useId");
76057
+ __name$25(useId$1, "useId");
76022
76058
  //#endregion
76023
76059
  //#region node_modules/.pnpm/@radix-ui+react-direction@1_8310f062454398ef3b849e7d93575bcb/node_modules/@radix-ui/react-direction/dist/index.mjs
76024
76060
  var __defProp$27 = Object.defineProperty;
@@ -78639,9 +78675,9 @@ img.ProseMirror-separator {
78639
78675
  scope: __scopeDialog,
78640
78676
  triggerRef,
78641
78677
  contentRef,
78642
- contentId: useId(),
78643
- titleId: useId(),
78644
- descriptionId: useId(),
78678
+ contentId: useId$1(),
78679
+ titleId: useId$1(),
78680
+ descriptionId: useId$1(),
78645
78681
  titlePresent: titleCount > 0,
78646
78682
  descriptionPresent: descriptionCount > 0,
78647
78683
  setTitleCount,
@@ -80913,7 +80949,7 @@ img.ProseMirror-separator {
80913
80949
  var ITEM_NAME$3 = "RovingFocusGroupItem";
80914
80950
  var RovingFocusGroupItem = /* @__PURE__ */ react.forwardRef(/* @__PURE__ */ __name$13(function RovingFocusGroupItem2(props, forwardedRef) {
80915
80951
  const { __scopeRovingFocusGroup, focusable = true, active = false, tabStopId, children, ...itemProps } = props;
80916
- const autoId = useId();
80952
+ const autoId = useId$1();
80917
80953
  const id = tabStopId || autoId;
80918
80954
  const context = useRovingFocusContext(ITEM_NAME$3, __scopeRovingFocusGroup);
80919
80955
  const isCurrentTabStop = context.currentTabStopId === id;
@@ -81472,8 +81508,8 @@ img.ProseMirror-separator {
81472
81508
  onContentChange: setContent,
81473
81509
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MenuSubProvider, {
81474
81510
  scope: __scopeMenu,
81475
- contentId: useId(),
81476
- triggerId: useId(),
81511
+ contentId: useId$1(),
81512
+ triggerId: useId$1(),
81477
81513
  trigger,
81478
81514
  onTriggerChange: setTrigger,
81479
81515
  children
@@ -81931,9 +81967,9 @@ img.ProseMirror-separator {
81931
81967
  });
81932
81968
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DropdownMenuProvider, {
81933
81969
  scope: __scopeDropdownMenu,
81934
- triggerId: useId(),
81970
+ triggerId: useId$1(),
81935
81971
  triggerRef,
81936
- contentId: useId(),
81972
+ contentId: useId$1(),
81937
81973
  open,
81938
81974
  onOpenChange: setOpen,
81939
81975
  onOpenToggle: react.useCallback(() => setOpen((prevOpen) => !prevOpen), [setOpen]),
@@ -82165,7 +82201,7 @@ img.ProseMirror-separator {
82165
82201
  ...popperScope,
82166
82202
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PopoverProvider, {
82167
82203
  scope: __scopePopover,
82168
- contentId: useId(),
82204
+ contentId: useId$1(),
82169
82205
  triggerRef,
82170
82206
  open,
82171
82207
  onOpenChange: setOpen,
@@ -82397,7 +82433,7 @@ img.ProseMirror-separator {
82397
82433
  ]);
82398
82434
  const isFormControl = trigger ? !!form || !!trigger.closest("form") : true;
82399
82435
  const [nativeOptionsSet, setNativeOptionsSet] = react.useState(/* @__PURE__ */ new Set());
82400
- const contentId = useId();
82436
+ const contentId = useId$1();
82401
82437
  const nativeSelectKey = Array.from(nativeOptionsSet).map((option) => option.props.value).join(";");
82402
82438
  const handleNativeOptionAdd = react.useCallback((option) => {
82403
82439
  setNativeOptionsSet((prev) => new Set(prev).add(option));
@@ -83966,7 +84002,7 @@ img.ProseMirror-separator {
83966
84002
  const popperScope = usePopperScope(__scopeTooltip);
83967
84003
  const [trigger, setTrigger] = react.useState(null);
83968
84004
  const [contentIdState, setContentId] = react.useState(void 0);
83969
- const generatedContentId = useId();
84005
+ const generatedContentId = useId$1();
83970
84006
  const openTimerRef = react.useRef(0);
83971
84007
  const disableHoverableContent = disableHoverableContentProp ?? providerContext.disableHoverableContent;
83972
84008
  const delayDuration = delayDurationProp ?? providerContext.delayDuration;
@@ -108087,7 +108123,7 @@ img.ProseMirror-separator {
108087
108123
  }
108088
108124
  const MAX_SIZE = 256;
108089
108125
  const alphaCache = new LruCache(100);
108090
- const pending$1 = /* @__PURE__ */ new Set();
108126
+ const pending$2 = /* @__PURE__ */ new Set();
108091
108127
  let offscreenCanvas = null;
108092
108128
  function getOffscreenCanvas(w, h) {
108093
108129
  if (!offscreenCanvas) offscreenCanvas = new OffscreenCanvas(w, h);
@@ -108107,12 +108143,12 @@ img.ProseMirror-separator {
108107
108143
  function preloadAlphaData(url, cacheKey) {
108108
108144
  if (typeof OffscreenCanvas === "undefined") return;
108109
108145
  const key = cacheKey ?? url;
108110
- if (alphaCache.has(key) || pending$1.has(key)) return;
108111
- pending$1.add(key);
108146
+ if (alphaCache.has(key) || pending$2.has(key)) return;
108147
+ pending$2.add(key);
108112
108148
  const img = Image$1();
108113
108149
  img.crossOrigin = "anonymous";
108114
108150
  img.onload = async () => {
108115
- pending$1.delete(key);
108151
+ pending$2.delete(key);
108116
108152
  const { width: origW, height: origH } = img;
108117
108153
  if (origW === 0 || origH === 0) return;
108118
108154
  const scale = Math.min(1, MAX_SIZE / Math.max(origW, origH));
@@ -108139,7 +108175,7 @@ img.ProseMirror-separator {
108139
108175
  });
108140
108176
  };
108141
108177
  img.onerror = () => {
108142
- pending$1.delete(key);
108178
+ pending$2.delete(key);
108143
108179
  };
108144
108180
  img.src = url;
108145
108181
  }
@@ -115591,43 +115627,6 @@ img.ProseMirror-separator {
115591
115627
  });
115592
115628
  }
115593
115629
  //#endregion
115594
- //#region lib/types/client/tl/tl-canvas-bridge.js
115595
- const items = [];
115596
- const listeners = /* @__PURE__ */ new Set();
115597
- /** Queue freshly generated images for the tldraw canvases. Safe to call anywhere. */
115598
- function pushTlLandings(newItems) {
115599
- if (newItems.length === 0) return;
115600
- const known = new Set(items.map((item) => item.galleryId));
115601
- let pushed = 0;
115602
- for (const item of newItems) {
115603
- if (known.has(item.galleryId)) continue;
115604
- items.push(item);
115605
- known.add(item.galleryId);
115606
- pushed += 1;
115607
- }
115608
- if (pushed === 0) return;
115609
- for (const listener of listeners) listener();
115610
- }
115611
- /**
115612
- * Read every item landed so far in this browser session.
115613
- *
115614
- * The list is the in-memory session-scratch record shared by all canvases:
115615
- * reading never removes items, so a canvas mounted later (or a second canvas
115616
- * mounted beside the first) reconciles the same set. The list dies with the
115617
- * page, which is exactly the "unsaved content does not survive a restart"
115618
- * semantic the studio canvas keeps.
115619
- */
115620
- function getTlLandings() {
115621
- return items;
115622
- }
115623
- /** Subscribe to queue pushes. Returns an unsubscribe function. */
115624
- function subscribeTlLandings(listener) {
115625
- listeners.add(listener);
115626
- return () => {
115627
- listeners.delete(listener);
115628
- };
115629
- }
115630
- //#endregion
115631
115630
  //#region lib/types/client/tl/canvas-sync.js
115632
115631
  /** Debounce for ordinary document/session changes. */
115633
115632
  const STATE_DEBOUNCE_MS = 600;
@@ -115677,21 +115676,18 @@ img.ProseMirror-separator {
115677
115676
  * the selection identity list, so a selected node reads exactly like its
115678
115677
  * canvas node twin (same name, attachment id, dimensions).
115679
115678
  */
115680
- function describeShape(editor, shape, attachmentByGallery) {
115679
+ function describeShape(editor, shape) {
115681
115680
  const node = { kind: kindOf(shape.type) };
115682
115681
  if (shape.type === "image") {
115683
115682
  const meta = shape.meta;
115684
115683
  const galleryId = meta?.galleryId;
115685
- if (typeof galleryId === "string" && galleryId.length > 0) {
115686
- node.galleryId = galleryId;
115687
- const attachmentId = attachmentByGallery.get(galleryId);
115688
- if (attachmentId !== void 0) node.attachmentId = attachmentId;
115689
- }
115684
+ if (typeof galleryId === "string" && galleryId.length > 0) node.galleryId = galleryId;
115685
+ if (typeof meta?.attachmentId === "string" && meta.attachmentId.length > 0) node.attachmentId = meta.attachmentId;
115690
115686
  if (typeof meta?.prompt === "string" && meta.prompt.trim().length > 0) node.prompt = meta.prompt.replace(/\s+/g, " ").trim().slice(0, 120);
115691
115687
  if (typeof meta?.provider === "string" && meta.provider.length > 0) node.provider = meta.provider;
115692
115688
  if (typeof meta?.model === "string" && meta.model.length > 0) node.model = meta.model;
115693
115689
  const asset = shape.props.assetId === null ? void 0 : editor.getAsset(shape.props.assetId);
115694
- const assetName = asset !== void 0 && asset.type === "image" ? asset.props.name : void 0;
115690
+ const assetName = typeof meta?.name === "string" && meta.name.length > 0 ? meta.name : asset !== void 0 && asset.type === "image" ? asset.props.name : void 0;
115695
115691
  if (typeof assetName === "string" && assetName.length > 0) node.name = assetName.slice(0, 128);
115696
115692
  if (asset !== void 0 && asset.type === "image") {
115697
115693
  node.width = Math.round(asset.props.w);
@@ -115714,31 +115710,19 @@ img.ProseMirror-separator {
115714
115710
  return node;
115715
115711
  }
115716
115712
  function summarize(editor) {
115717
- const attachmentByGallery = /* @__PURE__ */ new Map();
115718
- const conversationGallery = /* @__PURE__ */ new Set();
115719
- for (const landing of getTlLandings()) {
115720
- attachmentByGallery.set(landing.galleryId, String(landing.attachment.attachmentId));
115721
- if (landing.fromConversation) conversationGallery.add(landing.galleryId);
115722
- }
115723
115713
  const shapes = editor.getCurrentPageShapes();
115724
115714
  const nodes = [];
115725
- for (const shape of shapes.slice(0, 48)) nodes.push(describeShape(editor, shape, attachmentByGallery));
115715
+ for (const shape of shapes.slice(0, 48)) nodes.push(describeShape(editor, shape));
115726
115716
  const selected = editor.getSelectedShapes();
115727
115717
  const selectionKinds = [...new Set(selected.map((shape) => kindOf(shape.type)))].slice(0, 8);
115728
- const selectionItems = selected.slice(0, 16).map((shape) => describeShape(editor, shape, attachmentByGallery));
115718
+ const selectionItems = selected.slice(0, 16).map((shape) => describeShape(editor, shape));
115729
115719
  const selectionKey = selectionKeyOf(editor);
115730
- const selectionAllConversationImages = selected.length > 0 && selected.every((shape) => {
115731
- if (shape.type !== "image") return false;
115732
- const galleryId = shape.meta?.galleryId;
115733
- return typeof galleryId === "string" && conversationGallery.has(galleryId);
115734
- });
115735
115720
  return {
115736
115721
  nodeCount: shapes.length,
115737
115722
  nodes,
115738
115723
  selectionCount: selected.length,
115739
115724
  selectionKinds,
115740
115725
  selectionItems,
115741
- selectionAllConversationImages,
115742
115726
  stateKey: JSON.stringify([
115743
115727
  shapes.length,
115744
115728
  nodes,
@@ -115805,7 +115789,7 @@ img.ProseMirror-separator {
115805
115789
  return;
115806
115790
  }
115807
115791
  let selectionImage;
115808
- if (withScreenshot && summary.selectionCount > 0 && !summary.selectionAllConversationImages) try {
115792
+ if (withScreenshot && summary.selectionCount > 0) try {
115809
115793
  const selectedBefore = editor.getSelectedShapes();
115810
115794
  const { url } = await editor.toImageDataUrl(selectedBefore, {
115811
115795
  background: true,
@@ -115813,6 +115797,7 @@ img.ProseMirror-separator {
115813
115797
  scale: 1,
115814
115798
  pixelRatio: 2
115815
115799
  });
115800
+ if (disposed) return;
115816
115801
  const selectedAfter = editor.getSelectedShapes();
115817
115802
  const idsBefore = new Set(selectedBefore.map((shape) => String(shape.id)));
115818
115803
  if (!(selectedBefore.length === selectedAfter.length && selectedAfter.every((shape) => idsBefore.has(String(shape.id))))) return;
@@ -115855,6 +115840,10 @@ img.ProseMirror-separator {
115855
115840
  stateTimer = void 0;
115856
115841
  flush(false);
115857
115842
  }, STATE_DEBOUNCE_MS);
115843
+ if (editor.getSelectedShapeIds().length > 0) exportTimer = setTimeout(() => {
115844
+ exportTimer = void 0;
115845
+ flush(true);
115846
+ }, SELECTION_EXPORT_DELAY_MS);
115858
115847
  return () => {
115859
115848
  if (disposed) return;
115860
115849
  disposed = true;
@@ -115877,25 +115866,81 @@ img.ProseMirror-separator {
115877
115866
  };
115878
115867
  }
115879
115868
  //#endregion
115869
+ //#region lib/types/client/tl/tl-canvas-bridge.js
115870
+ const pending$1 = /* @__PURE__ */ new Map();
115871
+ const consumers = /* @__PURE__ */ new Set();
115872
+ let delivering = false;
115873
+ let generation = 0;
115874
+ /** Invalidates async landings that started before the user cleared the canvas. */
115875
+ function getTlLandingGeneration() {
115876
+ return generation;
115877
+ }
115878
+ function clearTlLandings() {
115879
+ generation += 1;
115880
+ pending$1.clear();
115881
+ }
115882
+ /** Queue freshly generated images for the tldraw canvases. Safe to call anywhere. */
115883
+ function pushTlLandings(newItems) {
115884
+ for (const item of newItems) if (!pending$1.has(item.galleryId)) pending$1.set(item.galleryId, item);
115885
+ deliver();
115886
+ }
115887
+ /** Conversation results are admitted only while a canvas is mounted. */
115888
+ function pushTlLandingsLive(items) {
115889
+ if (consumers.size > 0) pushTlLandings(items);
115890
+ }
115891
+ /** Register a ready editor. False means it unmounted before committing. */
115892
+ function registerTlLandingConsumer(consumer) {
115893
+ consumers.add(consumer);
115894
+ deliver();
115895
+ return () => {
115896
+ consumers.delete(consumer);
115897
+ if (consumers.size === 0) {
115898
+ for (const [id, item] of pending$1) if (item.fromConversation) pending$1.delete(id);
115899
+ }
115900
+ };
115901
+ }
115902
+ async function deliver() {
115903
+ if (delivering) return;
115904
+ delivering = true;
115905
+ try {
115906
+ while (pending$1.size > 0) {
115907
+ const consumer = consumers.values().next().value;
115908
+ if (consumer === void 0) break;
115909
+ const batch = [...pending$1.values()];
115910
+ let completed = true;
115911
+ try {
115912
+ completed = await consumer(batch);
115913
+ } catch (error) {
115914
+ console.warn("[dsh-image-gen] canvas landing failed:", error);
115915
+ }
115916
+ if (completed) {
115917
+ for (const item of batch) if (pending$1.get(item.galleryId) === item) pending$1.delete(item.galleryId);
115918
+ }
115919
+ }
115920
+ } finally {
115921
+ delivering = false;
115922
+ }
115923
+ }
115924
+ //#endregion
115880
115925
  //#region lib/types/client/tl/studio-tl-canvas.js
115881
115926
  /**
115882
115927
  * Infinite canvas surface for the Studio workbench, backed by tldraw.
115883
115928
  *
115884
- * Generated images are pushed onto the tl-canvas-bridge landing bus (studio
115885
- * form paths and chat tool cards land the same way) and become in-memory
115886
- * image assets with data-URL sources. The bus broadcasts, so this surface
115887
- * RECONCILES instead of draining: it lands the bus items its own page does
115888
- * not carry yet. The conversation-view gallery tab and the right-sidebar
115889
- * studio tab can both be mounted, and each mirrors the full session-scratch
115890
- * set independently. The surface deliberately runs WITHOUT persistenceKey:
115891
- * unsaved generations vanish on restart, matching the plugin's save-first
115892
- * philosophy; data-URL assets therefore never touch disk. Shapes carry
115893
- * `meta.galleryId` so re-landing the same generation is a no-op. One batch
115894
- * = one createShapes transaction = one undo step.
115929
+ * Both surfaces share one locally persisted document. The landing queue is
115930
+ * consumed once; tldraw owns subsequent edits, deletion, undo and persistence.
115895
115931
  */
115896
115932
  const MAX_DISPLAY_SIDE = 380;
115897
115933
  const GRID_GAP = 40;
115898
115934
  const GRID_COLS = 3;
115935
+ const mountedCanvases = /* @__PURE__ */ new Set();
115936
+ /** Reset canvas-owned data only. Gallery records and host attachments are untouched. */
115937
+ function clearStudioTlCanvases() {
115938
+ if (mountedCanvases.size === 0) return false;
115939
+ clearTlLandings();
115940
+ for (const clear of mountedCanvases) clear();
115941
+ clearAttachmentCache();
115942
+ return true;
115943
+ }
115899
115944
  /**
115900
115945
  * Dock the stock toolbar vertically along the left edge (Figma-style) instead
115901
115946
  * of tldraw's signature bottom-center pill. It is still the same
@@ -115917,11 +115962,11 @@ img.ProseMirror-separator {
115917
115962
  h: Math.round(height * scale)
115918
115963
  };
115919
115964
  }
115920
- /** Gallery ids already on one page (image shapes' `meta.galleryId`). */
115965
+ /** Dedupe across the whole document, including images moved to other pages. */
115921
115966
  function landedIdsOf(editor) {
115922
115967
  const landedIds = /* @__PURE__ */ new Set();
115923
- for (const shape of editor.getCurrentPageShapes()) {
115924
- if (shape.type !== "image") continue;
115968
+ for (const shape of editor.store.allRecords()) {
115969
+ if (shape.typeName !== "shape" || shape.type !== "image") continue;
115925
115970
  const galleryId = shape.meta?.galleryId;
115926
115971
  if (typeof galleryId === "string") landedIds.add(galleryId);
115927
115972
  }
@@ -115933,13 +115978,14 @@ img.ProseMirror-separator {
115933
115978
  const collapsed = prompt.replace(/\s+/g, " ").trim();
115934
115979
  return collapsed.length === 0 ? void 0 : collapsed.slice(0, 120);
115935
115980
  }
115936
- async function landTlItems(editor, items, failedIds) {
115981
+ async function landTlItems(editor, items, isActive) {
115937
115982
  const landedIds = landedIdsOf(editor);
115938
115983
  const pending = items.filter((item) => !landedIds.has(item.galleryId));
115939
- if (pending.length === 0) return;
115984
+ if (pending.length === 0) return true;
115940
115985
  const assets = [];
115941
115986
  const landed = [];
115942
115987
  for (const item of pending) {
115988
+ if (!isActive()) return false;
115943
115989
  const assetId = tlAssetIdFor(item.galleryId);
115944
115990
  if (editor.getAsset(assetId) === void 0) try {
115945
115991
  const blob = await fetchAttachmentBlob(item.attachment);
@@ -115960,7 +116006,6 @@ img.ProseMirror-separator {
115960
116006
  }
115961
116007
  });
115962
116008
  } catch (error) {
115963
- failedIds.add(item.galleryId);
115964
116009
  console.warn("[dsh-image-gen] canvas landing skipped (attachment unreadable):", item.galleryId, error);
115965
116010
  continue;
115966
116011
  }
@@ -115969,6 +116014,9 @@ img.ProseMirror-separator {
115969
116014
  landed.push({
115970
116015
  galleryId: item.galleryId,
115971
116016
  assetId,
116017
+ attachmentId: String(item.attachment.attachmentId),
116018
+ name: item.attachment.name ?? item.galleryId,
116019
+ fromConversation: item.fromConversation === true,
115972
116020
  w: size.w,
115973
116021
  h: size.h,
115974
116022
  ...prompt !== void 0 ? { prompt } : {},
@@ -115976,10 +116024,13 @@ img.ProseMirror-separator {
115976
116024
  ...typeof item.model === "string" && item.model.length > 0 ? { model: item.model } : {}
115977
116025
  });
115978
116026
  }
115979
- if (landed.length === 0) return;
116027
+ if (!isActive()) return false;
116028
+ const nowLanded = landedIdsOf(editor);
116029
+ const ready = landed.filter((item) => !nowLanded.has(item.galleryId));
116030
+ if (ready.length === 0) return true;
115980
116031
  if (assets.length > 0) editor.createAssets(assets);
115981
116032
  const rows = [];
115982
- for (let index = 0; index < landed.length; index += GRID_COLS) rows.push(landed.slice(index, index + GRID_COLS));
116033
+ for (let index = 0; index < ready.length; index += GRID_COLS) rows.push(ready.slice(index, index + GRID_COLS));
115983
116034
  const rowSizes = rows.map((row) => ({
115984
116035
  width: row.reduce((sum, cell) => sum + cell.w, 0) + GRID_GAP * (row.length - 1),
115985
116036
  height: Math.max(...row.map((cell) => cell.h))
@@ -116014,7 +116065,7 @@ img.ProseMirror-separator {
116014
116065
  let cursorX = originX + (blockWidth - rowSize.width) / 2;
116015
116066
  for (const cell of row) {
116016
116067
  shapes.push({
116017
- id: createShapeId(),
116068
+ id: createShapeId(`ig-${cell.galleryId}`),
116018
116069
  type: "image",
116019
116070
  x: Math.round(cursorX),
116020
116071
  y: Math.round(cursorY),
@@ -116025,6 +116076,9 @@ img.ProseMirror-separator {
116025
116076
  },
116026
116077
  meta: {
116027
116078
  galleryId: cell.galleryId,
116079
+ attachmentId: cell.attachmentId,
116080
+ name: cell.name,
116081
+ fromConversation: cell.fromConversation,
116028
116082
  ...cell.prompt !== void 0 ? { prompt: cell.prompt } : {},
116029
116083
  ...cell.provider !== void 0 ? { provider: cell.provider } : {},
116030
116084
  ...cell.model !== void 0 ? { model: cell.model } : {}
@@ -116050,42 +116104,53 @@ img.ProseMirror-separator {
116050
116104
  inset: GRID_GAP
116051
116105
  });
116052
116106
  }
116107
+ return true;
116053
116108
  }
116054
116109
  const StudioTlCanvas = (0, react.memo)(function StudioTlCanvas() {
116055
- const editorRef = (0, react.useRef)(null);
116056
- const landingRef = (0, react.useRef)(false);
116057
- /** Per-canvas attachment misses, so one bad blob cannot spin the reconcile. */
116058
- const failedRef = (0, react.useRef)(/* @__PURE__ */ new Set());
116059
- const processQueue = (0, react.useCallback)(() => {
116060
- const editor = editorRef.current;
116061
- if (editor === null) return;
116062
- if (landingRef.current) return;
116063
- const landedIds = landedIdsOf(editor);
116064
- const items = getTlLandings().filter((item) => !landedIds.has(item.galleryId) && !failedRef.current.has(item.galleryId));
116065
- if (items.length === 0) return;
116066
- landingRef.current = true;
116067
- landTlItems(editor, items, failedRef.current).catch((error) => {
116068
- console.warn("[dsh-image-gen] tldraw canvas landing failed:", error);
116069
- }).finally(() => {
116070
- landingRef.current = false;
116071
- processQueue();
116072
- });
116073
- }, []);
116074
- (0, react.useEffect)(() => subscribeTlLandings(processQueue), [processQueue]);
116110
+ const sessionId = (0, react.useId)();
116075
116111
  return (0, react_jsx_runtime.jsx)("div", {
116076
116112
  className: "dsh-ig-tl-canvas",
116077
116113
  children: (0, react_jsx_runtime.jsx)(Tldraw, {
116114
+ persistenceKey: "dsh-image-gen-workbench-v1",
116115
+ sessionId,
116078
116116
  components: { Toolbar: StudioToolbar },
116079
116117
  onMount: (editor) => {
116080
- editorRef.current = editor;
116118
+ let active = true;
116081
116119
  applyBrandTheme(editor);
116082
116120
  editor.updateInstanceState({ isGridMode: true });
116083
116121
  console.info(`[dsh-image-gen] tldraw mounted (instance ${editor.id})`);
116084
- processQueue();
116085
- const stopSync = startCanvasSync(editor);
116122
+ const stopLanding = registerTlLandingConsumer((items) => {
116123
+ const generation = getTlLandingGeneration();
116124
+ return landTlItems(editor, items, () => active && generation === getTlLandingGeneration());
116125
+ });
116126
+ let stopSync = startCanvasSync(editor);
116127
+ const clear = () => {
116128
+ stopSync();
116129
+ editor.complete();
116130
+ editor.run(() => {
116131
+ editor.selectNone();
116132
+ const pages = editor.getPages();
116133
+ for (const page of pages) editor.deleteShapes([...editor.getPageShapeIds(page.id)]);
116134
+ for (const page of pages.slice(1)) editor.deletePage(page.id);
116135
+ editor.deleteAssets(editor.getAssets());
116136
+ editor.setCamera({
116137
+ x: 0,
116138
+ y: 0,
116139
+ z: 1
116140
+ });
116141
+ }, {
116142
+ history: "ignore",
116143
+ ignoreShapeLock: true
116144
+ });
116145
+ editor.clearHistory();
116146
+ stopSync = startCanvasSync(editor);
116147
+ };
116148
+ mountedCanvases.add(clear);
116086
116149
  return () => {
116150
+ active = false;
116151
+ mountedCanvases.delete(clear);
116152
+ stopLanding();
116087
116153
  stopSync();
116088
- editorRef.current = null;
116089
116154
  };
116090
116155
  }
116091
116156
  })
@@ -116196,7 +116261,10 @@ img.ProseMirror-separator {
116196
116261
  expandGenerate: "展开生成面板",
116197
116262
  generatePanel: "生成面板",
116198
116263
  findInspiration: "找灵感",
116199
- infiniteCanvasHint: "无限画布与侧边栏会话联动,才是完整功能。打开 DSH 右侧边栏的「图像工作台」即可解锁。"
116264
+ infiniteCanvasHint: "无限画布与侧边栏会话联动,才是完整功能。打开 DSH 右侧边栏的「图像工作台」即可解锁。",
116265
+ clearCanvas: "清空画布",
116266
+ clearCanvasDescription: "将永久清空所有画布页面的内容、图片资产和撤销历史。画廊已保存的卡片及原图不受影响,此操作无法撤销。",
116267
+ canvasCleared: "画布已清空,画廊不受影响"
116200
116268
  },
116201
116269
  en: {
116202
116270
  title: "Cloud Image Studio",
@@ -116298,7 +116366,10 @@ img.ProseMirror-separator {
116298
116366
  expandGenerate: "Expand generate panel",
116299
116367
  generatePanel: "Generate",
116300
116368
  findInspiration: "Find inspiration",
116301
- infiniteCanvasHint: "The infinite canvas is at its best when linked with the sidebar conversation. Open Image Studio from the DSH right sidebar to unlock the full experience."
116369
+ infiniteCanvasHint: "The infinite canvas is at its best when linked with the sidebar conversation. Open Image Studio from the DSH right sidebar to unlock the full experience.",
116370
+ clearCanvas: "Clear canvas",
116371
+ clearCanvasDescription: "Permanently clear all canvas pages, image assets, and undo history. Saved gallery cards and original images are unaffected. This cannot be undone.",
116372
+ canvasCleared: "Canvas cleared. Gallery unchanged."
116302
116373
  }
116303
116374
  };
116304
116375
  /** localStorage flag remembering that the one-time infinite-canvas hint was dismissed. */
@@ -116346,6 +116417,15 @@ img.ProseMirror-separator {
116346
116417
  const dragStartRef = (0, react.useRef)(null);
116347
116418
  const hasDraggedRef = (0, react.useRef)(false);
116348
116419
  const [canvasSurface, setCanvasSurface] = (0, react.useState)(initialCanvasSurface ?? "preview");
116420
+ const [showClearCanvasModal, setShowClearCanvasModal] = (0, react.useState)(false);
116421
+ (0, react.useEffect)(() => {
116422
+ if (!showClearCanvasModal) return;
116423
+ const close = (event) => {
116424
+ if (event.key === "Escape") setShowClearCanvasModal(false);
116425
+ };
116426
+ window.addEventListener("keydown", close);
116427
+ return () => window.removeEventListener("keydown", close);
116428
+ }, [showClearCanvasModal]);
116349
116429
  /** Generate-panel state from before the infinite canvas was opened, so the
116350
116430
  * panel is restored (not just forced open) when returning to preview. */
116351
116431
  const generatePanelBeforeInfiniteRef = (0, react.useRef)(false);
@@ -117183,6 +117263,12 @@ img.ProseMirror-separator {
117183
117263
  (0, react_jsx_runtime.jsxs)("div", {
117184
117264
  className: "dsh-ig-canvas-toolbar-right",
117185
117265
  children: [
117266
+ canvasSurface === "infinite" && (0, react_jsx_runtime.jsxs)("button", {
117267
+ type: "button",
117268
+ onClick: () => setShowClearCanvasModal(true),
117269
+ title: t("clearCanvas"),
117270
+ children: [(0, react_jsx_runtime.jsx)(Trash2, { size: 14 }), (0, react_jsx_runtime.jsx)("span", { children: t("clearCanvas") })]
117271
+ }),
117186
117272
  canvasSurface === "preview" && selected !== null && (0, react_jsx_runtime.jsxs)("button", {
117187
117273
  type: "button",
117188
117274
  onClick: startNew,
@@ -117789,6 +117875,57 @@ img.ProseMirror-separator {
117789
117875
  })
117790
117876
  ]
117791
117877
  }),
117878
+ showClearCanvasModal && (0, react_jsx_runtime.jsx)("div", {
117879
+ className: "dsh-ig-workbench-modal-backdrop",
117880
+ onClick: () => setShowClearCanvasModal(false),
117881
+ children: (0, react_jsx_runtime.jsxs)("div", {
117882
+ className: "dsh-ig-workbench-modal-box",
117883
+ role: "dialog",
117884
+ "aria-modal": "true",
117885
+ "aria-label": t("clearCanvas"),
117886
+ onClick: (event) => event.stopPropagation(),
117887
+ children: [
117888
+ (0, react_jsx_runtime.jsxs)("div", {
117889
+ className: "dsh-ig-workbench-modal-header",
117890
+ children: [(0, react_jsx_runtime.jsx)(TriangleAlert, {
117891
+ size: 20,
117892
+ color: "#dc2626"
117893
+ }), (0, react_jsx_runtime.jsx)("strong", { children: t("clearCanvas") })]
117894
+ }),
117895
+ (0, react_jsx_runtime.jsx)("p", {
117896
+ className: "dsh-ig-workbench-modal-desc",
117897
+ children: t("clearCanvasDescription")
117898
+ }),
117899
+ (0, react_jsx_runtime.jsxs)("div", {
117900
+ className: "dsh-ig-workbench-modal-actions",
117901
+ children: [(0, react_jsx_runtime.jsx)("button", {
117902
+ autoFocus: true,
117903
+ type: "button",
117904
+ className: "dsh-ig-workbench-modal-cancel",
117905
+ onClick: () => setShowClearCanvasModal(false),
117906
+ children: t("cancel")
117907
+ }), (0, react_jsx_runtime.jsx)("button", {
117908
+ type: "button",
117909
+ className: "dsh-ig-workbench-modal-danger",
117910
+ onClick: () => {
117911
+ try {
117912
+ if (!clearStudioTlCanvases()) {
117913
+ flash(t("loading"));
117914
+ return;
117915
+ }
117916
+ setShowClearCanvasModal(false);
117917
+ flash(t("canvasCleared"));
117918
+ } catch (error) {
117919
+ setError(messageOf(error));
117920
+ setShowClearCanvasModal(false);
117921
+ }
117922
+ },
117923
+ children: t("clearCanvas")
117924
+ })]
117925
+ })
117926
+ ]
117927
+ })
117928
+ }),
117792
117929
  showDeleteModal && selected !== null && (0, react_jsx_runtime.jsx)("div", {
117793
117930
  className: "dsh-ig-workbench-modal-backdrop",
117794
117931
  onClick: () => setShowDeleteModal(false),
@@ -120678,6 +120815,266 @@ img.ProseMirror-separator {
120678
120815
  if (fromBlockContent?.type === "image") return fromBlockContent.attachment;
120679
120816
  }
120680
120817
  //#endregion
120818
+ //#region lib/types/client/image-result-node.js
120819
+ const IMAGE_RESULT_NODE_KIND = "dsh-image-result";
120820
+ /**
120821
+ * One image result row per Turn, anchored at its own tool/result position while
120822
+ * the Turn runs, then optionally re-anchored beside the final answer once the
120823
+ * Turn closes (only needed under the folding Compact transcript view).
120824
+ */
120825
+ function createImageResultDefinition(options = {}) {
120826
+ return {
120827
+ kind: IMAGE_RESULT_NODE_KIND,
120828
+ target: "chat",
120829
+ match: (event) => {
120830
+ if (event.type === "tool/ptc-dispatch") {
120831
+ const subCallId = event.data.subCallId;
120832
+ if (typeof subCallId !== "string" || subCallId === "") return null;
120833
+ return imageResultFromPtcDispatch(event) === void 0 ? null : {
120834
+ id: `ptc:${subCallId}`,
120835
+ role: "start"
120836
+ };
120837
+ }
120838
+ const turn = eventTurn(event);
120839
+ if (turn === void 0) return null;
120840
+ if (event.type === "turn/start") return {
120841
+ id: String(turn),
120842
+ role: "start"
120843
+ };
120844
+ if (event.type === "turn/end") return {
120845
+ id: String(turn),
120846
+ role: "update"
120847
+ };
120848
+ if (event.type === "assistant/message") return {
120849
+ id: String(turn),
120850
+ role: "update"
120851
+ };
120852
+ if (event.type === "tool/result" && imageResultFromMeta(event.data.meta) !== void 0) return {
120853
+ id: String(turn),
120854
+ role: "update"
120855
+ };
120856
+ return null;
120857
+ },
120858
+ start: (_context, match) => {
120859
+ if (match.event.type === "tool/ptc-dispatch") {
120860
+ const result = imageResultFromPtcDispatch(match.event);
120861
+ if (result === void 0) throw new Error("dsh-image-result start requires a plugin image dispatch");
120862
+ return { results: [{
120863
+ ...result,
120864
+ seq: match.event.seq
120865
+ }] };
120866
+ }
120867
+ const turn = eventTurn(match.event);
120868
+ if (match.event.type !== "turn/start" || turn === void 0) throw new Error("dsh-image-result start requires turn/start");
120869
+ return {
120870
+ turn,
120871
+ results: []
120872
+ };
120873
+ },
120874
+ update: (context, match) => {
120875
+ if (match.event.type === "turn/end") return {
120876
+ ...context.state,
120877
+ endSeq: match.event.seq
120878
+ };
120879
+ if (match.event.type === "assistant/message") return {
120880
+ ...context.state,
120881
+ answerSeq: match.event.seq
120882
+ };
120883
+ if (match.event.type === "tool/ptc-dispatch") {
120884
+ const result = imageResultFromPtcDispatch(match.event);
120885
+ return result === void 0 ? context.state : appendResult(context.state, result, match.event.seq);
120886
+ }
120887
+ if (match.event.type !== "tool/result") return context.state;
120888
+ const result = imageResultFromMeta(match.event.data.meta);
120889
+ if (result === void 0) return context.state;
120890
+ return appendResult(context.state, result, match.event.seq);
120891
+ },
120892
+ buildViewNode: (context) => {
120893
+ const state = context.state;
120894
+ const last = state?.results.at(-1);
120895
+ if (state === void 0 || last === void 0) return null;
120896
+ const location = context.matches.at(-1)?.location ?? context.start?.location ?? { kind: "unresolved" };
120897
+ return {
120898
+ key: context.key,
120899
+ kind: IMAGE_RESULT_NODE_KIND,
120900
+ id: context.id,
120901
+ target: "chat",
120902
+ anchorSeq: state.endSeq !== void 0 && compactTranscript(options) ? state.answerSeq ?? state.endSeq ?? last.seq : last.seq,
120903
+ location,
120904
+ visibility: "visible",
120905
+ data: {
120906
+ ...state.turn !== void 0 ? { turn: state.turn } : {},
120907
+ results: state.results.map(({ seq: _seq, ...result }) => result)
120908
+ }
120909
+ };
120910
+ }
120911
+ };
120912
+ }
120913
+ /** Append a parsed result unless its attachment is already collected. */
120914
+ function appendResult(state, result, seq) {
120915
+ if (state.results.some((candidate) => candidate.attachment.attachmentId === result.attachment.attachmentId)) return state;
120916
+ return {
120917
+ ...state,
120918
+ results: [...state.results, {
120919
+ ...result,
120920
+ seq
120921
+ }]
120922
+ };
120923
+ }
120924
+ function compactTranscript(options) {
120925
+ try {
120926
+ return options.isCompactTranscript?.() ?? true;
120927
+ } catch {
120928
+ return true;
120929
+ }
120930
+ }
120931
+ /** Parse the plugin-owned durable presentation metadata from a Tool result event. */
120932
+ function imageResultFromMeta(value) {
120933
+ const meta = record(value);
120934
+ if (meta?.kind !== "dsh-image-gen") return void 0;
120935
+ const attachment = imageAttachment(meta.attachment);
120936
+ if (attachment === void 0) return void 0;
120937
+ return {
120938
+ attachment,
120939
+ prompt: stringValue(meta.prompt, "Generated Image"),
120940
+ provider: stringValue(meta.provider, "google"),
120941
+ model: stringValue(meta.model, ""),
120942
+ output: stringValue(meta.output, ""),
120943
+ ...typeof meta.savedTo === "string" ? { savedTo: meta.savedTo } : {},
120944
+ ...typeof meta.seed === "number" ? { seed: meta.seed } : {}
120945
+ };
120946
+ }
120947
+ function eventTurn(event) {
120948
+ const turn = event.data.turn;
120949
+ return typeof turn === "number" && Number.isSafeInteger(turn) && turn >= 0 ? turn : void 0;
120950
+ }
120951
+ /** Plugin tools whose PTC (run_code) dispatch results are image results. */
120952
+ const PTC_IMAGE_TOOL_NAMES = /* @__PURE__ */ new Set(["generate_image", "edit_image"]);
120953
+ /**
120954
+ * Fixed summary text rendered by the tools' imageOutput(). PTC dispatch events
120955
+ * carry no presentationMeta, so provider/model/output are recovered from this
120956
+ * template rather than parsed out of natural language.
120957
+ */
120958
+ const PTC_SUMMARY_PATTERN = /^(?:Generated|Edited) one image with ([^/\s]+)\/(\S+) \((.+)\)\. Attachment ID: /;
120959
+ const PTC_SAVED_TO_PATTERN = / It was also saved to the workspace as (.+)\. (?:(?:Saving it to the workspace failed)|(?:It has no local file path)|(?:Respond to the user))/;
120960
+ /**
120961
+ * Parse a plugin image result from a tool/ptc-dispatch event (#38). PTC
120962
+ * run_code sub-calls carry neither a turn nor presentationMeta, so the result
120963
+ * is rebuilt from the dispatch payload itself: the image content block for the
120964
+ * attachment, arguments for the prompt (and edit source ids), and the tool's
120965
+ * fixed-format summary text for provider/model/output. Returns undefined for
120966
+ * anything that is not a successful image result from our own image tools.
120967
+ */
120968
+ function imageResultFromPtcDispatch(event) {
120969
+ if (event.type !== "tool/ptc-dispatch") return void 0;
120970
+ const data = event.data;
120971
+ if (data.isError === true) return void 0;
120972
+ if (typeof data.name !== "string" || !PTC_IMAGE_TOOL_NAMES.has(data.name)) return void 0;
120973
+ const content = Array.isArray(data.content) ? data.content : [];
120974
+ let attachment;
120975
+ let summaryText = "";
120976
+ for (const block of content) {
120977
+ const candidate = record(block);
120978
+ if (candidate === void 0) continue;
120979
+ if (candidate.type === "image" && attachment === void 0) attachment = imageAttachment(candidate.attachment);
120980
+ else if (candidate.type === "text" && summaryText === "" && typeof candidate.text === "string") summaryText = candidate.text;
120981
+ }
120982
+ if (attachment === void 0) return void 0;
120983
+ const summary = PTC_SUMMARY_PATTERN.exec(summaryText);
120984
+ const savedTo = PTC_SAVED_TO_PATTERN.exec(summaryText)?.[1];
120985
+ const args = record(data.arguments);
120986
+ const sourceIds = sourceAttachmentIds(args);
120987
+ return {
120988
+ attachment,
120989
+ prompt: stringValue(args?.prompt, "Generated Image"),
120990
+ provider: summary?.[1] ?? "",
120991
+ model: summary?.[2] ?? "",
120992
+ output: summary?.[3] ?? "",
120993
+ ...savedTo !== void 0 ? { savedTo } : {},
120994
+ ...sourceIds !== void 0 ? { sourceAttachmentIds: sourceIds } : {}
120995
+ };
120996
+ }
120997
+ /** Collect edit_image source attachment ids (single or list form) from dispatch arguments. */
120998
+ function sourceAttachmentIds(args) {
120999
+ if (args === void 0) return void 0;
121000
+ const single = typeof args.source_attachment_id === "string" ? [args.source_attachment_id] : void 0;
121001
+ const list = Array.isArray(args.source_attachment_ids) ? args.source_attachment_ids.filter((id) => typeof id === "string") : void 0;
121002
+ const ids = single ?? list;
121003
+ return ids !== void 0 && ids.length > 0 ? ids : void 0;
121004
+ }
121005
+ function imageAttachment(value) {
121006
+ const candidate = record(value);
121007
+ if (candidate === void 0 || typeof candidate.attachmentId !== "string" || !isImageMediaType(candidate.mediaType) || !positiveInteger(candidate.bytes) || !positiveInteger(candidate.width) || !positiveInteger(candidate.height)) return void 0;
121008
+ const original = record(candidate.originalDimensions);
121009
+ if (candidate.originalDimensions !== void 0 && (original === void 0 || !positiveInteger(original.width) || !positiveInteger(original.height))) return void 0;
121010
+ return {
121011
+ attachmentId: candidate.attachmentId,
121012
+ mediaType: candidate.mediaType,
121013
+ bytes: candidate.bytes,
121014
+ width: candidate.width,
121015
+ height: candidate.height,
121016
+ ...typeof candidate.name === "string" ? { name: candidate.name } : {},
121017
+ ...original === void 0 ? {} : { originalDimensions: {
121018
+ width: original.width,
121019
+ height: original.height
121020
+ } }
121021
+ };
121022
+ }
121023
+ function isImageMediaType(value) {
121024
+ return value === "image/png" || value === "image/jpeg" || value === "image/webp" || value === "image/gif";
121025
+ }
121026
+ function positiveInteger(value) {
121027
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
121028
+ }
121029
+ function stringValue(value, fallback) {
121030
+ return typeof value === "string" ? value : fallback;
121031
+ }
121032
+ function record(value) {
121033
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
121034
+ }
121035
+ //#endregion
121036
+ //#region lib/types/client/tl/conversation-landings.js
121037
+ /** Subscribe to live additions, never to card rendering or history replay. */
121038
+ function startConversationLandings(sessions) {
121039
+ let source;
121040
+ let stopEvents;
121041
+ const followCurrent = () => {
121042
+ const id = sessions.list.getSnapshot().current;
121043
+ const next = id === void 0 ? void 0 : sessions.binding(id)?.eventSource;
121044
+ if (next === source) return;
121045
+ stopEvents?.();
121046
+ source = next;
121047
+ if (next === void 0) return;
121048
+ let lastSeq = next.getSnapshot().entries.reduce((seq, entry) => Math.max(seq, entry.event.seq), -1);
121049
+ stopEvents = next.subscribe(() => {
121050
+ const change = next.getSnapshot().change;
121051
+ for (const entry of change.entries ?? []) {
121052
+ if (entry.type !== "event") continue;
121053
+ const { event } = entry;
121054
+ const isNew = event.seq > lastSeq;
121055
+ lastSeq = Math.max(lastSeq, event.seq);
121056
+ if (change.kind !== "append" || !isNew) continue;
121057
+ const result = event.type === "tool/result" ? imageResultFromMeta(event.data.meta) : imageResultFromPtcDispatch(event);
121058
+ if (result === void 0) continue;
121059
+ pushTlLandingsLive([{
121060
+ galleryId: String(result.attachment.attachmentId),
121061
+ attachment: result.attachment,
121062
+ fromConversation: true,
121063
+ prompt: result.prompt,
121064
+ provider: result.provider,
121065
+ model: result.model
121066
+ }]);
121067
+ }
121068
+ });
121069
+ };
121070
+ const stopList = sessions.list.subscribe(followCurrent);
121071
+ followCurrent();
121072
+ return () => {
121073
+ stopList();
121074
+ stopEvents?.();
121075
+ };
121076
+ }
121077
+ //#endregion
120681
121078
  //#region lib/types/client/studio-style.js
120682
121079
  const STUDIO_STYLE = `
120683
121080
  .dsh-ig-gallery-page-body.is-workbench{flex:1 1 0;height:100%;min-height:0;padding:0;overflow:hidden;display:flex;flex-direction:column}
@@ -121019,224 +121416,6 @@ img.ProseMirror-separator {
121019
121416
  .dsh-ig-inspiration-lightbox{position:fixed;inset:0;z-index:10000;background:rgba(8,12,22,.88);backdrop-filter:blur(8px);display:flex;align-items:center;justify-content:center;padding:24px;box-sizing:border-box}.dsh-ig-inspiration-lightbox-content{position:relative;max-width:90vw;max-height:90vh;display:flex;flex-direction:column;align-items:center}.dsh-ig-inspiration-lightbox-img-wrap{max-width:85vw;max-height:82vh;border-radius:10px;overflow:hidden;box-shadow:0 25px 60px rgba(0,0,0,.7);background:#05070d}.dsh-ig-inspiration-lightbox-img-wrap img{width:auto;height:auto;max-width:85vw;max-height:82vh;object-fit:contain;display:block}.dsh-ig-inspiration-lightbox-close{position:absolute;top:-16px;right:-16px;width:34px;height:34px;border-radius:50%;background:#1e293b;color:#fff;border:1px solid rgba(255,255,255,.2);cursor:pointer;display:grid;place-items:center;transition:background .15s,transform .15s;z-index:10}.dsh-ig-inspiration-lightbox-close:hover{background:#334155;transform:scale(1.1)}.dsh-ig-inspiration-lightbox-caption{margin-top:12px;color:rgba(255,255,255,.9);font-size:13px;font-weight:500;text-shadow:0 2px 4px rgba(0,0,0,.6)}
121020
121417
  @media(max-width:980px){.dsh-ig-inspiration-layout{grid-template-columns:1fr}.dsh-ig-inspiration-inspector{position:relative;top:auto}.dsh-ig-inspiration-inspector-empty{min-height:150px}.dsh-ig-inspiration-inspector-image{max-height:290px}.dsh-ig-inspiration-inspector-image img{object-fit:contain;background:#f1f4fa}}@media(max-width:700px){.dsh-ig-inspiration{padding:14px}.dsh-ig-inspiration-head{align-items:center}.dsh-ig-inspiration-toolbar{grid-template-columns:1fr 1fr}.dsh-ig-inspiration-search{grid-column:1/-1}.dsh-ig-inspiration-fav-filter{grid-column:1/-1}.dsh-ig-inspiration-grid{grid-template-columns:repeat(auto-fill,minmax(145px,1fr));gap:9px}}@media(max-width:420px){.dsh-ig-inspiration-toolbar{grid-template-columns:1fr}.dsh-ig-inspiration-search{grid-column:auto}}
121021
121418
  `;
121022
- //#endregion
121023
- //#region lib/types/client/image-result-node.js
121024
- const IMAGE_RESULT_NODE_KIND = "dsh-image-result";
121025
- /**
121026
- * One image result row per Turn, anchored at its own tool/result position while
121027
- * the Turn runs, then optionally re-anchored beside the final answer once the
121028
- * Turn closes (only needed under the folding Compact transcript view).
121029
- */
121030
- function createImageResultDefinition(options = {}) {
121031
- return {
121032
- kind: IMAGE_RESULT_NODE_KIND,
121033
- target: "chat",
121034
- match: (event) => {
121035
- if (event.type === "tool/ptc-dispatch") {
121036
- const subCallId = event.data.subCallId;
121037
- if (typeof subCallId !== "string" || subCallId === "") return null;
121038
- return imageResultFromPtcDispatch(event) === void 0 ? null : {
121039
- id: `ptc:${subCallId}`,
121040
- role: "start"
121041
- };
121042
- }
121043
- const turn = eventTurn(event);
121044
- if (turn === void 0) return null;
121045
- if (event.type === "turn/start") return {
121046
- id: String(turn),
121047
- role: "start"
121048
- };
121049
- if (event.type === "turn/end") return {
121050
- id: String(turn),
121051
- role: "update"
121052
- };
121053
- if (event.type === "assistant/message") return {
121054
- id: String(turn),
121055
- role: "update"
121056
- };
121057
- if (event.type === "tool/result" && imageResultFromMeta(event.data.meta) !== void 0) return {
121058
- id: String(turn),
121059
- role: "update"
121060
- };
121061
- return null;
121062
- },
121063
- start: (_context, match) => {
121064
- if (match.event.type === "tool/ptc-dispatch") {
121065
- const result = imageResultFromPtcDispatch(match.event);
121066
- if (result === void 0) throw new Error("dsh-image-result start requires a plugin image dispatch");
121067
- return { results: [{
121068
- ...result,
121069
- seq: match.event.seq
121070
- }] };
121071
- }
121072
- const turn = eventTurn(match.event);
121073
- if (match.event.type !== "turn/start" || turn === void 0) throw new Error("dsh-image-result start requires turn/start");
121074
- return {
121075
- turn,
121076
- results: []
121077
- };
121078
- },
121079
- update: (context, match) => {
121080
- if (match.event.type === "turn/end") return {
121081
- ...context.state,
121082
- endSeq: match.event.seq
121083
- };
121084
- if (match.event.type === "assistant/message") return {
121085
- ...context.state,
121086
- answerSeq: match.event.seq
121087
- };
121088
- if (match.event.type === "tool/ptc-dispatch") {
121089
- const result = imageResultFromPtcDispatch(match.event);
121090
- return result === void 0 ? context.state : appendResult(context.state, result, match.event.seq);
121091
- }
121092
- if (match.event.type !== "tool/result") return context.state;
121093
- const result = imageResultFromMeta(match.event.data.meta);
121094
- if (result === void 0) return context.state;
121095
- return appendResult(context.state, result, match.event.seq);
121096
- },
121097
- buildViewNode: (context) => {
121098
- const state = context.state;
121099
- const last = state?.results.at(-1);
121100
- if (state === void 0 || last === void 0) return null;
121101
- const location = context.matches.at(-1)?.location ?? context.start?.location ?? { kind: "unresolved" };
121102
- return {
121103
- key: context.key,
121104
- kind: IMAGE_RESULT_NODE_KIND,
121105
- id: context.id,
121106
- target: "chat",
121107
- anchorSeq: state.endSeq !== void 0 && compactTranscript(options) ? state.answerSeq ?? state.endSeq ?? last.seq : last.seq,
121108
- location,
121109
- visibility: "visible",
121110
- data: {
121111
- ...state.turn !== void 0 ? { turn: state.turn } : {},
121112
- results: state.results.map(({ seq: _seq, ...result }) => result)
121113
- }
121114
- };
121115
- }
121116
- };
121117
- }
121118
- /** Append a parsed result unless its attachment is already collected. */
121119
- function appendResult(state, result, seq) {
121120
- if (state.results.some((candidate) => candidate.attachment.attachmentId === result.attachment.attachmentId)) return state;
121121
- return {
121122
- ...state,
121123
- results: [...state.results, {
121124
- ...result,
121125
- seq
121126
- }]
121127
- };
121128
- }
121129
- function compactTranscript(options) {
121130
- try {
121131
- return options.isCompactTranscript?.() ?? true;
121132
- } catch {
121133
- return true;
121134
- }
121135
- }
121136
- /** Parse the plugin-owned durable presentation metadata from a Tool result event. */
121137
- function imageResultFromMeta(value) {
121138
- const meta = record(value);
121139
- if (meta?.kind !== "dsh-image-gen") return void 0;
121140
- const attachment = imageAttachment(meta.attachment);
121141
- if (attachment === void 0) return void 0;
121142
- return {
121143
- attachment,
121144
- prompt: stringValue(meta.prompt, "Generated Image"),
121145
- provider: stringValue(meta.provider, "google"),
121146
- model: stringValue(meta.model, ""),
121147
- output: stringValue(meta.output, ""),
121148
- ...typeof meta.savedTo === "string" ? { savedTo: meta.savedTo } : {},
121149
- ...typeof meta.seed === "number" ? { seed: meta.seed } : {}
121150
- };
121151
- }
121152
- function eventTurn(event) {
121153
- const turn = event.data.turn;
121154
- return typeof turn === "number" && Number.isSafeInteger(turn) && turn >= 0 ? turn : void 0;
121155
- }
121156
- /** Plugin tools whose PTC (run_code) dispatch results are image results. */
121157
- const PTC_IMAGE_TOOL_NAMES = /* @__PURE__ */ new Set(["generate_image", "edit_image"]);
121158
- /**
121159
- * Fixed summary text rendered by the tools' imageOutput(). PTC dispatch events
121160
- * carry no presentationMeta, so provider/model/output are recovered from this
121161
- * template rather than parsed out of natural language.
121162
- */
121163
- const PTC_SUMMARY_PATTERN = /^(?:Generated|Edited) one image with ([^/\s]+)\/(\S+) \((.+)\)\. Attachment ID: /;
121164
- const PTC_SAVED_TO_PATTERN = / It was also saved to the workspace as (.+)\. (?:(?:Saving it to the workspace failed)|(?:It has no local file path)|(?:Respond to the user))/;
121165
- /**
121166
- * Parse a plugin image result from a tool/ptc-dispatch event (#38). PTC
121167
- * run_code sub-calls carry neither a turn nor presentationMeta, so the result
121168
- * is rebuilt from the dispatch payload itself: the image content block for the
121169
- * attachment, arguments for the prompt (and edit source ids), and the tool's
121170
- * fixed-format summary text for provider/model/output. Returns undefined for
121171
- * anything that is not a successful image result from our own image tools.
121172
- */
121173
- function imageResultFromPtcDispatch(event) {
121174
- if (event.type !== "tool/ptc-dispatch") return void 0;
121175
- const data = event.data;
121176
- if (data.isError === true) return void 0;
121177
- if (typeof data.name !== "string" || !PTC_IMAGE_TOOL_NAMES.has(data.name)) return void 0;
121178
- const content = Array.isArray(data.content) ? data.content : [];
121179
- let attachment;
121180
- let summaryText = "";
121181
- for (const block of content) {
121182
- const candidate = record(block);
121183
- if (candidate === void 0) continue;
121184
- if (candidate.type === "image" && attachment === void 0) attachment = imageAttachment(candidate.attachment);
121185
- else if (candidate.type === "text" && summaryText === "" && typeof candidate.text === "string") summaryText = candidate.text;
121186
- }
121187
- if (attachment === void 0) return void 0;
121188
- const summary = PTC_SUMMARY_PATTERN.exec(summaryText);
121189
- const savedTo = PTC_SAVED_TO_PATTERN.exec(summaryText)?.[1];
121190
- const args = record(data.arguments);
121191
- const sourceIds = sourceAttachmentIds(args);
121192
- return {
121193
- attachment,
121194
- prompt: stringValue(args?.prompt, "Generated Image"),
121195
- provider: summary?.[1] ?? "",
121196
- model: summary?.[2] ?? "",
121197
- output: summary?.[3] ?? "",
121198
- ...savedTo !== void 0 ? { savedTo } : {},
121199
- ...sourceIds !== void 0 ? { sourceAttachmentIds: sourceIds } : {}
121200
- };
121201
- }
121202
- /** Collect edit_image source attachment ids (single or list form) from dispatch arguments. */
121203
- function sourceAttachmentIds(args) {
121204
- if (args === void 0) return void 0;
121205
- const single = typeof args.source_attachment_id === "string" ? [args.source_attachment_id] : void 0;
121206
- const list = Array.isArray(args.source_attachment_ids) ? args.source_attachment_ids.filter((id) => typeof id === "string") : void 0;
121207
- const ids = single ?? list;
121208
- return ids !== void 0 && ids.length > 0 ? ids : void 0;
121209
- }
121210
- function imageAttachment(value) {
121211
- const candidate = record(value);
121212
- if (candidate === void 0 || typeof candidate.attachmentId !== "string" || !isImageMediaType(candidate.mediaType) || !positiveInteger(candidate.bytes) || !positiveInteger(candidate.width) || !positiveInteger(candidate.height)) return void 0;
121213
- const original = record(candidate.originalDimensions);
121214
- if (candidate.originalDimensions !== void 0 && (original === void 0 || !positiveInteger(original.width) || !positiveInteger(original.height))) return void 0;
121215
- return {
121216
- attachmentId: candidate.attachmentId,
121217
- mediaType: candidate.mediaType,
121218
- bytes: candidate.bytes,
121219
- width: candidate.width,
121220
- height: candidate.height,
121221
- ...typeof candidate.name === "string" ? { name: candidate.name } : {},
121222
- ...original === void 0 ? {} : { originalDimensions: {
121223
- width: original.width,
121224
- height: original.height
121225
- } }
121226
- };
121227
- }
121228
- function isImageMediaType(value) {
121229
- return value === "image/png" || value === "image/jpeg" || value === "image/webp" || value === "image/gif";
121230
- }
121231
- function positiveInteger(value) {
121232
- return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
121233
- }
121234
- function stringValue(value, fallback) {
121235
- return typeof value === "string" ? value : fallback;
121236
- }
121237
- function record(value) {
121238
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
121239
- }
121240
121419
  //#endregion
121241
121420
  //#region lib/types/client/conversation-image-revisions.js
121242
121421
  const STORAGE_PREFIX = "dsh_image_gen_conversation_revisions:";
@@ -121322,6 +121501,10 @@ img.ProseMirror-separator {
121322
121501
  keyMissing: "未配置 Key",
121323
121502
  noKeyNeeded: "无需 Key",
121324
121503
  noWorkflow: "未导入工作流",
121504
+ subNoKey: "订阅生图",
121505
+ subSignedIn: "已登录",
121506
+ subSignedOut: "未登录",
121507
+ subStatusUnknown: "状态未知",
121325
121508
  readOnly: "图像设置为只读,无法在此切换",
121326
121509
  switchFailed: "切换失败"
121327
121510
  },
@@ -121332,6 +121515,10 @@ img.ProseMirror-separator {
121332
121515
  keyMissing: "No key",
121333
121516
  noKeyNeeded: "No key needed",
121334
121517
  noWorkflow: "No workflow",
121518
+ subNoKey: "Subscription",
121519
+ subSignedIn: "Signed in",
121520
+ subSignedOut: "Signed out",
121521
+ subStatusUnknown: "Unknown",
121335
121522
  readOnly: "Image settings are read-only; switch them in the config source",
121336
121523
  switchFailed: "Switch failed"
121337
121524
  }
@@ -121345,8 +121532,25 @@ img.ProseMirror-separator {
121345
121532
  dashscope: "DashScope",
121346
121533
  xai: "Grok",
121347
121534
  zhipu: "智谱 GLM",
121348
- comfyui: "ComfyUI"
121349
- };
121535
+ comfyui: "ComfyUI",
121536
+ "chatgpt-sub": "ChatGPT 订阅",
121537
+ "grok-sub": "Grok 订阅",
121538
+ "google-sub": "Google 订阅"
121539
+ };
121540
+ /** Compute the fixed-position anchor from the pill button: open upward when
121541
+ * the chat pane leaves room above the composer, drop down otherwise. */
121542
+ function anchorOf(button) {
121543
+ const rect = button?.getBoundingClientRect();
121544
+ if (rect === void 0) return void 0;
121545
+ if (rect.top >= 332) return {
121546
+ left: rect.left,
121547
+ bottom: window.innerHeight - rect.top + 6
121548
+ };
121549
+ return {
121550
+ left: rect.left,
121551
+ top: rect.bottom + 6
121552
+ };
121553
+ }
121350
121554
  /** Whether the user opted into the composer pill; hidden (not merely inert) when off. */
121351
121555
  function pillVisible(value) {
121352
121556
  return value?.showProviderPill === true;
@@ -121357,6 +121561,7 @@ img.ProseMirror-separator {
121357
121561
  const workflow = activeComfyUIWorkflow(value ?? {});
121358
121562
  return workflow === void 0 ? "" : workflow.name;
121359
121563
  }
121564
+ if (SUBSCRIPTION_PROVIDERS.includes(provider)) return DEFAULT_MODELS[provider];
121360
121565
  const stored = provider === "google" ? value?.googleModel : provider === "openai" ? value?.openaiModel : provider === "openai-compat" ? value?.openaiCompatModel : provider === "seedream" ? value?.seedreamModel : provider === "dashscope" ? value?.dashscopeModel : provider === "xai" ? value?.xaiModel : value?.zhipuModel;
121361
121566
  return typeof stored === "string" && stored.length > 0 ? stored : DEFAULT_MODELS[provider];
121362
121567
  }
@@ -121377,6 +121582,11 @@ img.ProseMirror-separator {
121377
121582
  zhipu: "unknown"
121378
121583
  });
121379
121584
  const [keyTick, setKeyTick] = (0, react.useState)(0);
121585
+ const [subDots, setSubDots] = (0, react.useState)(() => ({
121586
+ "chatgpt-sub": "unknown",
121587
+ "grok-sub": "unknown",
121588
+ "google-sub": "unknown"
121589
+ }));
121380
121590
  const buttonRef = (0, react.useRef)(null);
121381
121591
  const [menuStyle, setMenuStyle] = (0, react.useState)(null);
121382
121592
  (0, react.useEffect)(() => props.scope.subscribe(() => {
@@ -121419,6 +121629,31 @@ img.ProseMirror-separator {
121419
121629
  active = false;
121420
121630
  };
121421
121631
  }, [props.credentials, keyTick]);
121632
+ (0, react.useEffect)(() => {
121633
+ if (!open) return;
121634
+ let active = true;
121635
+ const probe = async () => {
121636
+ try {
121637
+ const response = await fetch(SUBSCRIPTION_STATUS_ROUTE, {
121638
+ method: "POST",
121639
+ cache: "no-store"
121640
+ });
121641
+ if (!active || !response.ok) return;
121642
+ const payload = await response.json();
121643
+ if (!active || payload.statuses === void 0) return;
121644
+ const next = {};
121645
+ for (const provider of SUBSCRIPTION_PROVIDERS) {
121646
+ const row = payload.statuses[provider];
121647
+ next[provider] = row?.state === "logged-in" ? "logged-in" : row?.state === "logged-out" ? "logged-out" : "unknown";
121648
+ }
121649
+ setSubDots(next);
121650
+ } catch {}
121651
+ };
121652
+ probe();
121653
+ return () => {
121654
+ active = false;
121655
+ };
121656
+ }, [open, keyTick]);
121422
121657
  const t = (keyName) => (lang === "en" ? PILL_DICT.en : PILL_DICT.zh)[keyName];
121423
121658
  const current = snapshot.value?.provider ?? "google";
121424
121659
  const writable = snapshot.writable;
@@ -121427,11 +121662,15 @@ img.ProseMirror-separator {
121427
121662
  setMenuStyle(null);
121428
121663
  return;
121429
121664
  }
121430
- const rect = buttonRef.current?.getBoundingClientRect();
121431
- if (rect !== void 0) setMenuStyle({
121432
- left: rect.left,
121433
- bottom: window.innerHeight - rect.top + 6
121434
- });
121665
+ const anchor = () => {
121666
+ const next = anchorOf(buttonRef.current);
121667
+ if (next !== void 0) setMenuStyle(next);
121668
+ };
121669
+ anchor();
121670
+ window.addEventListener("resize", anchor);
121671
+ return () => {
121672
+ window.removeEventListener("resize", anchor);
121673
+ };
121435
121674
  }, [open]);
121436
121675
  (0, react.useEffect)(() => {
121437
121676
  if (!open) return;
@@ -121442,8 +121681,15 @@ img.ProseMirror-separator {
121442
121681
  const onKeyDown = (event) => {
121443
121682
  if (event.key === "Escape") setOpen(false);
121444
121683
  };
121445
- const onReflow = () => {
121446
- setOpen(false);
121684
+ const onReflow = (event) => {
121685
+ if (event.target instanceof Element && event.target.closest(".dsh-ig-pill-menu") !== null) return;
121686
+ const rect = buttonRef.current?.getBoundingClientRect();
121687
+ if (rect !== void 0 && (rect.bottom < 0 || rect.top > window.innerHeight)) {
121688
+ setOpen(false);
121689
+ return;
121690
+ }
121691
+ const next = anchorOf(buttonRef.current);
121692
+ if (next !== void 0) setMenuStyle(next);
121447
121693
  };
121448
121694
  document.addEventListener("mousedown", onPointerDown, true);
121449
121695
  document.addEventListener("keydown", onKeyDown, true);
@@ -121473,8 +121719,12 @@ img.ProseMirror-separator {
121473
121719
  setPending(void 0);
121474
121720
  });
121475
121721
  };
121722
+ /** Plan A dots: ComfyUI is always fixed blue (theme-independent); each
121723
+ * subscription reflects its login state (green in, gray out); cloud rows
121724
+ * reflect their API-key state as before. */
121476
121725
  const dotClassOf = (provider) => {
121477
121726
  if (provider === "comfyui") return "dsh-ig-pill-dot-neutral";
121727
+ if (isSubscriptionProvider(provider)) return subDots[provider] === "logged-in" ? "dsh-ig-pill-dot-ok" : "dsh-ig-pill-dot-missing";
121478
121728
  const dot = dots[provider];
121479
121729
  return dot === "configured" ? "dsh-ig-pill-dot-ok" : dot === "missing" ? "dsh-ig-pill-dot-missing" : "dsh-ig-pill-dot-neutral";
121480
121730
  };
@@ -121483,6 +121733,11 @@ img.ProseMirror-separator {
121483
121733
  const workflow = pillModelOf(provider, snapshot.value);
121484
121734
  return workflow.length > 0 ? `${t("noKeyNeeded")} · ${workflow}` : `${t("noKeyNeeded")} · ${t("noWorkflow")}`;
121485
121735
  }
121736
+ if (isSubscriptionProvider(provider)) {
121737
+ const state = subDots[provider];
121738
+ const stateText = state === "logged-in" ? t("subSignedIn") : state === "logged-out" ? t("subSignedOut") : t("subStatusUnknown");
121739
+ return `${t("subNoKey")} · ${stateText} · ${pillModelOf(provider, snapshot.value)}`;
121740
+ }
121486
121741
  const dot = dots[provider];
121487
121742
  const model = pillModelOf(provider, snapshot.value);
121488
121743
  if (dot === "configured") return `${t("keyConfigured")} · ${model}`;
@@ -121635,8 +121890,8 @@ img.ProseMirror-separator {
121635
121890
  .dsh-ig-pill-option-active{background:var(--dsw-alias-brand-primary-soft,rgba(76,120,255,.10))}
121636
121891
  .dsh-ig-pill-dot{flex:none;width:7px;height:7px;border-radius:50%}
121637
121892
  .dsh-ig-pill-dot-ok{background:#22c55e}
121638
- .dsh-ig-pill-dot-missing{background:var(--dsw-alias-border-l2,#d1d5db)}
121639
- .dsh-ig-pill-dot-neutral{background:var(--dsw-alias-brand-primary,#4c78ff)}
121893
+ .dsh-ig-pill-dot-missing{background:#9ca3af}
121894
+ .dsh-ig-pill-dot-neutral{background:#4c78ff}
121640
121895
  .dsh-ig-pill-option-text{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
121641
121896
  .dsh-ig-pill-option-name{font-size:12.5px;font-weight:500;color:var(--dsw-alias-label-primary,#111827);line-height:1.2}
121642
121897
  .dsh-ig-pill-option-sub{font-size:11px;color:var(--dsw-alias-label-tertiary,#6b7280);line-height:1.2;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
@@ -121645,7 +121900,7 @@ img.ProseMirror-separator {
121645
121900
  `;
121646
121901
  //#endregion
121647
121902
  //#region lib/types/client/index.js
121648
- console.info(`[dsh-image-gen] canvas bundle 2026-09-12T16:41:18.261Z`);
121903
+ console.info(`[dsh-image-gen] canvas bundle 2026-09-14T07:49:16.664Z`);
121649
121904
  /** Right-sidebar tab-type registration id (DSH 0.1.5 `sidebarRightTabs`). */
121650
121905
  const SIDEBAR_STUDIO_TAB_ID = "dsh-image-gen/studio";
121651
121906
  /** The tab kind: a page type opened by kind, recognizing no resource address. */
@@ -121665,6 +121920,27 @@ img.ProseMirror-separator {
121665
121920
  providerXAI: "xAI Grok Imagine",
121666
121921
  providerZhipu: "智谱 GLM-Image",
121667
121922
  providerComfyUI: "本地 ComfyUI",
121923
+ providerChatGPTSub: "ChatGPT 订阅",
121924
+ providerGrokSub: "Grok 订阅",
121925
+ providerGoogleSub: "Google 订阅",
121926
+ subBadgeLoggedIn: "已登录",
121927
+ subBadgeLoggedOut: "未登录",
121928
+ subBadgeUnknown: "状态未知",
121929
+ subSectionTitle: "订阅生图",
121930
+ subHint: "通过已登录的订阅账号生图,无需 API Key。登录在你的授权下进行,不会修改任何 API Key 或默认 Provider。",
121931
+ subAccountLabel: "账号",
121932
+ subAccountEmail: "已登录:{email}",
121933
+ subAccountNone: "未登录",
121934
+ subLogin: "登录",
121935
+ subLoggingIn: "正在登录…",
121936
+ subLogout: "退出登录",
121937
+ subLoggingOut: "正在退出…",
121938
+ subWaitingLogin: "已打开授权页面,请在浏览器中完成登录…",
121939
+ subLoginOk: "登录成功",
121940
+ subModelLabel: "订阅模型",
121941
+ subModelHint: "由订阅通道固定,不可更改。",
121942
+ subEditUnsupported: "订阅通道暂不支持图生图(编辑图片),仅支持文字生图。",
121943
+ subDefaultHint: "订阅 Provider 只能生成,不能编辑图片;需要编辑时请切换到 API Key Provider。",
121668
121944
  apiKeyLabel: "{provider} API Key",
121669
121945
  apiKeyPlaceholder: "留空即可保留已配置的 Key",
121670
121946
  apiKeyHint: "安全保存为 {key};页面不会读回明文。",
@@ -121692,6 +121968,14 @@ img.ProseMirror-separator {
121692
121968
  endpointHintGoogle: "Google 官方地址或反代端点(全路径)。",
121693
121969
  endpointHintOpenAI: "官方 api.openai.com 的 /v1 地址;中转站请使用下方「OpenAI 兼容」行。",
121694
121970
  endpointHintOpenAICompat: "中转站/自建服务的 OpenAI 兼容 /v1 地址(必填),例如 https://your-relay.example.com/v1。",
121971
+ editFormat: "图生图请求形态",
121972
+ editFormatMultipart: "标准 multipart(OpenAI 官方)",
121973
+ editFormatJsonImageUrlArray: "JSON images 数组(商汤等中转)",
121974
+ editFormatHint: "文生图不受影响。仅当中转站的图生图接口使用自有 JSON 契约(如商汤 SenseNova)时才需要切换。",
121975
+ editExtra: "附加 JSON 字段",
121976
+ editExtraPlaceholder: "{\"watermark\": false, \"prompt_extend\": true}",
121977
+ editExtraHint: "仅 JSON 形态生效;会合并到请求体末尾,可覆盖默认字段。留空表示不附加。",
121978
+ editExtraInvalid: "附加 JSON 字段必须是合法的 JSON 对象。",
121695
121979
  endpointHintSeedream: "火山方舟兼容的 /api/v3 地址。",
121696
121980
  endpointHintDashScope: "阿里云百炼 DashScope 官方接口地址。",
121697
121981
  endpointHintXAI: "xAI 官方 api.x.ai 的 /v1 地址。",
@@ -121765,6 +122049,27 @@ img.ProseMirror-separator {
121765
122049
  providerXAI: "xAI Grok Imagine",
121766
122050
  providerZhipu: "Zhipu GLM-Image",
121767
122051
  providerComfyUI: "Local ComfyUI",
122052
+ providerChatGPTSub: "ChatGPT Subscription",
122053
+ providerGrokSub: "Grok Subscription",
122054
+ providerGoogleSub: "Google Subscription",
122055
+ subBadgeLoggedIn: "Signed in",
122056
+ subBadgeLoggedOut: "Signed out",
122057
+ subBadgeUnknown: "Unknown",
122058
+ subSectionTitle: "Subscription generation",
122059
+ subHint: "Generates through a logged-in subscription account; no API key needed. Signing in never changes any API key or the default provider.",
122060
+ subAccountLabel: "Account",
122061
+ subAccountEmail: "Signed in: {email}",
122062
+ subAccountNone: "Not signed in",
122063
+ subLogin: "Sign in",
122064
+ subLoggingIn: "Signing in…",
122065
+ subLogout: "Sign out",
122066
+ subLoggingOut: "Signing out…",
122067
+ subWaitingLogin: "Authorization page opened; complete sign-in in your browser…",
122068
+ subLoginOk: "Signed in",
122069
+ subModelLabel: "Subscription model",
122070
+ subModelHint: "Fixed by the subscription channel; not changeable.",
122071
+ subEditUnsupported: "The subscription channel does not support image editing yet; text-to-image only.",
122072
+ subDefaultHint: "Subscription providers can only generate; switch to an API-key provider for editing.",
121768
122073
  apiKeyLabel: "{provider} API Key",
121769
122074
  apiKeyPlaceholder: "Leave empty to keep configured key",
121770
122075
  apiKeyHint: "Securely saved as {key}; never read back in plaintext.",
@@ -121792,6 +122097,14 @@ img.ProseMirror-separator {
121792
122097
  endpointHintGoogle: "Official Google endpoint or reverse proxy (full path).",
121793
122098
  endpointHintOpenAI: "Official api.openai.com /v1 base URL; for relays use the \"OpenAI-compatible\" row below.",
121794
122099
  endpointHintOpenAICompat: "OpenAI-compatible /v1 base URL of your relay or self-hosted service (required), e.g. https://your-relay.example.com/v1.",
122100
+ editFormat: "Edit request format",
122101
+ editFormatMultipart: "Standard multipart (official OpenAI)",
122102
+ editFormatJsonImageUrlArray: "JSON images array (SenseNova etc.)",
122103
+ editFormatHint: "Text-to-image is unaffected. Only switch when the relay runs image edits on its own JSON contract (e.g. SenseNova); size is pinned to \"auto\" in this format (the only value SenseNova's edits endpoint accepts) and can be overridden via extra fields.",
122104
+ editExtra: "Extra JSON fields",
122105
+ editExtraPlaceholder: "{\"watermark\": false, \"prompt_extend\": true}",
122106
+ editExtraHint: "Only used with the JSON format; merged into the request body last and may override defaults. Leave empty for none.",
122107
+ editExtraInvalid: "Extra JSON fields must be a valid JSON object.",
121795
122108
  endpointHintSeedream: "Volcengine Ark compatible /api/v3 base URL.",
121796
122109
  endpointHintDashScope: "Official Aliyun DashScope endpoint.",
121797
122110
  endpointHintXAI: "Official xAI api.x.ai /v1 base URL.",
@@ -121868,6 +122181,7 @@ img.ProseMirror-separator {
121868
122181
  .dsh-ig-label{font-size:13px;font-weight:500;color:var(--dsw-alias-label-primary,inherit)}
121869
122182
  .dsh-ig-input{box-sizing:border-box;width:100%;padding:8px 12px;font-size:13px;border:1px solid var(--dsw-alias-border-l2,#d7dbe0);border-radius:8px;background:var(--dsw-alias-bg-layer-3,transparent);color:inherit;outline:none;transition:border-color .15s}
121870
122183
  .dsh-ig-input:focus{border-color:var(--dsw-alias-brand-primary,#4c78ff)}
122184
+ .dsh-ig-textarea{resize:vertical;min-height:56px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;line-height:1.5}
121871
122185
  .dsh-ig-input-group{display:flex;gap:8px;align-items:center}
121872
122186
  .dsh-ig-file-row{display:flex;align-items:center;gap:10px;min-width:0}
121873
122187
  .dsh-ig-file-input{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;clip-path:inset(50%)}
@@ -122153,6 +122467,11 @@ img.ProseMirror-separator {
122153
122467
  const isCompactTranscript = () => chatScope.getSnapshot().value?.transcriptView !== "normal";
122154
122468
  const locale = ctx.get("locale");
122155
122469
  const promotion = { enabled: false };
122470
+ ctx.inject(["sessions"], (owner) => {
122471
+ const sessions = owner.get("sessions");
122472
+ if (typeof sessions?.binding !== "function" || typeof sessions.list?.subscribe !== "function") return;
122473
+ owner.effect(() => startConversationLandings(sessions), "dsh-image-gen: live canvas images");
122474
+ });
122156
122475
  ctx.effect(() => {
122157
122476
  const style = document.createElement("style");
122158
122477
  style.dataset.plugin = "dsh-image-gen";
@@ -122363,7 +122682,9 @@ img.ProseMirror-separator {
122363
122682
  messageIsError: false,
122364
122683
  workflows: [],
122365
122684
  activeWorkflow: "",
122366
- timeoutSeconds: DEFAULT_COMFYUI_TIMEOUT_MS / 1e3
122685
+ timeoutSeconds: DEFAULT_COMFYUI_TIMEOUT_MS / 1e3,
122686
+ editFormat: "multipart",
122687
+ editExtraText: ""
122367
122688
  };
122368
122689
  }
122369
122690
  /** Settings field each cloud provider persists its model under. */
@@ -122408,6 +122729,15 @@ img.ProseMirror-separator {
122408
122729
  function modelPullSupported(_provider) {
122409
122730
  return true;
122410
122731
  }
122732
+ /** Serialize the extra-fields record into editor text; empty record becomes ''. */
122733
+ function editExtraTextOf(value) {
122734
+ if (value === void 0 || Object.keys(value).length === 0) return "";
122735
+ try {
122736
+ return JSON.stringify(value);
122737
+ } catch {
122738
+ return "";
122739
+ }
122740
+ }
122411
122741
  /** Build one row per provider from persisted settings, including ComfyUI extras. */
122412
122742
  function rowsFromSettings(value) {
122413
122743
  const rows = {};
@@ -122419,6 +122749,10 @@ img.ProseMirror-separator {
122419
122749
  workflows: resolveComfyUIWorkflows(value ?? {}),
122420
122750
  activeWorkflow: activeComfyUIWorkflow(value ?? {})?.name ?? "",
122421
122751
  timeoutSeconds: Math.max(1, Math.round((value?.comfyuiTimeoutMs ?? 3e5) / 1e3))
122752
+ } : {},
122753
+ ...provider === "openai-compat" ? {
122754
+ editFormat: value?.openaiCompatEditFormat === "jsonImageUrlArray" ? "jsonImageUrlArray" : "multipart",
122755
+ editExtraText: editExtraTextOf(value?.openaiCompatEditExtra)
122422
122756
  } : {}
122423
122757
  };
122424
122758
  return rows;
@@ -122440,6 +122774,14 @@ img.ProseMirror-separator {
122440
122774
  const [workspaceMessageIsError, setWorkspaceMessageIsError] = (0, react.useState)(false);
122441
122775
  const [rows, setRows] = (0, react.useState)(() => rowsFromSettings(props.scope.getSnapshot().value));
122442
122776
  const [keyTick, setKeyTick] = (0, react.useState)(0);
122777
+ const [subStatus, setSubStatus] = (0, react.useState)(() => ({
122778
+ "chatgpt-sub": { state: "unknown" },
122779
+ "grok-sub": { state: "unknown" },
122780
+ "google-sub": { state: "unknown" }
122781
+ }));
122782
+ const [subTick, setSubTick] = (0, react.useState)(0);
122783
+ const [subBusy, setSubBusy] = (0, react.useState)({});
122784
+ const [subPending, setSubPending] = (0, react.useState)([]);
122443
122785
  (0, react.useEffect)(() => props.scope.subscribe(() => {
122444
122786
  setSnapshot(props.scope.getSnapshot());
122445
122787
  }), [props.scope]);
@@ -122452,6 +122794,143 @@ img.ProseMirror-separator {
122452
122794
  (0, react.useEffect)(() => credentialEvents?.listen(() => {
122453
122795
  setKeyTick((tick) => tick + 1);
122454
122796
  }), [credentialEvents]);
122797
+ const pending = subPending;
122798
+ (0, react.useEffect)(() => {
122799
+ if (!open) return void 0;
122800
+ let active = true;
122801
+ const probe = async () => {
122802
+ try {
122803
+ const response = await fetch(SUBSCRIPTION_STATUS_ROUTE, {
122804
+ method: "POST",
122805
+ cache: "no-store"
122806
+ });
122807
+ if (!response.ok) return;
122808
+ const payload = await response.json();
122809
+ if (!active || payload.statuses === void 0) return;
122810
+ const next = {
122811
+ "chatgpt-sub": { state: "unknown" },
122812
+ "grok-sub": { state: "unknown" },
122813
+ "google-sub": { state: "unknown" }
122814
+ };
122815
+ for (const provider of SUBSCRIPTION_PROVIDERS) {
122816
+ const row = payload.statuses[provider];
122817
+ if (row?.state === "logged-in") next[provider] = {
122818
+ state: "logged-in",
122819
+ ...typeof row.email === "string" ? { email: row.email } : {}
122820
+ };
122821
+ else if (row?.state === "logged-out") next[provider] = { state: "logged-out" };
122822
+ }
122823
+ setSubStatus(next);
122824
+ const landed = pending.filter((entry) => next[entry.provider].state === "logged-in");
122825
+ if (landed.length > 0) {
122826
+ setSubPending((current) => current.filter((entry) => next[entry.provider].state !== "logged-in"));
122827
+ for (const entry of landed) updateRow(entry.provider, {
122828
+ message: t("subLoginOk"),
122829
+ messageIsError: false
122830
+ });
122831
+ }
122832
+ const WINDOW_MS = 63e4;
122833
+ if (pending.filter((entry) => Date.now() - entry.startedAt > WINDOW_MS).length > 0) setSubPending((current) => current.filter((entry) => Date.now() - entry.startedAt <= WINDOW_MS));
122834
+ } catch {}
122835
+ };
122836
+ probe();
122837
+ let timer;
122838
+ if (pending.length > 0) timer = setInterval(() => {
122839
+ probe();
122840
+ }, 2e3);
122841
+ const onFocus = () => {
122842
+ probe();
122843
+ };
122844
+ window.addEventListener("focus", onFocus);
122845
+ return () => {
122846
+ active = false;
122847
+ if (timer !== void 0) clearInterval(timer);
122848
+ window.removeEventListener("focus", onFocus);
122849
+ };
122850
+ }, [
122851
+ open,
122852
+ subTick,
122853
+ pending
122854
+ ]);
122855
+ /** Start a subscription login: the host registers PKCE and the loopback
122856
+ * catch server, then the browser opens the vendor's authorize URL. The
122857
+ * provider joins the pending set so the badge polls until login lands. */
122858
+ const subscriptionLogin = async (provider) => {
122859
+ updateRow(provider, {
122860
+ message: "",
122861
+ messageIsError: false
122862
+ });
122863
+ setSubBusy((current) => ({
122864
+ ...current,
122865
+ [provider]: "login"
122866
+ }));
122867
+ try {
122868
+ const response = await fetch(SUBSCRIPTION_LOGIN_ROUTE, {
122869
+ method: "POST",
122870
+ headers: { "content-type": "application/json" },
122871
+ body: JSON.stringify({ provider })
122872
+ });
122873
+ const payload = await response.json().catch(() => ({}));
122874
+ if (!response.ok || payload.ok !== true || typeof payload.url !== "string") {
122875
+ const detail = typeof payload.error === "string" ? payload.error : payload.error?.message;
122876
+ throw new Error(detail ?? `HTTP ${String(response.status)}`);
122877
+ }
122878
+ setSubPending((current) => [...current.filter((entry) => entry.provider !== provider), {
122879
+ provider,
122880
+ startedAt: Date.now()
122881
+ }]);
122882
+ window.open(payload.url, "_blank", "noopener");
122883
+ updateRow(provider, {
122884
+ message: t("subWaitingLogin"),
122885
+ messageIsError: false
122886
+ });
122887
+ } catch (cause) {
122888
+ updateRow(provider, {
122889
+ message: cause instanceof Error ? cause.message : String(cause),
122890
+ messageIsError: true
122891
+ });
122892
+ } finally {
122893
+ setSubBusy((current) => {
122894
+ const next = { ...current };
122895
+ delete next[provider];
122896
+ return next;
122897
+ });
122898
+ }
122899
+ };
122900
+ /** Sign out one subscription account; nothing else changes. */
122901
+ const subscriptionLogout = async (provider) => {
122902
+ setSubBusy((current) => ({
122903
+ ...current,
122904
+ [provider]: "logout"
122905
+ }));
122906
+ try {
122907
+ const response = await fetch(SUBSCRIPTION_LOGIN_ROUTE, {
122908
+ method: "POST",
122909
+ headers: { "content-type": "application/json" },
122910
+ body: JSON.stringify({
122911
+ provider,
122912
+ action: "logout"
122913
+ })
122914
+ });
122915
+ if (!response.ok) throw new Error(`HTTP ${String(response.status)}`);
122916
+ updateRow(provider, {
122917
+ message: "",
122918
+ messageIsError: false
122919
+ });
122920
+ } catch (cause) {
122921
+ updateRow(provider, {
122922
+ message: cause instanceof Error ? cause.message : String(cause),
122923
+ messageIsError: true
122924
+ });
122925
+ } finally {
122926
+ setSubBusy((current) => {
122927
+ const next = { ...current };
122928
+ delete next[provider];
122929
+ return next;
122930
+ });
122931
+ setSubTick((tick) => tick + 1);
122932
+ }
122933
+ };
122455
122934
  const t = (keyName, params) => {
122456
122935
  let text = (lang === "en" ? DICT.en : DICT.zh)[keyName] || DICT.zh[keyName] || keyName;
122457
122936
  if (params) for (const [k, v] of Object.entries(params)) text = text.replace(`{${k}}`, v);
@@ -122465,7 +122944,10 @@ img.ProseMirror-separator {
122465
122944
  dashscope: t("providerDashScope"),
122466
122945
  xai: t("providerXAI"),
122467
122946
  zhipu: t("providerZhipu"),
122468
- comfyui: t("providerComfyUI")
122947
+ comfyui: t("providerComfyUI"),
122948
+ "chatgpt-sub": t("providerChatGPTSub"),
122949
+ "grok-sub": t("providerGrokSub"),
122950
+ "google-sub": t("providerGoogleSub")
122469
122951
  };
122470
122952
  (0, react.useEffect)(() => {
122471
122953
  const value = snapshot.value;
@@ -122592,9 +123074,28 @@ img.ProseMirror-separator {
122592
123074
  await props.scope.set("comfyuiWorkflowJson", activeEntry === void 0 ? "" : activeEntry.json);
122593
123075
  await props.scope.set("comfyuiWorkflowName", activeEntry === void 0 ? "" : activeEntry.name);
122594
123076
  await props.scope.set("comfyuiTimeoutMs", Math.max(1, Math.round(row.timeoutSeconds)) * 1e3);
122595
- } else {
123077
+ } else if (isSubscriptionProvider(provider)) await Promise.resolve();
123078
+ else {
123079
+ let editExtra = {};
123080
+ if (provider === "openai-compat") {
123081
+ const extraText = row.editExtraText.trim();
123082
+ if (extraText.length > 0) {
123083
+ let parsed;
123084
+ try {
123085
+ parsed = JSON.parse(extraText);
123086
+ } catch {
123087
+ throw new Error(t("editExtraInvalid"));
123088
+ }
123089
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error(t("editExtraInvalid"));
123090
+ editExtra = parsed;
123091
+ }
123092
+ }
122596
123093
  await props.scope.set(modelFieldOf(provider), row.model);
122597
123094
  await props.scope.set(baseURLFieldOf(provider), row.baseURL);
123095
+ if (provider === "openai-compat") {
123096
+ await props.scope.set("openaiCompatEditFormat", row.editFormat);
123097
+ await props.scope.set("openaiCompatEditExtra", editExtra);
123098
+ }
122598
123099
  if (row.keyInput.trim().length > 0) {
122599
123100
  const keyRef = cloudCredentialRef(provider);
122600
123101
  if (keyRef === void 0) throw new Error(t("comfyuiNoKey"));
@@ -122738,6 +123239,21 @@ img.ProseMirror-separator {
122738
123239
  text: t("comfyuiNoKey"),
122739
123240
  className: "dsh-ig-badge dsh-ig-badge-neutral"
122740
123241
  };
123242
+ if (isSubscriptionProvider(provider)) {
123243
+ const status = subStatus[provider].state;
123244
+ if (status === "logged-in") return {
123245
+ text: t("subBadgeLoggedIn"),
123246
+ className: "dsh-ig-badge dsh-ig-badge-ok"
123247
+ };
123248
+ if (status === "logged-out") return {
123249
+ text: t("subBadgeLoggedOut"),
123250
+ className: "dsh-ig-badge dsh-ig-badge-missing"
123251
+ };
123252
+ return {
123253
+ text: t("subBadgeUnknown"),
123254
+ className: "dsh-ig-badge dsh-ig-badge-neutral"
123255
+ };
123256
+ }
122741
123257
  const status = rows[provider].keyStatus;
122742
123258
  if (status === "checking") return {
122743
123259
  text: t("badgeChecking"),
@@ -123007,6 +123523,57 @@ img.ProseMirror-separator {
123007
123523
  }) : null
123008
123524
  ]
123009
123525
  }),
123526
+ provider === "openai-compat" ? (0, react_jsx_runtime.jsxs)("label", {
123527
+ className: "dsh-ig-field",
123528
+ children: [
123529
+ (0, react_jsx_runtime.jsx)("span", {
123530
+ className: "dsh-ig-label",
123531
+ children: t("editFormat")
123532
+ }),
123533
+ (0, react_jsx_runtime.jsxs)("select", {
123534
+ className: "dsh-ig-input",
123535
+ value: row.editFormat,
123536
+ onChange: (event) => {
123537
+ updateRow(provider, { editFormat: event.target.value === "jsonImageUrlArray" ? "jsonImageUrlArray" : "multipart" });
123538
+ },
123539
+ disabled: !snapshot.writable,
123540
+ children: [(0, react_jsx_runtime.jsx)("option", {
123541
+ value: "multipart",
123542
+ children: t("editFormatMultipart")
123543
+ }), (0, react_jsx_runtime.jsx)("option", {
123544
+ value: "jsonImageUrlArray",
123545
+ children: t("editFormatJsonImageUrlArray")
123546
+ })]
123547
+ }),
123548
+ (0, react_jsx_runtime.jsx)("span", {
123549
+ className: "dsh-ig-hint",
123550
+ children: t("editFormatHint")
123551
+ })
123552
+ ]
123553
+ }) : null,
123554
+ provider === "openai-compat" && row.editFormat === "jsonImageUrlArray" ? (0, react_jsx_runtime.jsxs)("label", {
123555
+ className: "dsh-ig-field",
123556
+ children: [
123557
+ (0, react_jsx_runtime.jsx)("span", {
123558
+ className: "dsh-ig-label",
123559
+ children: t("editExtra")
123560
+ }),
123561
+ (0, react_jsx_runtime.jsx)("textarea", {
123562
+ className: "dsh-ig-input dsh-ig-textarea",
123563
+ value: row.editExtraText,
123564
+ onChange: (event) => {
123565
+ updateRow(provider, { editExtraText: event.target.value });
123566
+ },
123567
+ placeholder: t("editExtraPlaceholder"),
123568
+ rows: 2,
123569
+ disabled: !snapshot.writable
123570
+ }),
123571
+ (0, react_jsx_runtime.jsx)("span", {
123572
+ className: "dsh-ig-hint",
123573
+ children: t("editExtraHint")
123574
+ })
123575
+ ]
123576
+ }) : null,
123010
123577
  (0, react_jsx_runtime.jsxs)("div", {
123011
123578
  className: "dsh-ig-row-actions",
123012
123579
  children: [(0, react_jsx_runtime.jsx)("p", {
@@ -123224,6 +123791,119 @@ img.ProseMirror-separator {
123224
123791
  })
123225
123792
  });
123226
123793
  };
123794
+ /** Subscription rows: account login/logout, fixed model, edit limitation. */
123795
+ const renderSubscriptionBody = (provider) => {
123796
+ const row = rows[provider];
123797
+ const status = subStatus[provider];
123798
+ const busy = subBusy[provider];
123799
+ return (0, react_jsx_runtime.jsx)("div", {
123800
+ className: "dsh-ig-provider-body",
123801
+ children: (0, react_jsx_runtime.jsxs)("form", {
123802
+ onSubmit: (event) => {
123803
+ event.preventDefault();
123804
+ saveProviderRow(provider);
123805
+ },
123806
+ children: [
123807
+ (0, react_jsx_runtime.jsxs)("div", {
123808
+ className: "dsh-ig-field",
123809
+ children: [(0, react_jsx_runtime.jsx)("span", {
123810
+ className: "dsh-ig-label",
123811
+ children: t("subSectionTitle")
123812
+ }), (0, react_jsx_runtime.jsx)("p", {
123813
+ className: "dsh-ig-hint",
123814
+ children: t("subHint")
123815
+ })]
123816
+ }),
123817
+ (0, react_jsx_runtime.jsxs)("div", {
123818
+ className: "dsh-ig-field",
123819
+ children: [
123820
+ (0, react_jsx_runtime.jsx)("span", {
123821
+ className: "dsh-ig-label",
123822
+ children: t("subAccountLabel")
123823
+ }),
123824
+ (0, react_jsx_runtime.jsx)("p", {
123825
+ className: "dsh-ig-hint",
123826
+ children: status.state === "logged-in" ? t("subAccountEmail", { email: status.email ?? "" }) : status.state === "logged-out" ? t("subAccountNone") : t("subBadgeUnknown")
123827
+ }),
123828
+ (0, react_jsx_runtime.jsx)("span", {
123829
+ className: "dsh-ig-row-buttons",
123830
+ children: status.state === "logged-in" ? (0, react_jsx_runtime.jsx)("button", {
123831
+ type: "button",
123832
+ className: "dsh-ig-btn-secondary",
123833
+ disabled: busy !== void 0,
123834
+ onClick: () => {
123835
+ subscriptionLogout(provider);
123836
+ },
123837
+ children: busy === "logout" ? t("subLoggingOut") : t("subLogout")
123838
+ }) : (0, react_jsx_runtime.jsx)("button", {
123839
+ type: "button",
123840
+ className: "dsh-ig-save",
123841
+ disabled: busy !== void 0,
123842
+ onClick: () => {
123843
+ subscriptionLogin(provider);
123844
+ setSubTick((tick) => tick + 1);
123845
+ },
123846
+ children: busy === "login" ? t("subLoggingIn") : t("subLogin")
123847
+ })
123848
+ })
123849
+ ]
123850
+ }),
123851
+ (0, react_jsx_runtime.jsxs)("label", {
123852
+ className: "dsh-ig-field",
123853
+ children: [
123854
+ (0, react_jsx_runtime.jsx)("span", {
123855
+ className: "dsh-ig-label",
123856
+ children: t("subModelLabel")
123857
+ }),
123858
+ (0, react_jsx_runtime.jsx)("input", {
123859
+ className: "dsh-ig-input",
123860
+ type: "text",
123861
+ value: row.model,
123862
+ readOnly: true,
123863
+ disabled: true
123864
+ }),
123865
+ (0, react_jsx_runtime.jsx)("span", {
123866
+ className: "dsh-ig-hint",
123867
+ children: t("subModelHint")
123868
+ })
123869
+ ]
123870
+ }),
123871
+ (0, react_jsx_runtime.jsx)("div", {
123872
+ className: "dsh-ig-field",
123873
+ children: (0, react_jsx_runtime.jsx)("span", {
123874
+ className: "dsh-ig-label",
123875
+ children: t("subEditUnsupported")
123876
+ })
123877
+ }),
123878
+ (0, react_jsx_runtime.jsxs)("div", {
123879
+ className: "dsh-ig-row-actions",
123880
+ children: [(0, react_jsx_runtime.jsx)("p", {
123881
+ className: `dsh-ig-status${row.messageIsError ? " dsh-ig-status-error" : ""}`,
123882
+ role: "status",
123883
+ children: row.message || testResultText(row.testResult)
123884
+ }), (0, react_jsx_runtime.jsxs)("span", {
123885
+ className: "dsh-ig-row-buttons",
123886
+ children: [(0, react_jsx_runtime.jsx)("button", {
123887
+ type: "button",
123888
+ className: "dsh-ig-btn-secondary",
123889
+ disabled: row.testing,
123890
+ onClick: () => {
123891
+ testConnection(provider);
123892
+ setSubTick((tick) => tick + 1);
123893
+ },
123894
+ children: row.testing ? t("testing") : t("testConnection")
123895
+ }), (0, react_jsx_runtime.jsx)("button", {
123896
+ className: "dsh-ig-save",
123897
+ type: "submit",
123898
+ disabled: row.saving || !snapshot.writable,
123899
+ children: row.saving ? t("saving") : t("save")
123900
+ })]
123901
+ })]
123902
+ })
123903
+ ]
123904
+ })
123905
+ });
123906
+ };
123227
123907
  const renderProviderRow = (provider) => {
123228
123908
  const row = rows[provider];
123229
123909
  const badge = badgeOf(provider);
@@ -123265,7 +123945,7 @@ img.ProseMirror-separator {
123265
123945
  })
123266
123946
  })
123267
123947
  ]
123268
- }), row.expanded ? provider === "comfyui" ? renderComfyUIBody() : renderCloudBody(provider) : null]
123948
+ }), row.expanded ? provider === "comfyui" ? renderComfyUIBody() : isSubscriptionProvider(provider) ? renderSubscriptionBody(provider) : renderCloudBody(provider) : null]
123269
123949
  }, provider);
123270
123950
  };
123271
123951
  return (0, react_jsx_runtime.jsxs)("li", {
@@ -123510,14 +124190,6 @@ img.ProseMirror-separator {
123510
124190
  };
123511
124191
  (0, react.useEffect)(() => {
123512
124192
  if (result === void 0) return;
123513
- pushTlLandings([{
123514
- galleryId: result.attachment.attachmentId,
123515
- attachment: result.attachment,
123516
- fromConversation: true,
123517
- prompt: result.prompt,
123518
- provider: result.provider,
123519
- model: result.model
123520
- }]);
123521
124193
  saveGalleryItem({
123522
124194
  id: result.attachment.attachmentId,
123523
124195
  attachment: result.attachment,
@@ -124004,11 +124676,11 @@ img.ProseMirror-separator {
124004
124676
  };
124005
124677
  }
124006
124678
  function modelOf(provider, value) {
124007
- const stored = provider === "comfyui" ? activeComfyUIWorkflow(value ?? {})?.name : value?.[CLOUD_MODEL_FIELDS[provider]];
124679
+ const stored = provider === "comfyui" ? activeComfyUIWorkflow(value ?? {})?.name : isSubscriptionProvider(provider) ? DEFAULT_MODELS[provider] : value?.[CLOUD_MODEL_FIELDS[provider]];
124008
124680
  return typeof stored === "string" && stored.length > 0 ? stored : DEFAULT_MODELS[provider];
124009
124681
  }
124010
124682
  function baseURLOf(provider, value) {
124011
- const stored = provider === "comfyui" ? value?.comfyuiBaseURL : value?.[CLOUD_URL_FIELDS[provider]];
124683
+ const stored = provider === "comfyui" ? value?.comfyuiBaseURL : isSubscriptionProvider(provider) ? "" : value?.[CLOUD_URL_FIELDS[provider]];
124012
124684
  return typeof stored === "string" && stored.length > 0 ? stored : DEFAULT_BASE_URLS[provider];
124013
124685
  }
124014
124686
  //#endregion