dsh-image-gen 0.6.5 → 0.6.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -129,6 +129,12 @@ window.__ModuleLoader__.load({
129
129
  "xai",
130
130
  "zhipu"
131
131
  ];
132
+ /**
133
+ * Providers the browser workbench can drive: the BYOK cloud set plus the
134
+ * logged-in subscription channels. ComfyUI stays out; it has its own workflow
135
+ * pipeline and no shared request shape.
136
+ */
137
+ const STUDIO_PROVIDERS = [...CLOUD_IMAGE_PROVIDERS, ...SUBSCRIPTION_PROVIDERS];
132
138
  /** The credential reference each cloud provider's API key is stored under. */
133
139
  const CLOUD_CREDENTIAL_REFS = {
134
140
  google: "GEMINI_API_KEY",
@@ -343,9 +349,9 @@ window.__ModuleLoader__.load({
343
349
  }
344
350
  });
345
351
  }
346
- const listeners$1 = /* @__PURE__ */ new Set();
352
+ const listeners = /* @__PURE__ */ new Set();
347
353
  function notifyListeners() {
348
- for (const listener of listeners$1) try {
354
+ for (const listener of listeners) try {
349
355
  listener();
350
356
  } catch (err) {
351
357
  console.error("[dsh-image-gen] Gallery listener error:", err);
@@ -355,9 +361,9 @@ window.__ModuleLoader__.load({
355
361
  * Subscribe to gallery mutations (insert/delete/clear).
356
362
  */
357
363
  function subscribeGallery(listener) {
358
- listeners$1.add(listener);
364
+ listeners.add(listener);
359
365
  return () => {
360
- listeners$1.delete(listener);
366
+ listeners.delete(listener);
361
367
  };
362
368
  }
363
369
  /**
@@ -1440,6 +1446,11 @@ window.__ModuleLoader__.load({
1440
1446
  cache.delete(key);
1441
1447
  }
1442
1448
  }
1449
+ /** Clear all in-memory image caches. */
1450
+ function clearAttachmentCache() {
1451
+ cache.clear();
1452
+ currentCacheBytes = 0;
1453
+ }
1443
1454
  function enforceLimits() {
1444
1455
  while (cache.size > 30 || currentCacheBytes > 134217728) {
1445
1456
  const oldestKey = cache.keys().next().value;
@@ -76042,14 +76053,14 @@ img.ProseMirror-separator {
76042
76053
  });
76043
76054
  var useReactId = react[" useId ".trim().toString()] || (() => void 0);
76044
76055
  var count$1 = 0;
76045
- function useId(deterministicId) {
76056
+ function useId$1(deterministicId) {
76046
76057
  const [id, setId] = react.useState(useReactId());
76047
76058
  useLayoutEffect2(() => {
76048
76059
  if (!deterministicId) setId((reactId) => reactId ?? String(count$1++));
76049
76060
  }, [deterministicId]);
76050
76061
  return deterministicId || (id ? `radix-${id}` : "");
76051
76062
  }
76052
- __name$25(useId, "useId");
76063
+ __name$25(useId$1, "useId");
76053
76064
  //#endregion
76054
76065
  //#region node_modules/.pnpm/@radix-ui+react-direction@1_8310f062454398ef3b849e7d93575bcb/node_modules/@radix-ui/react-direction/dist/index.mjs
76055
76066
  var __defProp$27 = Object.defineProperty;
@@ -78670,9 +78681,9 @@ img.ProseMirror-separator {
78670
78681
  scope: __scopeDialog,
78671
78682
  triggerRef,
78672
78683
  contentRef,
78673
- contentId: useId(),
78674
- titleId: useId(),
78675
- descriptionId: useId(),
78684
+ contentId: useId$1(),
78685
+ titleId: useId$1(),
78686
+ descriptionId: useId$1(),
78676
78687
  titlePresent: titleCount > 0,
78677
78688
  descriptionPresent: descriptionCount > 0,
78678
78689
  setTitleCount,
@@ -80944,7 +80955,7 @@ img.ProseMirror-separator {
80944
80955
  var ITEM_NAME$3 = "RovingFocusGroupItem";
80945
80956
  var RovingFocusGroupItem = /* @__PURE__ */ react.forwardRef(/* @__PURE__ */ __name$13(function RovingFocusGroupItem2(props, forwardedRef) {
80946
80957
  const { __scopeRovingFocusGroup, focusable = true, active = false, tabStopId, children, ...itemProps } = props;
80947
- const autoId = useId();
80958
+ const autoId = useId$1();
80948
80959
  const id = tabStopId || autoId;
80949
80960
  const context = useRovingFocusContext(ITEM_NAME$3, __scopeRovingFocusGroup);
80950
80961
  const isCurrentTabStop = context.currentTabStopId === id;
@@ -81503,8 +81514,8 @@ img.ProseMirror-separator {
81503
81514
  onContentChange: setContent,
81504
81515
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MenuSubProvider, {
81505
81516
  scope: __scopeMenu,
81506
- contentId: useId(),
81507
- triggerId: useId(),
81517
+ contentId: useId$1(),
81518
+ triggerId: useId$1(),
81508
81519
  trigger,
81509
81520
  onTriggerChange: setTrigger,
81510
81521
  children
@@ -81962,9 +81973,9 @@ img.ProseMirror-separator {
81962
81973
  });
81963
81974
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DropdownMenuProvider, {
81964
81975
  scope: __scopeDropdownMenu,
81965
- triggerId: useId(),
81976
+ triggerId: useId$1(),
81966
81977
  triggerRef,
81967
- contentId: useId(),
81978
+ contentId: useId$1(),
81968
81979
  open,
81969
81980
  onOpenChange: setOpen,
81970
81981
  onOpenToggle: react.useCallback(() => setOpen((prevOpen) => !prevOpen), [setOpen]),
@@ -82196,7 +82207,7 @@ img.ProseMirror-separator {
82196
82207
  ...popperScope,
82197
82208
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PopoverProvider, {
82198
82209
  scope: __scopePopover,
82199
- contentId: useId(),
82210
+ contentId: useId$1(),
82200
82211
  triggerRef,
82201
82212
  open,
82202
82213
  onOpenChange: setOpen,
@@ -82428,7 +82439,7 @@ img.ProseMirror-separator {
82428
82439
  ]);
82429
82440
  const isFormControl = trigger ? !!form || !!trigger.closest("form") : true;
82430
82441
  const [nativeOptionsSet, setNativeOptionsSet] = react.useState(/* @__PURE__ */ new Set());
82431
- const contentId = useId();
82442
+ const contentId = useId$1();
82432
82443
  const nativeSelectKey = Array.from(nativeOptionsSet).map((option) => option.props.value).join(";");
82433
82444
  const handleNativeOptionAdd = react.useCallback((option) => {
82434
82445
  setNativeOptionsSet((prev) => new Set(prev).add(option));
@@ -83997,7 +84008,7 @@ img.ProseMirror-separator {
83997
84008
  const popperScope = usePopperScope(__scopeTooltip);
83998
84009
  const [trigger, setTrigger] = react.useState(null);
83999
84010
  const [contentIdState, setContentId] = react.useState(void 0);
84000
- const generatedContentId = useId();
84011
+ const generatedContentId = useId$1();
84001
84012
  const openTimerRef = react.useRef(0);
84002
84013
  const disableHoverableContent = disableHoverableContentProp ?? providerContext.disableHoverableContent;
84003
84014
  const delayDuration = delayDurationProp ?? providerContext.delayDuration;
@@ -108118,7 +108129,7 @@ img.ProseMirror-separator {
108118
108129
  }
108119
108130
  const MAX_SIZE = 256;
108120
108131
  const alphaCache = new LruCache(100);
108121
- const pending$1 = /* @__PURE__ */ new Set();
108132
+ const pending$2 = /* @__PURE__ */ new Set();
108122
108133
  let offscreenCanvas = null;
108123
108134
  function getOffscreenCanvas(w, h) {
108124
108135
  if (!offscreenCanvas) offscreenCanvas = new OffscreenCanvas(w, h);
@@ -108138,12 +108149,12 @@ img.ProseMirror-separator {
108138
108149
  function preloadAlphaData(url, cacheKey) {
108139
108150
  if (typeof OffscreenCanvas === "undefined") return;
108140
108151
  const key = cacheKey ?? url;
108141
- if (alphaCache.has(key) || pending$1.has(key)) return;
108142
- pending$1.add(key);
108152
+ if (alphaCache.has(key) || pending$2.has(key)) return;
108153
+ pending$2.add(key);
108143
108154
  const img = Image$1();
108144
108155
  img.crossOrigin = "anonymous";
108145
108156
  img.onload = async () => {
108146
- pending$1.delete(key);
108157
+ pending$2.delete(key);
108147
108158
  const { width: origW, height: origH } = img;
108148
108159
  if (origW === 0 || origH === 0) return;
108149
108160
  const scale = Math.min(1, MAX_SIZE / Math.max(origW, origH));
@@ -108170,7 +108181,7 @@ img.ProseMirror-separator {
108170
108181
  });
108171
108182
  };
108172
108183
  img.onerror = () => {
108173
- pending$1.delete(key);
108184
+ pending$2.delete(key);
108174
108185
  };
108175
108186
  img.src = url;
108176
108187
  }
@@ -115622,43 +115633,6 @@ img.ProseMirror-separator {
115622
115633
  });
115623
115634
  }
115624
115635
  //#endregion
115625
- //#region lib/types/client/tl/tl-canvas-bridge.js
115626
- const items = [];
115627
- const listeners = /* @__PURE__ */ new Set();
115628
- /** Queue freshly generated images for the tldraw canvases. Safe to call anywhere. */
115629
- function pushTlLandings(newItems) {
115630
- if (newItems.length === 0) return;
115631
- const known = new Set(items.map((item) => item.galleryId));
115632
- let pushed = 0;
115633
- for (const item of newItems) {
115634
- if (known.has(item.galleryId)) continue;
115635
- items.push(item);
115636
- known.add(item.galleryId);
115637
- pushed += 1;
115638
- }
115639
- if (pushed === 0) return;
115640
- for (const listener of listeners) listener();
115641
- }
115642
- /**
115643
- * Read every item landed so far in this browser session.
115644
- *
115645
- * The list is the in-memory session-scratch record shared by all canvases:
115646
- * reading never removes items, so a canvas mounted later (or a second canvas
115647
- * mounted beside the first) reconciles the same set. The list dies with the
115648
- * page, which is exactly the "unsaved content does not survive a restart"
115649
- * semantic the studio canvas keeps.
115650
- */
115651
- function getTlLandings() {
115652
- return items;
115653
- }
115654
- /** Subscribe to queue pushes. Returns an unsubscribe function. */
115655
- function subscribeTlLandings(listener) {
115656
- listeners.add(listener);
115657
- return () => {
115658
- listeners.delete(listener);
115659
- };
115660
- }
115661
- //#endregion
115662
115636
  //#region lib/types/client/tl/canvas-sync.js
115663
115637
  /** Debounce for ordinary document/session changes. */
115664
115638
  const STATE_DEBOUNCE_MS = 600;
@@ -115708,21 +115682,18 @@ img.ProseMirror-separator {
115708
115682
  * the selection identity list, so a selected node reads exactly like its
115709
115683
  * canvas node twin (same name, attachment id, dimensions).
115710
115684
  */
115711
- function describeShape(editor, shape, attachmentByGallery) {
115685
+ function describeShape(editor, shape) {
115712
115686
  const node = { kind: kindOf(shape.type) };
115713
115687
  if (shape.type === "image") {
115714
115688
  const meta = shape.meta;
115715
115689
  const galleryId = meta?.galleryId;
115716
- if (typeof galleryId === "string" && galleryId.length > 0) {
115717
- node.galleryId = galleryId;
115718
- const attachmentId = attachmentByGallery.get(galleryId);
115719
- if (attachmentId !== void 0) node.attachmentId = attachmentId;
115720
- }
115690
+ if (typeof galleryId === "string" && galleryId.length > 0) node.galleryId = galleryId;
115691
+ if (typeof meta?.attachmentId === "string" && meta.attachmentId.length > 0) node.attachmentId = meta.attachmentId;
115721
115692
  if (typeof meta?.prompt === "string" && meta.prompt.trim().length > 0) node.prompt = meta.prompt.replace(/\s+/g, " ").trim().slice(0, 120);
115722
115693
  if (typeof meta?.provider === "string" && meta.provider.length > 0) node.provider = meta.provider;
115723
115694
  if (typeof meta?.model === "string" && meta.model.length > 0) node.model = meta.model;
115724
115695
  const asset = shape.props.assetId === null ? void 0 : editor.getAsset(shape.props.assetId);
115725
- const assetName = asset !== void 0 && asset.type === "image" ? asset.props.name : void 0;
115696
+ const assetName = typeof meta?.name === "string" && meta.name.length > 0 ? meta.name : asset !== void 0 && asset.type === "image" ? asset.props.name : void 0;
115726
115697
  if (typeof assetName === "string" && assetName.length > 0) node.name = assetName.slice(0, 128);
115727
115698
  if (asset !== void 0 && asset.type === "image") {
115728
115699
  node.width = Math.round(asset.props.w);
@@ -115745,31 +115716,19 @@ img.ProseMirror-separator {
115745
115716
  return node;
115746
115717
  }
115747
115718
  function summarize(editor) {
115748
- const attachmentByGallery = /* @__PURE__ */ new Map();
115749
- const conversationGallery = /* @__PURE__ */ new Set();
115750
- for (const landing of getTlLandings()) {
115751
- attachmentByGallery.set(landing.galleryId, String(landing.attachment.attachmentId));
115752
- if (landing.fromConversation) conversationGallery.add(landing.galleryId);
115753
- }
115754
115719
  const shapes = editor.getCurrentPageShapes();
115755
115720
  const nodes = [];
115756
- for (const shape of shapes.slice(0, 48)) nodes.push(describeShape(editor, shape, attachmentByGallery));
115721
+ for (const shape of shapes.slice(0, 48)) nodes.push(describeShape(editor, shape));
115757
115722
  const selected = editor.getSelectedShapes();
115758
115723
  const selectionKinds = [...new Set(selected.map((shape) => kindOf(shape.type)))].slice(0, 8);
115759
- const selectionItems = selected.slice(0, 16).map((shape) => describeShape(editor, shape, attachmentByGallery));
115724
+ const selectionItems = selected.slice(0, 16).map((shape) => describeShape(editor, shape));
115760
115725
  const selectionKey = selectionKeyOf(editor);
115761
- const selectionAllConversationImages = selected.length > 0 && selected.every((shape) => {
115762
- if (shape.type !== "image") return false;
115763
- const galleryId = shape.meta?.galleryId;
115764
- return typeof galleryId === "string" && conversationGallery.has(galleryId);
115765
- });
115766
115726
  return {
115767
115727
  nodeCount: shapes.length,
115768
115728
  nodes,
115769
115729
  selectionCount: selected.length,
115770
115730
  selectionKinds,
115771
115731
  selectionItems,
115772
- selectionAllConversationImages,
115773
115732
  stateKey: JSON.stringify([
115774
115733
  shapes.length,
115775
115734
  nodes,
@@ -115836,7 +115795,7 @@ img.ProseMirror-separator {
115836
115795
  return;
115837
115796
  }
115838
115797
  let selectionImage;
115839
- if (withScreenshot && summary.selectionCount > 0 && !summary.selectionAllConversationImages) try {
115798
+ if (withScreenshot && summary.selectionCount > 0) try {
115840
115799
  const selectedBefore = editor.getSelectedShapes();
115841
115800
  const { url } = await editor.toImageDataUrl(selectedBefore, {
115842
115801
  background: true,
@@ -115844,6 +115803,7 @@ img.ProseMirror-separator {
115844
115803
  scale: 1,
115845
115804
  pixelRatio: 2
115846
115805
  });
115806
+ if (disposed) return;
115847
115807
  const selectedAfter = editor.getSelectedShapes();
115848
115808
  const idsBefore = new Set(selectedBefore.map((shape) => String(shape.id)));
115849
115809
  if (!(selectedBefore.length === selectedAfter.length && selectedAfter.every((shape) => idsBefore.has(String(shape.id))))) return;
@@ -115886,6 +115846,10 @@ img.ProseMirror-separator {
115886
115846
  stateTimer = void 0;
115887
115847
  flush(false);
115888
115848
  }, STATE_DEBOUNCE_MS);
115849
+ if (editor.getSelectedShapeIds().length > 0) exportTimer = setTimeout(() => {
115850
+ exportTimer = void 0;
115851
+ flush(true);
115852
+ }, SELECTION_EXPORT_DELAY_MS);
115889
115853
  return () => {
115890
115854
  if (disposed) return;
115891
115855
  disposed = true;
@@ -115908,25 +115872,81 @@ img.ProseMirror-separator {
115908
115872
  };
115909
115873
  }
115910
115874
  //#endregion
115875
+ //#region lib/types/client/tl/tl-canvas-bridge.js
115876
+ const pending$1 = /* @__PURE__ */ new Map();
115877
+ const consumers = /* @__PURE__ */ new Set();
115878
+ let delivering = false;
115879
+ let generation = 0;
115880
+ /** Invalidates async landings that started before the user cleared the canvas. */
115881
+ function getTlLandingGeneration() {
115882
+ return generation;
115883
+ }
115884
+ function clearTlLandings() {
115885
+ generation += 1;
115886
+ pending$1.clear();
115887
+ }
115888
+ /** Queue freshly generated images for the tldraw canvases. Safe to call anywhere. */
115889
+ function pushTlLandings(newItems) {
115890
+ for (const item of newItems) if (!pending$1.has(item.galleryId)) pending$1.set(item.galleryId, item);
115891
+ deliver();
115892
+ }
115893
+ /** Conversation results are admitted only while a canvas is mounted. */
115894
+ function pushTlLandingsLive(items) {
115895
+ if (consumers.size > 0) pushTlLandings(items);
115896
+ }
115897
+ /** Register a ready editor. False means it unmounted before committing. */
115898
+ function registerTlLandingConsumer(consumer) {
115899
+ consumers.add(consumer);
115900
+ deliver();
115901
+ return () => {
115902
+ consumers.delete(consumer);
115903
+ if (consumers.size === 0) {
115904
+ for (const [id, item] of pending$1) if (item.fromConversation) pending$1.delete(id);
115905
+ }
115906
+ };
115907
+ }
115908
+ async function deliver() {
115909
+ if (delivering) return;
115910
+ delivering = true;
115911
+ try {
115912
+ while (pending$1.size > 0) {
115913
+ const consumer = consumers.values().next().value;
115914
+ if (consumer === void 0) break;
115915
+ const batch = [...pending$1.values()];
115916
+ let completed = true;
115917
+ try {
115918
+ completed = await consumer(batch);
115919
+ } catch (error) {
115920
+ console.warn("[dsh-image-gen] canvas landing failed:", error);
115921
+ }
115922
+ if (completed) {
115923
+ for (const item of batch) if (pending$1.get(item.galleryId) === item) pending$1.delete(item.galleryId);
115924
+ }
115925
+ }
115926
+ } finally {
115927
+ delivering = false;
115928
+ }
115929
+ }
115930
+ //#endregion
115911
115931
  //#region lib/types/client/tl/studio-tl-canvas.js
115912
115932
  /**
115913
115933
  * Infinite canvas surface for the Studio workbench, backed by tldraw.
115914
115934
  *
115915
- * Generated images are pushed onto the tl-canvas-bridge landing bus (studio
115916
- * form paths and chat tool cards land the same way) and become in-memory
115917
- * image assets with data-URL sources. The bus broadcasts, so this surface
115918
- * RECONCILES instead of draining: it lands the bus items its own page does
115919
- * not carry yet. The conversation-view gallery tab and the right-sidebar
115920
- * studio tab can both be mounted, and each mirrors the full session-scratch
115921
- * set independently. The surface deliberately runs WITHOUT persistenceKey:
115922
- * unsaved generations vanish on restart, matching the plugin's save-first
115923
- * philosophy; data-URL assets therefore never touch disk. Shapes carry
115924
- * `meta.galleryId` so re-landing the same generation is a no-op. One batch
115925
- * = one createShapes transaction = one undo step.
115935
+ * Both surfaces share one locally persisted document. The landing queue is
115936
+ * consumed once; tldraw owns subsequent edits, deletion, undo and persistence.
115926
115937
  */
115927
115938
  const MAX_DISPLAY_SIDE = 380;
115928
115939
  const GRID_GAP = 40;
115929
115940
  const GRID_COLS = 3;
115941
+ const mountedCanvases = /* @__PURE__ */ new Set();
115942
+ /** Reset canvas-owned data only. Gallery records and host attachments are untouched. */
115943
+ function clearStudioTlCanvases() {
115944
+ if (mountedCanvases.size === 0) return false;
115945
+ clearTlLandings();
115946
+ for (const clear of mountedCanvases) clear();
115947
+ clearAttachmentCache();
115948
+ return true;
115949
+ }
115930
115950
  /**
115931
115951
  * Dock the stock toolbar vertically along the left edge (Figma-style) instead
115932
115952
  * of tldraw's signature bottom-center pill. It is still the same
@@ -115948,11 +115968,11 @@ img.ProseMirror-separator {
115948
115968
  h: Math.round(height * scale)
115949
115969
  };
115950
115970
  }
115951
- /** Gallery ids already on one page (image shapes' `meta.galleryId`). */
115971
+ /** Dedupe across the whole document, including images moved to other pages. */
115952
115972
  function landedIdsOf(editor) {
115953
115973
  const landedIds = /* @__PURE__ */ new Set();
115954
- for (const shape of editor.getCurrentPageShapes()) {
115955
- if (shape.type !== "image") continue;
115974
+ for (const shape of editor.store.allRecords()) {
115975
+ if (shape.typeName !== "shape" || shape.type !== "image") continue;
115956
115976
  const galleryId = shape.meta?.galleryId;
115957
115977
  if (typeof galleryId === "string") landedIds.add(galleryId);
115958
115978
  }
@@ -115964,13 +115984,14 @@ img.ProseMirror-separator {
115964
115984
  const collapsed = prompt.replace(/\s+/g, " ").trim();
115965
115985
  return collapsed.length === 0 ? void 0 : collapsed.slice(0, 120);
115966
115986
  }
115967
- async function landTlItems(editor, items, failedIds) {
115987
+ async function landTlItems(editor, items, isActive) {
115968
115988
  const landedIds = landedIdsOf(editor);
115969
115989
  const pending = items.filter((item) => !landedIds.has(item.galleryId));
115970
- if (pending.length === 0) return;
115990
+ if (pending.length === 0) return true;
115971
115991
  const assets = [];
115972
115992
  const landed = [];
115973
115993
  for (const item of pending) {
115994
+ if (!isActive()) return false;
115974
115995
  const assetId = tlAssetIdFor(item.galleryId);
115975
115996
  if (editor.getAsset(assetId) === void 0) try {
115976
115997
  const blob = await fetchAttachmentBlob(item.attachment);
@@ -115991,7 +116012,6 @@ img.ProseMirror-separator {
115991
116012
  }
115992
116013
  });
115993
116014
  } catch (error) {
115994
- failedIds.add(item.galleryId);
115995
116015
  console.warn("[dsh-image-gen] canvas landing skipped (attachment unreadable):", item.galleryId, error);
115996
116016
  continue;
115997
116017
  }
@@ -116000,6 +116020,9 @@ img.ProseMirror-separator {
116000
116020
  landed.push({
116001
116021
  galleryId: item.galleryId,
116002
116022
  assetId,
116023
+ attachmentId: String(item.attachment.attachmentId),
116024
+ name: item.attachment.name ?? item.galleryId,
116025
+ fromConversation: item.fromConversation === true,
116003
116026
  w: size.w,
116004
116027
  h: size.h,
116005
116028
  ...prompt !== void 0 ? { prompt } : {},
@@ -116007,10 +116030,13 @@ img.ProseMirror-separator {
116007
116030
  ...typeof item.model === "string" && item.model.length > 0 ? { model: item.model } : {}
116008
116031
  });
116009
116032
  }
116010
- if (landed.length === 0) return;
116033
+ if (!isActive()) return false;
116034
+ const nowLanded = landedIdsOf(editor);
116035
+ const ready = landed.filter((item) => !nowLanded.has(item.galleryId));
116036
+ if (ready.length === 0) return true;
116011
116037
  if (assets.length > 0) editor.createAssets(assets);
116012
116038
  const rows = [];
116013
- for (let index = 0; index < landed.length; index += GRID_COLS) rows.push(landed.slice(index, index + GRID_COLS));
116039
+ for (let index = 0; index < ready.length; index += GRID_COLS) rows.push(ready.slice(index, index + GRID_COLS));
116014
116040
  const rowSizes = rows.map((row) => ({
116015
116041
  width: row.reduce((sum, cell) => sum + cell.w, 0) + GRID_GAP * (row.length - 1),
116016
116042
  height: Math.max(...row.map((cell) => cell.h))
@@ -116045,7 +116071,7 @@ img.ProseMirror-separator {
116045
116071
  let cursorX = originX + (blockWidth - rowSize.width) / 2;
116046
116072
  for (const cell of row) {
116047
116073
  shapes.push({
116048
- id: createShapeId(),
116074
+ id: createShapeId(`ig-${cell.galleryId}`),
116049
116075
  type: "image",
116050
116076
  x: Math.round(cursorX),
116051
116077
  y: Math.round(cursorY),
@@ -116056,6 +116082,9 @@ img.ProseMirror-separator {
116056
116082
  },
116057
116083
  meta: {
116058
116084
  galleryId: cell.galleryId,
116085
+ attachmentId: cell.attachmentId,
116086
+ name: cell.name,
116087
+ fromConversation: cell.fromConversation,
116059
116088
  ...cell.prompt !== void 0 ? { prompt: cell.prompt } : {},
116060
116089
  ...cell.provider !== void 0 ? { provider: cell.provider } : {},
116061
116090
  ...cell.model !== void 0 ? { model: cell.model } : {}
@@ -116081,42 +116110,53 @@ img.ProseMirror-separator {
116081
116110
  inset: GRID_GAP
116082
116111
  });
116083
116112
  }
116113
+ return true;
116084
116114
  }
116085
116115
  const StudioTlCanvas = (0, react.memo)(function StudioTlCanvas() {
116086
- const editorRef = (0, react.useRef)(null);
116087
- const landingRef = (0, react.useRef)(false);
116088
- /** Per-canvas attachment misses, so one bad blob cannot spin the reconcile. */
116089
- const failedRef = (0, react.useRef)(/* @__PURE__ */ new Set());
116090
- const processQueue = (0, react.useCallback)(() => {
116091
- const editor = editorRef.current;
116092
- if (editor === null) return;
116093
- if (landingRef.current) return;
116094
- const landedIds = landedIdsOf(editor);
116095
- const items = getTlLandings().filter((item) => !landedIds.has(item.galleryId) && !failedRef.current.has(item.galleryId));
116096
- if (items.length === 0) return;
116097
- landingRef.current = true;
116098
- landTlItems(editor, items, failedRef.current).catch((error) => {
116099
- console.warn("[dsh-image-gen] tldraw canvas landing failed:", error);
116100
- }).finally(() => {
116101
- landingRef.current = false;
116102
- processQueue();
116103
- });
116104
- }, []);
116105
- (0, react.useEffect)(() => subscribeTlLandings(processQueue), [processQueue]);
116116
+ const sessionId = (0, react.useId)();
116106
116117
  return (0, react_jsx_runtime.jsx)("div", {
116107
116118
  className: "dsh-ig-tl-canvas",
116108
116119
  children: (0, react_jsx_runtime.jsx)(Tldraw, {
116120
+ persistenceKey: "dsh-image-gen-workbench-v1",
116121
+ sessionId,
116109
116122
  components: { Toolbar: StudioToolbar },
116110
116123
  onMount: (editor) => {
116111
- editorRef.current = editor;
116124
+ let active = true;
116112
116125
  applyBrandTheme(editor);
116113
116126
  editor.updateInstanceState({ isGridMode: true });
116114
116127
  console.info(`[dsh-image-gen] tldraw mounted (instance ${editor.id})`);
116115
- processQueue();
116116
- const stopSync = startCanvasSync(editor);
116128
+ const stopLanding = registerTlLandingConsumer((items) => {
116129
+ const generation = getTlLandingGeneration();
116130
+ return landTlItems(editor, items, () => active && generation === getTlLandingGeneration());
116131
+ });
116132
+ let stopSync = startCanvasSync(editor);
116133
+ const clear = () => {
116134
+ stopSync();
116135
+ editor.complete();
116136
+ editor.run(() => {
116137
+ editor.selectNone();
116138
+ const pages = editor.getPages();
116139
+ for (const page of pages) editor.deleteShapes([...editor.getPageShapeIds(page.id)]);
116140
+ for (const page of pages.slice(1)) editor.deletePage(page.id);
116141
+ editor.deleteAssets(editor.getAssets());
116142
+ editor.setCamera({
116143
+ x: 0,
116144
+ y: 0,
116145
+ z: 1
116146
+ });
116147
+ }, {
116148
+ history: "ignore",
116149
+ ignoreShapeLock: true
116150
+ });
116151
+ editor.clearHistory();
116152
+ stopSync = startCanvasSync(editor);
116153
+ };
116154
+ mountedCanvases.add(clear);
116117
116155
  return () => {
116156
+ active = false;
116157
+ mountedCanvases.delete(clear);
116158
+ stopLanding();
116118
116159
  stopSync();
116119
- editorRef.current = null;
116120
116160
  };
116121
116161
  }
116122
116162
  })
@@ -116221,13 +116261,19 @@ img.ProseMirror-separator {
116221
116261
  configuredCount: "API 已配置 · {count}",
116222
116262
  unconfiguredStatus: "API 未配置",
116223
116263
  providerStatus: "云端提供商与模型状态",
116264
+ configuredSplit: "API {api} · 订阅 {sub}",
116265
+ subSignedIn: "已登录",
116266
+ subSignedOut: "未登录",
116224
116267
  collapseSidebar: "折叠最近生成",
116225
116268
  expandSidebar: "展开最近生成",
116226
116269
  collapseGenerate: "折叠生成面板",
116227
116270
  expandGenerate: "展开生成面板",
116228
116271
  generatePanel: "生成面板",
116229
116272
  findInspiration: "找灵感",
116230
- infiniteCanvasHint: "无限画布与侧边栏会话联动,才是完整功能。打开 DSH 右侧边栏的「图像工作台」即可解锁。"
116273
+ infiniteCanvasHint: "无限画布与侧边栏会话联动,才是完整功能。打开 DSH 右侧边栏的「图像工作台」即可解锁。",
116274
+ clearCanvas: "清空画布",
116275
+ clearCanvasDescription: "将永久清空所有画布页面的内容、图片资产和撤销历史。画廊已保存的卡片及原图不受影响,此操作无法撤销。",
116276
+ canvasCleared: "画布已清空,画廊不受影响"
116231
116277
  },
116232
116278
  en: {
116233
116279
  title: "Cloud Image Studio",
@@ -116323,18 +116369,24 @@ img.ProseMirror-separator {
116323
116369
  configuredCount: "API configured · {count}",
116324
116370
  unconfiguredStatus: "Not configured",
116325
116371
  providerStatus: "Provider & Model Status",
116372
+ configuredSplit: "API {api} · Sub {sub}",
116373
+ subSignedIn: "Signed in",
116374
+ subSignedOut: "Signed out",
116326
116375
  collapseSidebar: "Collapse sidebar",
116327
116376
  expandSidebar: "Expand recent list",
116328
116377
  collapseGenerate: "Collapse generate panel",
116329
116378
  expandGenerate: "Expand generate panel",
116330
116379
  generatePanel: "Generate",
116331
116380
  findInspiration: "Find inspiration",
116332
- 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."
116381
+ 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.",
116382
+ clearCanvas: "Clear canvas",
116383
+ clearCanvasDescription: "Permanently clear all canvas pages, image assets, and undo history. Saved gallery cards and original images are unaffected. This cannot be undone.",
116384
+ canvasCleared: "Canvas cleared. Gallery unchanged."
116333
116385
  }
116334
116386
  };
116335
116387
  /** localStorage flag remembering that the one-time infinite-canvas hint was dismissed. */
116336
116388
  const INFINITE_CANVAS_HINT_KEY = "dsh-ig-infinite-canvas-hint-dismissed";
116337
- const StudioView = ({ locale, workspace, initialPrompt, initialCanvasSurface, showInfiniteCanvasHint, onInitialPromptApplied, onOpenInspiration }) => {
116389
+ const StudioView = ({ locale, credentialEvents, workspace, initialPrompt, initialCanvasSurface, showInfiniteCanvasHint, onInitialPromptApplied, onOpenInspiration }) => {
116338
116390
  const [lang, setLang] = (0, react.useState)(() => locale?.getSnapshot?.().active?.startsWith("en") ? "en" : "zh");
116339
116391
  const [config, setConfig] = (0, react.useState)(null);
116340
116392
  const [configLoading, setConfigLoading] = (0, react.useState)(true);
@@ -116377,6 +116429,15 @@ img.ProseMirror-separator {
116377
116429
  const dragStartRef = (0, react.useRef)(null);
116378
116430
  const hasDraggedRef = (0, react.useRef)(false);
116379
116431
  const [canvasSurface, setCanvasSurface] = (0, react.useState)(initialCanvasSurface ?? "preview");
116432
+ const [showClearCanvasModal, setShowClearCanvasModal] = (0, react.useState)(false);
116433
+ (0, react.useEffect)(() => {
116434
+ if (!showClearCanvasModal) return;
116435
+ const close = (event) => {
116436
+ if (event.key === "Escape") setShowClearCanvasModal(false);
116437
+ };
116438
+ window.addEventListener("keydown", close);
116439
+ return () => window.removeEventListener("keydown", close);
116440
+ }, [showClearCanvasModal]);
116380
116441
  /** Generate-panel state from before the infinite canvas was opened, so the
116381
116442
  * panel is restored (not just forced open) when returning to preview. */
116382
116443
  const generatePanelBeforeInfiniteRef = (0, react.useRef)(false);
@@ -116432,15 +116493,28 @@ img.ProseMirror-separator {
116432
116493
  const maxReferences = comparisonEnabled ? comparisonProviders.includes("dashscope") ? 3 : 5 : provider === "dashscope" ? 3 : 5;
116433
116494
  const referencesRef = (0, react.useRef)(references);
116434
116495
  referencesRef.current = references;
116496
+ const comparisonSelectionsAllSubscription = comparisonEnabled && comparisonProviders.length > 0 && comparisonProviders.every((provider) => isSubscriptionProvider(provider));
116497
+ const singleProviderIsSubscription = !comparisonEnabled && isSubscriptionProvider(provider);
116435
116498
  const t = (key, values) => {
116436
116499
  let text = COPY$1[lang][key];
116437
116500
  for (const [name, value] of Object.entries(values ?? {})) text = text.replace(`{${name}}`, value);
116438
116501
  return text;
116439
116502
  };
116440
116503
  (0, react.useEffect)(() => locale?.subscribe?.(() => setLang(locale.getSnapshot().active?.startsWith("en") ? "en" : "zh")), [locale]);
116441
- const loadConfig = (0, react.useCallback)(async () => {
116442
- setConfigLoading(true);
116443
- setConfigError(null);
116504
+ const formStateRef = (0, react.useRef)({
116505
+ provider,
116506
+ model
116507
+ });
116508
+ formStateRef.current = {
116509
+ provider,
116510
+ model
116511
+ };
116512
+ const loadConfig = (0, react.useCallback)(async (options) => {
116513
+ const silent = options?.silent === true;
116514
+ if (!silent) {
116515
+ setConfigLoading(true);
116516
+ setConfigError(null);
116517
+ }
116444
116518
  const controller = new AbortController();
116445
116519
  try {
116446
116520
  const response = await fetch(STUDIO_ROUTE, {
@@ -116450,20 +116524,35 @@ img.ProseMirror-separator {
116450
116524
  const payload = await response.json();
116451
116525
  if (!response.ok || !("providers" in payload)) throw new Error("error" in payload && payload.error ? payload.error : "Studio unavailable");
116452
116526
  setConfig(payload);
116527
+ if (silent) {
116528
+ const { provider: currentProvider, model: currentModel } = formStateRef.current;
116529
+ const activeProfile = payload.providers.find((item) => item.provider === currentProvider);
116530
+ if (activeProfile !== void 0 && activeProfile.model !== currentModel) setModel(activeProfile.model);
116531
+ setComparisonProviders((current) => current.filter((selectedProvider) => payload.providers.some((item) => item.provider === selectedProvider && item.configured)));
116532
+ return;
116533
+ }
116453
116534
  const initial = payload.providers.find((item) => item.provider === payload.activeProvider) ?? payload.providers[0];
116454
116535
  if (initial !== void 0) {
116455
116536
  applyProvider(initial);
116456
116537
  setComparisonProviders(initialComparisonProviders(payload.providers, initial.provider));
116457
116538
  }
116458
116539
  } catch (fetchError) {
116459
- if (!controller.signal.aborted) setConfigError(messageOf(fetchError));
116540
+ if (!silent && !controller.signal.aborted) setConfigError(messageOf(fetchError));
116460
116541
  } finally {
116461
- setConfigLoading(false);
116542
+ if (!silent) setConfigLoading(false);
116462
116543
  }
116463
116544
  }, []);
116464
116545
  (0, react.useEffect)(() => {
116465
116546
  loadConfig();
116466
116547
  }, [loadConfig]);
116548
+ (0, react.useEffect)(() => {
116549
+ if (credentialEvents === void 0) return;
116550
+ return credentialEvents.listen(() => {
116551
+ window.setTimeout(() => {
116552
+ loadConfig({ silent: true });
116553
+ }, 1e3);
116554
+ });
116555
+ }, [credentialEvents, loadConfig]);
116467
116556
  (0, react.useEffect)(() => {
116468
116557
  let mounted = true;
116469
116558
  const load = () => void getGalleryItems().then((next) => {
@@ -116535,6 +116624,8 @@ img.ProseMirror-separator {
116535
116624
  }, []);
116536
116625
  const activeProfile = (0, react.useMemo)(() => config?.providers.find((item) => item.provider === provider), [config, provider]);
116537
116626
  const configuredCount = config?.providers.filter((item) => item.configured).length ?? 0;
116627
+ const apiConfiguredCount = config?.providers.filter((item) => item.configured && !isSubscriptionProvider(item.provider)).length ?? 0;
116628
+ const subscriptionSignedInCount = config?.providers.filter((item) => item.configured && isSubscriptionProvider(item.provider)).length ?? 0;
116538
116629
  const displayItems = (0, react.useMemo)(() => items.slice(0, visibleLimit), [items, visibleLimit]);
116539
116630
  const comparisonProfiles = (0, react.useMemo)(() => (config?.providers ?? []).filter((item) => item.configured && (mode === "generate" || item.supportsEditing)), [config, mode]);
116540
116631
  const comparisonTargets = (0, react.useMemo)(() => buildComparisonTargets(comparisonProfiles, comparisonProviders, ratio, quality), [
@@ -117130,7 +117221,10 @@ img.ProseMirror-separator {
117130
117221
  (0, react_jsx_runtime.jsx)("span", { className: `dsh-ig-status-dot ${configuredCount > 0 ? "is-ready" : "is-muted"}` }),
117131
117222
  (0, react_jsx_runtime.jsx)("span", {
117132
117223
  className: "dsh-ig-status-text",
117133
- children: configuredCount > 0 ? t("configuredCount", { count: String(configuredCount) }) : t("unconfiguredStatus")
117224
+ children: configuredCount > 0 ? subscriptionSignedInCount > 0 ? t("configuredSplit", {
117225
+ api: String(apiConfiguredCount),
117226
+ sub: String(subscriptionSignedInCount)
117227
+ }) : t("configuredCount", { count: String(apiConfiguredCount) }) : t("unconfiguredStatus")
117134
117228
  }),
117135
117229
  (0, react_jsx_runtime.jsx)(ChevronDown, {
117136
117230
  size: 11,
@@ -117151,7 +117245,7 @@ img.ProseMirror-separator {
117151
117245
  children: [(0, react_jsx_runtime.jsx)("strong", { children: p.label }), (0, react_jsx_runtime.jsx)("small", { children: p.model })]
117152
117246
  }), (0, react_jsx_runtime.jsx)("span", {
117153
117247
  className: "dsh-ig-status-item-tag",
117154
- children: p.configured ? t("configured") : t("unconfigured")
117248
+ children: isSubscriptionProvider(p.provider) ? p.configured ? t("subSignedIn") : t("subSignedOut") : p.configured ? t("configured") : t("unconfigured")
117155
117249
  })]
117156
117250
  }, p.provider))
117157
117251
  })]
@@ -117214,6 +117308,12 @@ img.ProseMirror-separator {
117214
117308
  (0, react_jsx_runtime.jsxs)("div", {
117215
117309
  className: "dsh-ig-canvas-toolbar-right",
117216
117310
  children: [
117311
+ canvasSurface === "infinite" && (0, react_jsx_runtime.jsxs)("button", {
117312
+ type: "button",
117313
+ onClick: () => setShowClearCanvasModal(true),
117314
+ title: t("clearCanvas"),
117315
+ children: [(0, react_jsx_runtime.jsx)(Trash2, { size: 14 }), (0, react_jsx_runtime.jsx)("span", { children: t("clearCanvas") })]
117316
+ }),
117217
117317
  canvasSurface === "preview" && selected !== null && (0, react_jsx_runtime.jsxs)("button", {
117218
117318
  type: "button",
117219
117319
  onClick: startNew,
@@ -117591,7 +117691,26 @@ img.ProseMirror-separator {
117591
117691
  }),
117592
117692
  (0, react_jsx_runtime.jsx)("p", { children: t("compareParameterHint") })
117593
117693
  ]
117594
- }), (0, react_jsx_runtime.jsxs)("div", {
117694
+ }), comparisonSelectionsAllSubscription ? (0, react_jsx_runtime.jsxs)("div", {
117695
+ className: "dsh-ig-field-grid",
117696
+ children: [(0, react_jsx_runtime.jsx)(FieldSelect, {
117697
+ label: t("ratio"),
117698
+ value: "auto",
117699
+ onChange: () => {},
117700
+ options: [{
117701
+ value: "auto",
117702
+ label: lang === "en" ? "Channel default" : "通道默认"
117703
+ }]
117704
+ }), (0, react_jsx_runtime.jsx)(FieldSelect, {
117705
+ label: t("quality"),
117706
+ value: "auto",
117707
+ onChange: () => {},
117708
+ options: [{
117709
+ value: "auto",
117710
+ label: lang === "en" ? "Channel default" : "通道默认"
117711
+ }]
117712
+ })]
117713
+ }) : (0, react_jsx_runtime.jsxs)("div", {
117595
117714
  className: "dsh-ig-field-grid",
117596
117715
  children: [(0, react_jsx_runtime.jsx)(FieldSelect, {
117597
117716
  label: t("ratio"),
@@ -117625,7 +117744,26 @@ img.ProseMirror-separator {
117625
117744
  }]
117626
117745
  })]
117627
117746
  }),
117628
- (0, react_jsx_runtime.jsxs)("div", {
117747
+ singleProviderIsSubscription ? (0, react_jsx_runtime.jsxs)("div", {
117748
+ className: "dsh-ig-field-grid",
117749
+ children: [(0, react_jsx_runtime.jsx)(FieldSelect, {
117750
+ label: t("ratio"),
117751
+ value: "auto",
117752
+ onChange: () => {},
117753
+ options: [{
117754
+ value: "auto",
117755
+ label: lang === "en" ? "Channel default" : "通道默认"
117756
+ }]
117757
+ }), (0, react_jsx_runtime.jsx)(FieldSelect, {
117758
+ label: t("quality"),
117759
+ value: "auto",
117760
+ onChange: () => {},
117761
+ options: [{
117762
+ value: "auto",
117763
+ label: lang === "en" ? "Channel default" : "通道默认"
117764
+ }]
117765
+ })]
117766
+ }) : (0, react_jsx_runtime.jsxs)("div", {
117629
117767
  className: "dsh-ig-field-grid",
117630
117768
  children: [(0, react_jsx_runtime.jsx)(FieldSelect, {
117631
117769
  label: t("ratio"),
@@ -117820,6 +117958,57 @@ img.ProseMirror-separator {
117820
117958
  })
117821
117959
  ]
117822
117960
  }),
117961
+ showClearCanvasModal && (0, react_jsx_runtime.jsx)("div", {
117962
+ className: "dsh-ig-workbench-modal-backdrop",
117963
+ onClick: () => setShowClearCanvasModal(false),
117964
+ children: (0, react_jsx_runtime.jsxs)("div", {
117965
+ className: "dsh-ig-workbench-modal-box",
117966
+ role: "dialog",
117967
+ "aria-modal": "true",
117968
+ "aria-label": t("clearCanvas"),
117969
+ onClick: (event) => event.stopPropagation(),
117970
+ children: [
117971
+ (0, react_jsx_runtime.jsxs)("div", {
117972
+ className: "dsh-ig-workbench-modal-header",
117973
+ children: [(0, react_jsx_runtime.jsx)(TriangleAlert, {
117974
+ size: 20,
117975
+ color: "#dc2626"
117976
+ }), (0, react_jsx_runtime.jsx)("strong", { children: t("clearCanvas") })]
117977
+ }),
117978
+ (0, react_jsx_runtime.jsx)("p", {
117979
+ className: "dsh-ig-workbench-modal-desc",
117980
+ children: t("clearCanvasDescription")
117981
+ }),
117982
+ (0, react_jsx_runtime.jsxs)("div", {
117983
+ className: "dsh-ig-workbench-modal-actions",
117984
+ children: [(0, react_jsx_runtime.jsx)("button", {
117985
+ autoFocus: true,
117986
+ type: "button",
117987
+ className: "dsh-ig-workbench-modal-cancel",
117988
+ onClick: () => setShowClearCanvasModal(false),
117989
+ children: t("cancel")
117990
+ }), (0, react_jsx_runtime.jsx)("button", {
117991
+ type: "button",
117992
+ className: "dsh-ig-workbench-modal-danger",
117993
+ onClick: () => {
117994
+ try {
117995
+ if (!clearStudioTlCanvases()) {
117996
+ flash(t("loading"));
117997
+ return;
117998
+ }
117999
+ setShowClearCanvasModal(false);
118000
+ flash(t("canvasCleared"));
118001
+ } catch (error) {
118002
+ setError(messageOf(error));
118003
+ setShowClearCanvasModal(false);
118004
+ }
118005
+ },
118006
+ children: t("clearCanvas")
118007
+ })]
118008
+ })
118009
+ ]
118010
+ })
118011
+ }),
117823
118012
  showDeleteModal && selected !== null && (0, react_jsx_runtime.jsx)("div", {
117824
118013
  className: "dsh-ig-workbench-modal-backdrop",
117825
118014
  onClick: () => setShowDeleteModal(false),
@@ -119427,7 +119616,7 @@ img.ProseMirror-separator {
119427
119616
  return "1:1";
119428
119617
  }
119429
119618
  const GalleryViewTab = (props) => {
119430
- const { locale, sessionId, useSessions, useWorkspaces, inSidebar, defaultTab, initialCanvasSurface } = props;
119619
+ const { locale, sessionId, useSessions, useWorkspaces, credentialEvents, inSidebar, defaultTab, initialCanvasSurface } = props;
119431
119620
  const [activeTab, setActiveTab] = (0, react.useState)(defaultTab ?? "gallery");
119432
119621
  const [studioDraft, setStudioDraft] = (0, react.useState)(void 0);
119433
119622
  const [items, setItems] = (0, react.useState)([]);
@@ -120184,6 +120373,7 @@ img.ProseMirror-separator {
120184
120373
  onUsePrompt: useInspirationPrompt
120185
120374
  }) : (0, react_jsx_runtime.jsx)(StudioView, {
120186
120375
  locale,
120376
+ credentialEvents,
120187
120377
  workspace: activeWorkspace,
120188
120378
  initialPrompt: studioDraft,
120189
120379
  initialCanvasSurface,
@@ -120709,6 +120899,266 @@ img.ProseMirror-separator {
120709
120899
  if (fromBlockContent?.type === "image") return fromBlockContent.attachment;
120710
120900
  }
120711
120901
  //#endregion
120902
+ //#region lib/types/client/image-result-node.js
120903
+ const IMAGE_RESULT_NODE_KIND = "dsh-image-result";
120904
+ /**
120905
+ * One image result row per Turn, anchored at its own tool/result position while
120906
+ * the Turn runs, then optionally re-anchored beside the final answer once the
120907
+ * Turn closes (only needed under the folding Compact transcript view).
120908
+ */
120909
+ function createImageResultDefinition(options = {}) {
120910
+ return {
120911
+ kind: IMAGE_RESULT_NODE_KIND,
120912
+ target: "chat",
120913
+ match: (event) => {
120914
+ if (event.type === "tool/ptc-dispatch") {
120915
+ const subCallId = event.data.subCallId;
120916
+ if (typeof subCallId !== "string" || subCallId === "") return null;
120917
+ return imageResultFromPtcDispatch(event) === void 0 ? null : {
120918
+ id: `ptc:${subCallId}`,
120919
+ role: "start"
120920
+ };
120921
+ }
120922
+ const turn = eventTurn(event);
120923
+ if (turn === void 0) return null;
120924
+ if (event.type === "turn/start") return {
120925
+ id: String(turn),
120926
+ role: "start"
120927
+ };
120928
+ if (event.type === "turn/end") return {
120929
+ id: String(turn),
120930
+ role: "update"
120931
+ };
120932
+ if (event.type === "assistant/message") return {
120933
+ id: String(turn),
120934
+ role: "update"
120935
+ };
120936
+ if (event.type === "tool/result" && imageResultFromMeta(event.data.meta) !== void 0) return {
120937
+ id: String(turn),
120938
+ role: "update"
120939
+ };
120940
+ return null;
120941
+ },
120942
+ start: (_context, match) => {
120943
+ if (match.event.type === "tool/ptc-dispatch") {
120944
+ const result = imageResultFromPtcDispatch(match.event);
120945
+ if (result === void 0) throw new Error("dsh-image-result start requires a plugin image dispatch");
120946
+ return { results: [{
120947
+ ...result,
120948
+ seq: match.event.seq
120949
+ }] };
120950
+ }
120951
+ const turn = eventTurn(match.event);
120952
+ if (match.event.type !== "turn/start" || turn === void 0) throw new Error("dsh-image-result start requires turn/start");
120953
+ return {
120954
+ turn,
120955
+ results: []
120956
+ };
120957
+ },
120958
+ update: (context, match) => {
120959
+ if (match.event.type === "turn/end") return {
120960
+ ...context.state,
120961
+ endSeq: match.event.seq
120962
+ };
120963
+ if (match.event.type === "assistant/message") return {
120964
+ ...context.state,
120965
+ answerSeq: match.event.seq
120966
+ };
120967
+ if (match.event.type === "tool/ptc-dispatch") {
120968
+ const result = imageResultFromPtcDispatch(match.event);
120969
+ return result === void 0 ? context.state : appendResult(context.state, result, match.event.seq);
120970
+ }
120971
+ if (match.event.type !== "tool/result") return context.state;
120972
+ const result = imageResultFromMeta(match.event.data.meta);
120973
+ if (result === void 0) return context.state;
120974
+ return appendResult(context.state, result, match.event.seq);
120975
+ },
120976
+ buildViewNode: (context) => {
120977
+ const state = context.state;
120978
+ const last = state?.results.at(-1);
120979
+ if (state === void 0 || last === void 0) return null;
120980
+ const location = context.matches.at(-1)?.location ?? context.start?.location ?? { kind: "unresolved" };
120981
+ return {
120982
+ key: context.key,
120983
+ kind: IMAGE_RESULT_NODE_KIND,
120984
+ id: context.id,
120985
+ target: "chat",
120986
+ anchorSeq: state.endSeq !== void 0 && compactTranscript(options) ? state.answerSeq ?? state.endSeq ?? last.seq : last.seq,
120987
+ location,
120988
+ visibility: "visible",
120989
+ data: {
120990
+ ...state.turn !== void 0 ? { turn: state.turn } : {},
120991
+ results: state.results.map(({ seq: _seq, ...result }) => result)
120992
+ }
120993
+ };
120994
+ }
120995
+ };
120996
+ }
120997
+ /** Append a parsed result unless its attachment is already collected. */
120998
+ function appendResult(state, result, seq) {
120999
+ if (state.results.some((candidate) => candidate.attachment.attachmentId === result.attachment.attachmentId)) return state;
121000
+ return {
121001
+ ...state,
121002
+ results: [...state.results, {
121003
+ ...result,
121004
+ seq
121005
+ }]
121006
+ };
121007
+ }
121008
+ function compactTranscript(options) {
121009
+ try {
121010
+ return options.isCompactTranscript?.() ?? true;
121011
+ } catch {
121012
+ return true;
121013
+ }
121014
+ }
121015
+ /** Parse the plugin-owned durable presentation metadata from a Tool result event. */
121016
+ function imageResultFromMeta(value) {
121017
+ const meta = record(value);
121018
+ if (meta?.kind !== "dsh-image-gen") return void 0;
121019
+ const attachment = imageAttachment(meta.attachment);
121020
+ if (attachment === void 0) return void 0;
121021
+ return {
121022
+ attachment,
121023
+ prompt: stringValue(meta.prompt, "Generated Image"),
121024
+ provider: stringValue(meta.provider, "google"),
121025
+ model: stringValue(meta.model, ""),
121026
+ output: stringValue(meta.output, ""),
121027
+ ...typeof meta.savedTo === "string" ? { savedTo: meta.savedTo } : {},
121028
+ ...typeof meta.seed === "number" ? { seed: meta.seed } : {}
121029
+ };
121030
+ }
121031
+ function eventTurn(event) {
121032
+ const turn = event.data.turn;
121033
+ return typeof turn === "number" && Number.isSafeInteger(turn) && turn >= 0 ? turn : void 0;
121034
+ }
121035
+ /** Plugin tools whose PTC (run_code) dispatch results are image results. */
121036
+ const PTC_IMAGE_TOOL_NAMES = /* @__PURE__ */ new Set(["generate_image", "edit_image"]);
121037
+ /**
121038
+ * Fixed summary text rendered by the tools' imageOutput(). PTC dispatch events
121039
+ * carry no presentationMeta, so provider/model/output are recovered from this
121040
+ * template rather than parsed out of natural language.
121041
+ */
121042
+ const PTC_SUMMARY_PATTERN = /^(?:Generated|Edited) one image with ([^/\s]+)\/(\S+) \((.+)\)\. Attachment ID: /;
121043
+ 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))/;
121044
+ /**
121045
+ * Parse a plugin image result from a tool/ptc-dispatch event (#38). PTC
121046
+ * run_code sub-calls carry neither a turn nor presentationMeta, so the result
121047
+ * is rebuilt from the dispatch payload itself: the image content block for the
121048
+ * attachment, arguments for the prompt (and edit source ids), and the tool's
121049
+ * fixed-format summary text for provider/model/output. Returns undefined for
121050
+ * anything that is not a successful image result from our own image tools.
121051
+ */
121052
+ function imageResultFromPtcDispatch(event) {
121053
+ if (event.type !== "tool/ptc-dispatch") return void 0;
121054
+ const data = event.data;
121055
+ if (data.isError === true) return void 0;
121056
+ if (typeof data.name !== "string" || !PTC_IMAGE_TOOL_NAMES.has(data.name)) return void 0;
121057
+ const content = Array.isArray(data.content) ? data.content : [];
121058
+ let attachment;
121059
+ let summaryText = "";
121060
+ for (const block of content) {
121061
+ const candidate = record(block);
121062
+ if (candidate === void 0) continue;
121063
+ if (candidate.type === "image" && attachment === void 0) attachment = imageAttachment(candidate.attachment);
121064
+ else if (candidate.type === "text" && summaryText === "" && typeof candidate.text === "string") summaryText = candidate.text;
121065
+ }
121066
+ if (attachment === void 0) return void 0;
121067
+ const summary = PTC_SUMMARY_PATTERN.exec(summaryText);
121068
+ const savedTo = PTC_SAVED_TO_PATTERN.exec(summaryText)?.[1];
121069
+ const args = record(data.arguments);
121070
+ const sourceIds = sourceAttachmentIds(args);
121071
+ return {
121072
+ attachment,
121073
+ prompt: stringValue(args?.prompt, "Generated Image"),
121074
+ provider: summary?.[1] ?? "",
121075
+ model: summary?.[2] ?? "",
121076
+ output: summary?.[3] ?? "",
121077
+ ...savedTo !== void 0 ? { savedTo } : {},
121078
+ ...sourceIds !== void 0 ? { sourceAttachmentIds: sourceIds } : {}
121079
+ };
121080
+ }
121081
+ /** Collect edit_image source attachment ids (single or list form) from dispatch arguments. */
121082
+ function sourceAttachmentIds(args) {
121083
+ if (args === void 0) return void 0;
121084
+ const single = typeof args.source_attachment_id === "string" ? [args.source_attachment_id] : void 0;
121085
+ const list = Array.isArray(args.source_attachment_ids) ? args.source_attachment_ids.filter((id) => typeof id === "string") : void 0;
121086
+ const ids = single ?? list;
121087
+ return ids !== void 0 && ids.length > 0 ? ids : void 0;
121088
+ }
121089
+ function imageAttachment(value) {
121090
+ const candidate = record(value);
121091
+ if (candidate === void 0 || typeof candidate.attachmentId !== "string" || !isImageMediaType(candidate.mediaType) || !positiveInteger(candidate.bytes) || !positiveInteger(candidate.width) || !positiveInteger(candidate.height)) return void 0;
121092
+ const original = record(candidate.originalDimensions);
121093
+ if (candidate.originalDimensions !== void 0 && (original === void 0 || !positiveInteger(original.width) || !positiveInteger(original.height))) return void 0;
121094
+ return {
121095
+ attachmentId: candidate.attachmentId,
121096
+ mediaType: candidate.mediaType,
121097
+ bytes: candidate.bytes,
121098
+ width: candidate.width,
121099
+ height: candidate.height,
121100
+ ...typeof candidate.name === "string" ? { name: candidate.name } : {},
121101
+ ...original === void 0 ? {} : { originalDimensions: {
121102
+ width: original.width,
121103
+ height: original.height
121104
+ } }
121105
+ };
121106
+ }
121107
+ function isImageMediaType(value) {
121108
+ return value === "image/png" || value === "image/jpeg" || value === "image/webp" || value === "image/gif";
121109
+ }
121110
+ function positiveInteger(value) {
121111
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
121112
+ }
121113
+ function stringValue(value, fallback) {
121114
+ return typeof value === "string" ? value : fallback;
121115
+ }
121116
+ function record(value) {
121117
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
121118
+ }
121119
+ //#endregion
121120
+ //#region lib/types/client/tl/conversation-landings.js
121121
+ /** Subscribe to live additions, never to card rendering or history replay. */
121122
+ function startConversationLandings(sessions) {
121123
+ let source;
121124
+ let stopEvents;
121125
+ const followCurrent = () => {
121126
+ const id = sessions.list.getSnapshot().current;
121127
+ const next = id === void 0 ? void 0 : sessions.binding(id)?.eventSource;
121128
+ if (next === source) return;
121129
+ stopEvents?.();
121130
+ source = next;
121131
+ if (next === void 0) return;
121132
+ let lastSeq = next.getSnapshot().entries.reduce((seq, entry) => Math.max(seq, entry.event.seq), -1);
121133
+ stopEvents = next.subscribe(() => {
121134
+ const change = next.getSnapshot().change;
121135
+ for (const entry of change.entries ?? []) {
121136
+ if (entry.type !== "event") continue;
121137
+ const { event } = entry;
121138
+ const isNew = event.seq > lastSeq;
121139
+ lastSeq = Math.max(lastSeq, event.seq);
121140
+ if (change.kind !== "append" || !isNew) continue;
121141
+ const result = event.type === "tool/result" ? imageResultFromMeta(event.data.meta) : imageResultFromPtcDispatch(event);
121142
+ if (result === void 0) continue;
121143
+ pushTlLandingsLive([{
121144
+ galleryId: String(result.attachment.attachmentId),
121145
+ attachment: result.attachment,
121146
+ fromConversation: true,
121147
+ prompt: result.prompt,
121148
+ provider: result.provider,
121149
+ model: result.model
121150
+ }]);
121151
+ }
121152
+ });
121153
+ };
121154
+ const stopList = sessions.list.subscribe(followCurrent);
121155
+ followCurrent();
121156
+ return () => {
121157
+ stopList();
121158
+ stopEvents?.();
121159
+ };
121160
+ }
121161
+ //#endregion
120712
121162
  //#region lib/types/client/studio-style.js
120713
121163
  const STUDIO_STYLE = `
120714
121164
  .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}
@@ -121050,224 +121500,6 @@ img.ProseMirror-separator {
121050
121500
  .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)}
121051
121501
  @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}}
121052
121502
  `;
121053
- //#endregion
121054
- //#region lib/types/client/image-result-node.js
121055
- const IMAGE_RESULT_NODE_KIND = "dsh-image-result";
121056
- /**
121057
- * One image result row per Turn, anchored at its own tool/result position while
121058
- * the Turn runs, then optionally re-anchored beside the final answer once the
121059
- * Turn closes (only needed under the folding Compact transcript view).
121060
- */
121061
- function createImageResultDefinition(options = {}) {
121062
- return {
121063
- kind: IMAGE_RESULT_NODE_KIND,
121064
- target: "chat",
121065
- match: (event) => {
121066
- if (event.type === "tool/ptc-dispatch") {
121067
- const subCallId = event.data.subCallId;
121068
- if (typeof subCallId !== "string" || subCallId === "") return null;
121069
- return imageResultFromPtcDispatch(event) === void 0 ? null : {
121070
- id: `ptc:${subCallId}`,
121071
- role: "start"
121072
- };
121073
- }
121074
- const turn = eventTurn(event);
121075
- if (turn === void 0) return null;
121076
- if (event.type === "turn/start") return {
121077
- id: String(turn),
121078
- role: "start"
121079
- };
121080
- if (event.type === "turn/end") return {
121081
- id: String(turn),
121082
- role: "update"
121083
- };
121084
- if (event.type === "assistant/message") return {
121085
- id: String(turn),
121086
- role: "update"
121087
- };
121088
- if (event.type === "tool/result" && imageResultFromMeta(event.data.meta) !== void 0) return {
121089
- id: String(turn),
121090
- role: "update"
121091
- };
121092
- return null;
121093
- },
121094
- start: (_context, match) => {
121095
- if (match.event.type === "tool/ptc-dispatch") {
121096
- const result = imageResultFromPtcDispatch(match.event);
121097
- if (result === void 0) throw new Error("dsh-image-result start requires a plugin image dispatch");
121098
- return { results: [{
121099
- ...result,
121100
- seq: match.event.seq
121101
- }] };
121102
- }
121103
- const turn = eventTurn(match.event);
121104
- if (match.event.type !== "turn/start" || turn === void 0) throw new Error("dsh-image-result start requires turn/start");
121105
- return {
121106
- turn,
121107
- results: []
121108
- };
121109
- },
121110
- update: (context, match) => {
121111
- if (match.event.type === "turn/end") return {
121112
- ...context.state,
121113
- endSeq: match.event.seq
121114
- };
121115
- if (match.event.type === "assistant/message") return {
121116
- ...context.state,
121117
- answerSeq: match.event.seq
121118
- };
121119
- if (match.event.type === "tool/ptc-dispatch") {
121120
- const result = imageResultFromPtcDispatch(match.event);
121121
- return result === void 0 ? context.state : appendResult(context.state, result, match.event.seq);
121122
- }
121123
- if (match.event.type !== "tool/result") return context.state;
121124
- const result = imageResultFromMeta(match.event.data.meta);
121125
- if (result === void 0) return context.state;
121126
- return appendResult(context.state, result, match.event.seq);
121127
- },
121128
- buildViewNode: (context) => {
121129
- const state = context.state;
121130
- const last = state?.results.at(-1);
121131
- if (state === void 0 || last === void 0) return null;
121132
- const location = context.matches.at(-1)?.location ?? context.start?.location ?? { kind: "unresolved" };
121133
- return {
121134
- key: context.key,
121135
- kind: IMAGE_RESULT_NODE_KIND,
121136
- id: context.id,
121137
- target: "chat",
121138
- anchorSeq: state.endSeq !== void 0 && compactTranscript(options) ? state.answerSeq ?? state.endSeq ?? last.seq : last.seq,
121139
- location,
121140
- visibility: "visible",
121141
- data: {
121142
- ...state.turn !== void 0 ? { turn: state.turn } : {},
121143
- results: state.results.map(({ seq: _seq, ...result }) => result)
121144
- }
121145
- };
121146
- }
121147
- };
121148
- }
121149
- /** Append a parsed result unless its attachment is already collected. */
121150
- function appendResult(state, result, seq) {
121151
- if (state.results.some((candidate) => candidate.attachment.attachmentId === result.attachment.attachmentId)) return state;
121152
- return {
121153
- ...state,
121154
- results: [...state.results, {
121155
- ...result,
121156
- seq
121157
- }]
121158
- };
121159
- }
121160
- function compactTranscript(options) {
121161
- try {
121162
- return options.isCompactTranscript?.() ?? true;
121163
- } catch {
121164
- return true;
121165
- }
121166
- }
121167
- /** Parse the plugin-owned durable presentation metadata from a Tool result event. */
121168
- function imageResultFromMeta(value) {
121169
- const meta = record(value);
121170
- if (meta?.kind !== "dsh-image-gen") return void 0;
121171
- const attachment = imageAttachment(meta.attachment);
121172
- if (attachment === void 0) return void 0;
121173
- return {
121174
- attachment,
121175
- prompt: stringValue(meta.prompt, "Generated Image"),
121176
- provider: stringValue(meta.provider, "google"),
121177
- model: stringValue(meta.model, ""),
121178
- output: stringValue(meta.output, ""),
121179
- ...typeof meta.savedTo === "string" ? { savedTo: meta.savedTo } : {},
121180
- ...typeof meta.seed === "number" ? { seed: meta.seed } : {}
121181
- };
121182
- }
121183
- function eventTurn(event) {
121184
- const turn = event.data.turn;
121185
- return typeof turn === "number" && Number.isSafeInteger(turn) && turn >= 0 ? turn : void 0;
121186
- }
121187
- /** Plugin tools whose PTC (run_code) dispatch results are image results. */
121188
- const PTC_IMAGE_TOOL_NAMES = /* @__PURE__ */ new Set(["generate_image", "edit_image"]);
121189
- /**
121190
- * Fixed summary text rendered by the tools' imageOutput(). PTC dispatch events
121191
- * carry no presentationMeta, so provider/model/output are recovered from this
121192
- * template rather than parsed out of natural language.
121193
- */
121194
- const PTC_SUMMARY_PATTERN = /^(?:Generated|Edited) one image with ([^/\s]+)\/(\S+) \((.+)\)\. Attachment ID: /;
121195
- 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))/;
121196
- /**
121197
- * Parse a plugin image result from a tool/ptc-dispatch event (#38). PTC
121198
- * run_code sub-calls carry neither a turn nor presentationMeta, so the result
121199
- * is rebuilt from the dispatch payload itself: the image content block for the
121200
- * attachment, arguments for the prompt (and edit source ids), and the tool's
121201
- * fixed-format summary text for provider/model/output. Returns undefined for
121202
- * anything that is not a successful image result from our own image tools.
121203
- */
121204
- function imageResultFromPtcDispatch(event) {
121205
- if (event.type !== "tool/ptc-dispatch") return void 0;
121206
- const data = event.data;
121207
- if (data.isError === true) return void 0;
121208
- if (typeof data.name !== "string" || !PTC_IMAGE_TOOL_NAMES.has(data.name)) return void 0;
121209
- const content = Array.isArray(data.content) ? data.content : [];
121210
- let attachment;
121211
- let summaryText = "";
121212
- for (const block of content) {
121213
- const candidate = record(block);
121214
- if (candidate === void 0) continue;
121215
- if (candidate.type === "image" && attachment === void 0) attachment = imageAttachment(candidate.attachment);
121216
- else if (candidate.type === "text" && summaryText === "" && typeof candidate.text === "string") summaryText = candidate.text;
121217
- }
121218
- if (attachment === void 0) return void 0;
121219
- const summary = PTC_SUMMARY_PATTERN.exec(summaryText);
121220
- const savedTo = PTC_SAVED_TO_PATTERN.exec(summaryText)?.[1];
121221
- const args = record(data.arguments);
121222
- const sourceIds = sourceAttachmentIds(args);
121223
- return {
121224
- attachment,
121225
- prompt: stringValue(args?.prompt, "Generated Image"),
121226
- provider: summary?.[1] ?? "",
121227
- model: summary?.[2] ?? "",
121228
- output: summary?.[3] ?? "",
121229
- ...savedTo !== void 0 ? { savedTo } : {},
121230
- ...sourceIds !== void 0 ? { sourceAttachmentIds: sourceIds } : {}
121231
- };
121232
- }
121233
- /** Collect edit_image source attachment ids (single or list form) from dispatch arguments. */
121234
- function sourceAttachmentIds(args) {
121235
- if (args === void 0) return void 0;
121236
- const single = typeof args.source_attachment_id === "string" ? [args.source_attachment_id] : void 0;
121237
- const list = Array.isArray(args.source_attachment_ids) ? args.source_attachment_ids.filter((id) => typeof id === "string") : void 0;
121238
- const ids = single ?? list;
121239
- return ids !== void 0 && ids.length > 0 ? ids : void 0;
121240
- }
121241
- function imageAttachment(value) {
121242
- const candidate = record(value);
121243
- if (candidate === void 0 || typeof candidate.attachmentId !== "string" || !isImageMediaType(candidate.mediaType) || !positiveInteger(candidate.bytes) || !positiveInteger(candidate.width) || !positiveInteger(candidate.height)) return void 0;
121244
- const original = record(candidate.originalDimensions);
121245
- if (candidate.originalDimensions !== void 0 && (original === void 0 || !positiveInteger(original.width) || !positiveInteger(original.height))) return void 0;
121246
- return {
121247
- attachmentId: candidate.attachmentId,
121248
- mediaType: candidate.mediaType,
121249
- bytes: candidate.bytes,
121250
- width: candidate.width,
121251
- height: candidate.height,
121252
- ...typeof candidate.name === "string" ? { name: candidate.name } : {},
121253
- ...original === void 0 ? {} : { originalDimensions: {
121254
- width: original.width,
121255
- height: original.height
121256
- } }
121257
- };
121258
- }
121259
- function isImageMediaType(value) {
121260
- return value === "image/png" || value === "image/jpeg" || value === "image/webp" || value === "image/gif";
121261
- }
121262
- function positiveInteger(value) {
121263
- return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
121264
- }
121265
- function stringValue(value, fallback) {
121266
- return typeof value === "string" ? value : fallback;
121267
- }
121268
- function record(value) {
121269
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
121270
- }
121271
121503
  //#endregion
121272
121504
  //#region lib/types/client/conversation-image-revisions.js
121273
121505
  const STORAGE_PREFIX = "dsh_image_gen_conversation_revisions:";
@@ -121330,7 +121562,7 @@ img.ProseMirror-separator {
121330
121562
  function isRevision(value) {
121331
121563
  if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
121332
121564
  const candidate = value;
121333
- return isAttachment(candidate.attachment) && typeof candidate.provider === "string" && CLOUD_IMAGE_PROVIDERS.includes(candidate.provider) && typeof candidate.prompt === "string" && typeof candidate.model === "string" && typeof candidate.output === "string" && typeof candidate.createdAt === "number" && Number.isFinite(candidate.createdAt) && typeof candidate.ratio === "string" && typeof candidate.quality === "string";
121565
+ return isAttachment(candidate.attachment) && typeof candidate.provider === "string" && STUDIO_PROVIDERS.includes(candidate.provider) && typeof candidate.prompt === "string" && typeof candidate.model === "string" && typeof candidate.output === "string" && typeof candidate.createdAt === "number" && Number.isFinite(candidate.createdAt) && typeof candidate.ratio === "string" && typeof candidate.quality === "string";
121334
121566
  }
121335
121567
  function isAttachment(value) {
121336
121568
  if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
@@ -121375,19 +121607,36 @@ img.ProseMirror-separator {
121375
121607
  switchFailed: "Switch failed"
121376
121608
  }
121377
121609
  };
121378
- /** Short row labels; the settings card keeps the longer descriptive names. */
121610
+ /** Provider row names, per locale. Brand words stay untranslated; only the
121611
+ * qualifier ("订阅" / "Sub") localizes, so the pill and the menu agree with the
121612
+ * settings card in both languages. */
121379
121613
  const PILL_PROVIDER_LABELS = {
121380
- google: "Gemini",
121381
- openai: "OpenAI",
121382
- "openai-compat": "OpenAI 兼容",
121383
- seedream: "Seedream",
121384
- dashscope: "DashScope",
121385
- xai: "Grok",
121386
- zhipu: "智谱 GLM",
121387
- comfyui: "ComfyUI",
121388
- "chatgpt-sub": "ChatGPT 订阅",
121389
- "grok-sub": "Grok 订阅",
121390
- "google-sub": "Google 订阅"
121614
+ zh: {
121615
+ google: "Gemini",
121616
+ openai: "OpenAI",
121617
+ "openai-compat": "OpenAI 兼容",
121618
+ seedream: "Seedream",
121619
+ dashscope: "DashScope",
121620
+ xai: "Grok",
121621
+ zhipu: "智谱 GLM",
121622
+ comfyui: "ComfyUI",
121623
+ "chatgpt-sub": "ChatGPT 订阅",
121624
+ "grok-sub": "Grok 订阅",
121625
+ "google-sub": "Google 订阅"
121626
+ },
121627
+ en: {
121628
+ google: "Gemini",
121629
+ openai: "OpenAI",
121630
+ "openai-compat": "OpenAI Compatible",
121631
+ seedream: "Seedream",
121632
+ dashscope: "DashScope",
121633
+ xai: "Grok",
121634
+ zhipu: "Zhipu GLM",
121635
+ comfyui: "ComfyUI",
121636
+ "chatgpt-sub": "ChatGPT Sub",
121637
+ "grok-sub": "Grok Sub",
121638
+ "google-sub": "Google Sub"
121639
+ }
121391
121640
  };
121392
121641
  /** Compute the fixed-position anchor from the pill button: open upward when
121393
121642
  * the chat pane leaves room above the composer, drop down otherwise. */
@@ -121639,7 +121888,7 @@ img.ProseMirror-separator {
121639
121888
  }),
121640
121889
  (0, react_jsx_runtime.jsx)("span", {
121641
121890
  className: "dsh-ig-pill-provider",
121642
- children: PILL_PROVIDER_LABELS[current]
121891
+ children: PILL_PROVIDER_LABELS[lang][current]
121643
121892
  }),
121644
121893
  (0, react_jsx_runtime.jsx)("span", {
121645
121894
  className: `dsh-ig-pill-caret${open ? " dsh-ig-pill-caret-open" : ""}`,
@@ -121685,7 +121934,7 @@ img.ProseMirror-separator {
121685
121934
  className: "dsh-ig-pill-option-text",
121686
121935
  children: [(0, react_jsx_runtime.jsx)("span", {
121687
121936
  className: "dsh-ig-pill-option-name",
121688
- children: PILL_PROVIDER_LABELS[provider]
121937
+ children: PILL_PROVIDER_LABELS[lang][provider]
121689
121938
  }), (0, react_jsx_runtime.jsx)("span", {
121690
121939
  className: "dsh-ig-pill-option-sub",
121691
121940
  children: subtitleOf(provider)
@@ -121723,16 +121972,20 @@ img.ProseMirror-separator {
121723
121972
  }
121724
121973
  /** Styles for the pill and its fixed-position menu; injected by the client entry. */
121725
121974
  const PROVIDER_PILL_STYLE = `
121726
- .dsh-ig-pill-root{position:relative;display:inline-flex;align-items:center;height:26px}
121727
- .dsh-ig-pill-button{display:inline-flex;align-items:center;gap:5px;height:26px;padding:0 8px 0 7px;border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:13px;background:var(--dsw-alias-bg-layer-3,#fff);color:var(--dsw-alias-label-secondary,#4b5563);font:inherit;font-size:12px;line-height:1;cursor:pointer;transition:border-color .15s,background .15s,color .15s;-webkit-user-select:none;user-select:none}
121975
+ .dsh-ig-pill-root{position:relative;display:inline-flex;align-items:center;height:28px}
121976
+ .dsh-ig-pill-button{display:inline-flex;align-items:center;gap:5px;height:28px;padding:0 9px 0 8px;border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:14px;background:var(--dsw-alias-bg-layer-3,#fff);color:var(--dsw-alias-label-secondary,#4b5563);font:inherit;font-size:13px;line-height:20px;cursor:pointer;transition:border-color .15s,background .15s,color .15s;-webkit-user-select:none;user-select:none}
121728
121977
  .dsh-ig-pill-button:hover:not(:disabled){border-color:var(--dsw-alias-label-dimmed,#9ca3af);color:var(--dsw-alias-label-primary,#111827)}
121729
121978
  .dsh-ig-pill-button:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4c78ff);outline-offset:1px}
121730
121979
  .dsh-ig-pill-button:disabled{opacity:.5;cursor:not-allowed}
121731
121980
  .dsh-ig-pill-button-open{border-color:var(--dsw-alias-brand-primary,#4c78ff);color:var(--dsw-alias-label-primary,#111827)}
121732
121981
  .dsh-ig-pill-icon{flex:none;color:var(--dsw-alias-brand-primary,#4c78ff)}
121733
- .dsh-ig-pill-provider{max-width:96px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
121982
+ .dsh-ig-pill-provider{max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
121734
121983
  .dsh-ig-pill-caret{display:inline-flex;flex:none;transition:transform .15s}
121735
121984
  .dsh-ig-pill-caret-open{transform:rotate(180deg)}
121985
+ @container (max-width:700px){
121986
+ .dsh-ig-pill-caret{display:none}
121987
+ .dsh-ig-pill-button{padding:0 7px 0 6px}
121988
+ }
121736
121989
  .dsh-ig-pill-menu{position:fixed;z-index:1000;min-width:264px;max-width:min(320px,calc(100vw - 24px));padding:6px;border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:12px;background:var(--dsw-alias-bg-layer-3,#fff);box-shadow:0 12px 32px rgba(0,0,0,.16),0 2px 8px rgba(0,0,0,.08);max-height:min(320px,calc(100vh - 48px));overflow-y:auto}
121737
121990
  .dsh-ig-pill-menu-title{padding:6px 8px 4px;font-size:11px;font-weight:600;color:var(--dsw-alias-label-tertiary,#6b7280)}
121738
121991
  .dsh-ig-pill-option{display:flex;width:100%;align-items:center;gap:9px;padding:7px 8px;border:0;border-radius:8px;background:none;font:inherit;color:inherit;text-align:left;cursor:pointer}
@@ -121752,7 +122005,7 @@ img.ProseMirror-separator {
121752
122005
  `;
121753
122006
  //#endregion
121754
122007
  //#region lib/types/client/index.js
121755
- console.info(`[dsh-image-gen] canvas bundle 2026-09-14T00:33:35.649Z`);
122008
+ console.info(`[dsh-image-gen] canvas bundle 2026-09-14T15:26:26.831Z`);
121756
122009
  /** Right-sidebar tab-type registration id (DSH 0.1.5 `sidebarRightTabs`). */
121757
122010
  const SIDEBAR_STUDIO_TAB_ID = "dsh-image-gen/studio";
121758
122011
  /** The tab kind: a page type opened by kind, recognizing no resource address. */
@@ -121791,8 +122044,8 @@ img.ProseMirror-separator {
121791
122044
  subLoginOk: "登录成功",
121792
122045
  subModelLabel: "订阅模型",
121793
122046
  subModelHint: "由订阅通道固定,不可更改。",
121794
- subEditUnsupported: "订阅通道暂不支持图生图(编辑图片),仅支持文字生图。",
121795
- subDefaultHint: "订阅 Provider 只能生成,不能编辑图片;需要编辑时请切换到 API Key Provider。",
122047
+ subEditCapability: "订阅生图 · 图生图:支持。对话 edit_image、Studio 图生图与多模型对比均可上传参考图(最多 5 张)。",
122048
+ subDefaultHint: "订阅 Provider 已支持文字生图与图生图;图生图使用各订阅渠道默认参数。",
121796
122049
  apiKeyLabel: "{provider} API Key",
121797
122050
  apiKeyPlaceholder: "留空即可保留已配置的 Key",
121798
122051
  apiKeyHint: "安全保存为 {key};页面不会读回明文。",
@@ -121920,8 +122173,8 @@ img.ProseMirror-separator {
121920
122173
  subLoginOk: "Signed in",
121921
122174
  subModelLabel: "Subscription model",
121922
122175
  subModelHint: "Fixed by the subscription channel; not changeable.",
121923
- subEditUnsupported: "The subscription channel does not support image editing yet; text-to-image only.",
121924
- subDefaultHint: "Subscription providers can only generate; switch to an API-key provider for editing.",
122176
+ subEditCapability: "Subscription image editing is now supported: edit_image in chats, Studio and multi-model compare all accept reference images (up to 5).",
122177
+ subDefaultHint: "Subscription providers support both text-to-image and image editing; edits use each channel's default parameters.",
121925
122178
  apiKeyLabel: "{provider} API Key",
121926
122179
  apiKeyPlaceholder: "Leave empty to keep configured key",
121927
122180
  apiKeyHint: "Securely saved as {key}; never read back in plaintext.",
@@ -122319,6 +122572,11 @@ img.ProseMirror-separator {
122319
122572
  const isCompactTranscript = () => chatScope.getSnapshot().value?.transcriptView !== "normal";
122320
122573
  const locale = ctx.get("locale");
122321
122574
  const promotion = { enabled: false };
122575
+ ctx.inject(["sessions"], (owner) => {
122576
+ const sessions = owner.get("sessions");
122577
+ if (typeof sessions?.binding !== "function" || typeof sessions.list?.subscribe !== "function") return;
122578
+ owner.effect(() => startConversationLandings(sessions), "dsh-image-gen: live canvas images");
122579
+ });
122322
122580
  ctx.effect(() => {
122323
122581
  const style = document.createElement("style");
122324
122582
  style.dataset.plugin = "dsh-image-gen";
@@ -122444,7 +122702,10 @@ img.ProseMirror-separator {
122444
122702
  label: () => {
122445
122703
  return (locale?.getSnapshot?.()?.active)?.startsWith("en") ? "Gallery" : "画廊";
122446
122704
  },
122447
- inject: () => ({ locale })
122705
+ inject: () => ({
122706
+ locale,
122707
+ credentialEvents
122708
+ })
122448
122709
  }, GalleryViewTab));
122449
122710
  ctx.inject(["sidebarRightTabs"], (owner) => {
122450
122711
  const sidebarTabs = owner.get("sidebarRightTabs");
@@ -122467,6 +122728,7 @@ img.ProseMirror-separator {
122467
122728
  key: SIDEBAR_STUDIO_TAB_ID,
122468
122729
  inject: () => ({
122469
122730
  locale,
122731
+ credentialEvents,
122470
122732
  inSidebar: true,
122471
122733
  defaultTab: "studio",
122472
122734
  initialCanvasSurface: "infinite"
@@ -123718,8 +123980,8 @@ img.ProseMirror-separator {
123718
123980
  (0, react_jsx_runtime.jsx)("div", {
123719
123981
  className: "dsh-ig-field",
123720
123982
  children: (0, react_jsx_runtime.jsx)("span", {
123721
- className: "dsh-ig-label",
123722
- children: t("subEditUnsupported")
123983
+ className: "dsh-ig-hint",
123984
+ children: t("subEditCapability")
123723
123985
  })
123724
123986
  }),
123725
123987
  (0, react_jsx_runtime.jsxs)("div", {
@@ -124037,14 +124299,6 @@ img.ProseMirror-separator {
124037
124299
  };
124038
124300
  (0, react.useEffect)(() => {
124039
124301
  if (result === void 0) return;
124040
- pushTlLandings([{
124041
- galleryId: result.attachment.attachmentId,
124042
- attachment: result.attachment,
124043
- fromConversation: true,
124044
- prompt: result.prompt,
124045
- provider: result.provider,
124046
- model: result.model
124047
- }]);
124048
124302
  saveGalleryItem({
124049
124303
  id: result.attachment.attachmentId,
124050
124304
  attachment: result.attachment,