dsh-image-gen 0.6.5 → 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 (35) hide show
  1. package/README.en.md +381 -355
  2. package/README.md +381 -355
  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 +499 -354
  11. package/lib/client.js.map +1 -1
  12. package/lib/types/client/inspiration-api.d.ts +10 -0
  13. package/lib/types/client/inspiration-detail.d.ts +14 -0
  14. package/lib/types/client/tl/canvas-sync.d.ts +3 -3
  15. package/lib/types/client/tl/conversation-landings.d.ts +33 -0
  16. package/lib/types/client/tl/studio-tl-canvas.d.ts +2 -0
  17. package/lib/types/client/tl/tl-canvas-bridge.d.ts +12 -32
  18. package/lib/types/client/tldraw/generated/tldraw-assets.d.ts +12 -0
  19. package/lib/types/client/tldraw/generated/tldraw-css.d.ts +1 -0
  20. package/lib/types/client/tldraw/tldraw-style.d.ts +1 -0
  21. package/lib/types/client/tldraw/tldraw-workspace.d.ts +1 -0
  22. package/lib/types/inspiration/cache.d.ts +20 -0
  23. package/lib/types/inspiration/downloader.d.ts +27 -0
  24. package/lib/types/inspiration/routes.d.ts +13 -0
  25. package/lib/types/inspiration/sources.d.ts +33 -0
  26. package/lib/types/inspiration/types.d.ts +65 -0
  27. package/lib/types/inspiration/updater.d.ts +7 -0
  28. package/package.json +8 -10
  29. package/skills/install-dsh-image-gen/SKILL.md +22 -22
  30. package/lib/types/client/studio-chat-panel.d.ts +0 -23
  31. package/lib/types/client/tl/diagram-landing.d.ts +0 -75
  32. package/lib/types/client/tl/diagram-layout.d.ts +0 -76
  33. package/lib/types/client/tl/diagram-measure.d.ts +0 -40
  34. package/lib/types/diagram.d.ts +0 -42
  35. package/lib/types/studio-chat.d.ts +0 -48
package/lib/client.js CHANGED
@@ -343,9 +343,9 @@ window.__ModuleLoader__.load({
343
343
  }
344
344
  });
345
345
  }
346
- const listeners$1 = /* @__PURE__ */ new Set();
346
+ const listeners = /* @__PURE__ */ new Set();
347
347
  function notifyListeners() {
348
- for (const listener of listeners$1) try {
348
+ for (const listener of listeners) try {
349
349
  listener();
350
350
  } catch (err) {
351
351
  console.error("[dsh-image-gen] Gallery listener error:", err);
@@ -355,9 +355,9 @@ window.__ModuleLoader__.load({
355
355
  * Subscribe to gallery mutations (insert/delete/clear).
356
356
  */
357
357
  function subscribeGallery(listener) {
358
- listeners$1.add(listener);
358
+ listeners.add(listener);
359
359
  return () => {
360
- listeners$1.delete(listener);
360
+ listeners.delete(listener);
361
361
  };
362
362
  }
363
363
  /**
@@ -1440,6 +1440,11 @@ window.__ModuleLoader__.load({
1440
1440
  cache.delete(key);
1441
1441
  }
1442
1442
  }
1443
+ /** Clear all in-memory image caches. */
1444
+ function clearAttachmentCache() {
1445
+ cache.clear();
1446
+ currentCacheBytes = 0;
1447
+ }
1443
1448
  function enforceLimits() {
1444
1449
  while (cache.size > 30 || currentCacheBytes > 134217728) {
1445
1450
  const oldestKey = cache.keys().next().value;
@@ -76042,14 +76047,14 @@ img.ProseMirror-separator {
76042
76047
  });
76043
76048
  var useReactId = react[" useId ".trim().toString()] || (() => void 0);
76044
76049
  var count$1 = 0;
76045
- function useId(deterministicId) {
76050
+ function useId$1(deterministicId) {
76046
76051
  const [id, setId] = react.useState(useReactId());
76047
76052
  useLayoutEffect2(() => {
76048
76053
  if (!deterministicId) setId((reactId) => reactId ?? String(count$1++));
76049
76054
  }, [deterministicId]);
76050
76055
  return deterministicId || (id ? `radix-${id}` : "");
76051
76056
  }
76052
- __name$25(useId, "useId");
76057
+ __name$25(useId$1, "useId");
76053
76058
  //#endregion
76054
76059
  //#region node_modules/.pnpm/@radix-ui+react-direction@1_8310f062454398ef3b849e7d93575bcb/node_modules/@radix-ui/react-direction/dist/index.mjs
76055
76060
  var __defProp$27 = Object.defineProperty;
@@ -78670,9 +78675,9 @@ img.ProseMirror-separator {
78670
78675
  scope: __scopeDialog,
78671
78676
  triggerRef,
78672
78677
  contentRef,
78673
- contentId: useId(),
78674
- titleId: useId(),
78675
- descriptionId: useId(),
78678
+ contentId: useId$1(),
78679
+ titleId: useId$1(),
78680
+ descriptionId: useId$1(),
78676
78681
  titlePresent: titleCount > 0,
78677
78682
  descriptionPresent: descriptionCount > 0,
78678
78683
  setTitleCount,
@@ -80944,7 +80949,7 @@ img.ProseMirror-separator {
80944
80949
  var ITEM_NAME$3 = "RovingFocusGroupItem";
80945
80950
  var RovingFocusGroupItem = /* @__PURE__ */ react.forwardRef(/* @__PURE__ */ __name$13(function RovingFocusGroupItem2(props, forwardedRef) {
80946
80951
  const { __scopeRovingFocusGroup, focusable = true, active = false, tabStopId, children, ...itemProps } = props;
80947
- const autoId = useId();
80952
+ const autoId = useId$1();
80948
80953
  const id = tabStopId || autoId;
80949
80954
  const context = useRovingFocusContext(ITEM_NAME$3, __scopeRovingFocusGroup);
80950
80955
  const isCurrentTabStop = context.currentTabStopId === id;
@@ -81503,8 +81508,8 @@ img.ProseMirror-separator {
81503
81508
  onContentChange: setContent,
81504
81509
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MenuSubProvider, {
81505
81510
  scope: __scopeMenu,
81506
- contentId: useId(),
81507
- triggerId: useId(),
81511
+ contentId: useId$1(),
81512
+ triggerId: useId$1(),
81508
81513
  trigger,
81509
81514
  onTriggerChange: setTrigger,
81510
81515
  children
@@ -81962,9 +81967,9 @@ img.ProseMirror-separator {
81962
81967
  });
81963
81968
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DropdownMenuProvider, {
81964
81969
  scope: __scopeDropdownMenu,
81965
- triggerId: useId(),
81970
+ triggerId: useId$1(),
81966
81971
  triggerRef,
81967
- contentId: useId(),
81972
+ contentId: useId$1(),
81968
81973
  open,
81969
81974
  onOpenChange: setOpen,
81970
81975
  onOpenToggle: react.useCallback(() => setOpen((prevOpen) => !prevOpen), [setOpen]),
@@ -82196,7 +82201,7 @@ img.ProseMirror-separator {
82196
82201
  ...popperScope,
82197
82202
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PopoverProvider, {
82198
82203
  scope: __scopePopover,
82199
- contentId: useId(),
82204
+ contentId: useId$1(),
82200
82205
  triggerRef,
82201
82206
  open,
82202
82207
  onOpenChange: setOpen,
@@ -82428,7 +82433,7 @@ img.ProseMirror-separator {
82428
82433
  ]);
82429
82434
  const isFormControl = trigger ? !!form || !!trigger.closest("form") : true;
82430
82435
  const [nativeOptionsSet, setNativeOptionsSet] = react.useState(/* @__PURE__ */ new Set());
82431
- const contentId = useId();
82436
+ const contentId = useId$1();
82432
82437
  const nativeSelectKey = Array.from(nativeOptionsSet).map((option) => option.props.value).join(";");
82433
82438
  const handleNativeOptionAdd = react.useCallback((option) => {
82434
82439
  setNativeOptionsSet((prev) => new Set(prev).add(option));
@@ -83997,7 +84002,7 @@ img.ProseMirror-separator {
83997
84002
  const popperScope = usePopperScope(__scopeTooltip);
83998
84003
  const [trigger, setTrigger] = react.useState(null);
83999
84004
  const [contentIdState, setContentId] = react.useState(void 0);
84000
- const generatedContentId = useId();
84005
+ const generatedContentId = useId$1();
84001
84006
  const openTimerRef = react.useRef(0);
84002
84007
  const disableHoverableContent = disableHoverableContentProp ?? providerContext.disableHoverableContent;
84003
84008
  const delayDuration = delayDurationProp ?? providerContext.delayDuration;
@@ -108118,7 +108123,7 @@ img.ProseMirror-separator {
108118
108123
  }
108119
108124
  const MAX_SIZE = 256;
108120
108125
  const alphaCache = new LruCache(100);
108121
- const pending$1 = /* @__PURE__ */ new Set();
108126
+ const pending$2 = /* @__PURE__ */ new Set();
108122
108127
  let offscreenCanvas = null;
108123
108128
  function getOffscreenCanvas(w, h) {
108124
108129
  if (!offscreenCanvas) offscreenCanvas = new OffscreenCanvas(w, h);
@@ -108138,12 +108143,12 @@ img.ProseMirror-separator {
108138
108143
  function preloadAlphaData(url, cacheKey) {
108139
108144
  if (typeof OffscreenCanvas === "undefined") return;
108140
108145
  const key = cacheKey ?? url;
108141
- if (alphaCache.has(key) || pending$1.has(key)) return;
108142
- pending$1.add(key);
108146
+ if (alphaCache.has(key) || pending$2.has(key)) return;
108147
+ pending$2.add(key);
108143
108148
  const img = Image$1();
108144
108149
  img.crossOrigin = "anonymous";
108145
108150
  img.onload = async () => {
108146
- pending$1.delete(key);
108151
+ pending$2.delete(key);
108147
108152
  const { width: origW, height: origH } = img;
108148
108153
  if (origW === 0 || origH === 0) return;
108149
108154
  const scale = Math.min(1, MAX_SIZE / Math.max(origW, origH));
@@ -108170,7 +108175,7 @@ img.ProseMirror-separator {
108170
108175
  });
108171
108176
  };
108172
108177
  img.onerror = () => {
108173
- pending$1.delete(key);
108178
+ pending$2.delete(key);
108174
108179
  };
108175
108180
  img.src = url;
108176
108181
  }
@@ -115622,43 +115627,6 @@ img.ProseMirror-separator {
115622
115627
  });
115623
115628
  }
115624
115629
  //#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
115630
  //#region lib/types/client/tl/canvas-sync.js
115663
115631
  /** Debounce for ordinary document/session changes. */
115664
115632
  const STATE_DEBOUNCE_MS = 600;
@@ -115708,21 +115676,18 @@ img.ProseMirror-separator {
115708
115676
  * the selection identity list, so a selected node reads exactly like its
115709
115677
  * canvas node twin (same name, attachment id, dimensions).
115710
115678
  */
115711
- function describeShape(editor, shape, attachmentByGallery) {
115679
+ function describeShape(editor, shape) {
115712
115680
  const node = { kind: kindOf(shape.type) };
115713
115681
  if (shape.type === "image") {
115714
115682
  const meta = shape.meta;
115715
115683
  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
- }
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;
115721
115686
  if (typeof meta?.prompt === "string" && meta.prompt.trim().length > 0) node.prompt = meta.prompt.replace(/\s+/g, " ").trim().slice(0, 120);
115722
115687
  if (typeof meta?.provider === "string" && meta.provider.length > 0) node.provider = meta.provider;
115723
115688
  if (typeof meta?.model === "string" && meta.model.length > 0) node.model = meta.model;
115724
115689
  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;
115690
+ const assetName = typeof meta?.name === "string" && meta.name.length > 0 ? meta.name : asset !== void 0 && asset.type === "image" ? asset.props.name : void 0;
115726
115691
  if (typeof assetName === "string" && assetName.length > 0) node.name = assetName.slice(0, 128);
115727
115692
  if (asset !== void 0 && asset.type === "image") {
115728
115693
  node.width = Math.round(asset.props.w);
@@ -115745,31 +115710,19 @@ img.ProseMirror-separator {
115745
115710
  return node;
115746
115711
  }
115747
115712
  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
115713
  const shapes = editor.getCurrentPageShapes();
115755
115714
  const nodes = [];
115756
- 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));
115757
115716
  const selected = editor.getSelectedShapes();
115758
115717
  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));
115718
+ const selectionItems = selected.slice(0, 16).map((shape) => describeShape(editor, shape));
115760
115719
  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
115720
  return {
115767
115721
  nodeCount: shapes.length,
115768
115722
  nodes,
115769
115723
  selectionCount: selected.length,
115770
115724
  selectionKinds,
115771
115725
  selectionItems,
115772
- selectionAllConversationImages,
115773
115726
  stateKey: JSON.stringify([
115774
115727
  shapes.length,
115775
115728
  nodes,
@@ -115836,7 +115789,7 @@ img.ProseMirror-separator {
115836
115789
  return;
115837
115790
  }
115838
115791
  let selectionImage;
115839
- if (withScreenshot && summary.selectionCount > 0 && !summary.selectionAllConversationImages) try {
115792
+ if (withScreenshot && summary.selectionCount > 0) try {
115840
115793
  const selectedBefore = editor.getSelectedShapes();
115841
115794
  const { url } = await editor.toImageDataUrl(selectedBefore, {
115842
115795
  background: true,
@@ -115844,6 +115797,7 @@ img.ProseMirror-separator {
115844
115797
  scale: 1,
115845
115798
  pixelRatio: 2
115846
115799
  });
115800
+ if (disposed) return;
115847
115801
  const selectedAfter = editor.getSelectedShapes();
115848
115802
  const idsBefore = new Set(selectedBefore.map((shape) => String(shape.id)));
115849
115803
  if (!(selectedBefore.length === selectedAfter.length && selectedAfter.every((shape) => idsBefore.has(String(shape.id))))) return;
@@ -115886,6 +115840,10 @@ img.ProseMirror-separator {
115886
115840
  stateTimer = void 0;
115887
115841
  flush(false);
115888
115842
  }, STATE_DEBOUNCE_MS);
115843
+ if (editor.getSelectedShapeIds().length > 0) exportTimer = setTimeout(() => {
115844
+ exportTimer = void 0;
115845
+ flush(true);
115846
+ }, SELECTION_EXPORT_DELAY_MS);
115889
115847
  return () => {
115890
115848
  if (disposed) return;
115891
115849
  disposed = true;
@@ -115908,25 +115866,81 @@ img.ProseMirror-separator {
115908
115866
  };
115909
115867
  }
115910
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
115911
115925
  //#region lib/types/client/tl/studio-tl-canvas.js
115912
115926
  /**
115913
115927
  * Infinite canvas surface for the Studio workbench, backed by tldraw.
115914
115928
  *
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.
115929
+ * Both surfaces share one locally persisted document. The landing queue is
115930
+ * consumed once; tldraw owns subsequent edits, deletion, undo and persistence.
115926
115931
  */
115927
115932
  const MAX_DISPLAY_SIDE = 380;
115928
115933
  const GRID_GAP = 40;
115929
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
+ }
115930
115944
  /**
115931
115945
  * Dock the stock toolbar vertically along the left edge (Figma-style) instead
115932
115946
  * of tldraw's signature bottom-center pill. It is still the same
@@ -115948,11 +115962,11 @@ img.ProseMirror-separator {
115948
115962
  h: Math.round(height * scale)
115949
115963
  };
115950
115964
  }
115951
- /** Gallery ids already on one page (image shapes' `meta.galleryId`). */
115965
+ /** Dedupe across the whole document, including images moved to other pages. */
115952
115966
  function landedIdsOf(editor) {
115953
115967
  const landedIds = /* @__PURE__ */ new Set();
115954
- for (const shape of editor.getCurrentPageShapes()) {
115955
- if (shape.type !== "image") continue;
115968
+ for (const shape of editor.store.allRecords()) {
115969
+ if (shape.typeName !== "shape" || shape.type !== "image") continue;
115956
115970
  const galleryId = shape.meta?.galleryId;
115957
115971
  if (typeof galleryId === "string") landedIds.add(galleryId);
115958
115972
  }
@@ -115964,13 +115978,14 @@ img.ProseMirror-separator {
115964
115978
  const collapsed = prompt.replace(/\s+/g, " ").trim();
115965
115979
  return collapsed.length === 0 ? void 0 : collapsed.slice(0, 120);
115966
115980
  }
115967
- async function landTlItems(editor, items, failedIds) {
115981
+ async function landTlItems(editor, items, isActive) {
115968
115982
  const landedIds = landedIdsOf(editor);
115969
115983
  const pending = items.filter((item) => !landedIds.has(item.galleryId));
115970
- if (pending.length === 0) return;
115984
+ if (pending.length === 0) return true;
115971
115985
  const assets = [];
115972
115986
  const landed = [];
115973
115987
  for (const item of pending) {
115988
+ if (!isActive()) return false;
115974
115989
  const assetId = tlAssetIdFor(item.galleryId);
115975
115990
  if (editor.getAsset(assetId) === void 0) try {
115976
115991
  const blob = await fetchAttachmentBlob(item.attachment);
@@ -115991,7 +116006,6 @@ img.ProseMirror-separator {
115991
116006
  }
115992
116007
  });
115993
116008
  } catch (error) {
115994
- failedIds.add(item.galleryId);
115995
116009
  console.warn("[dsh-image-gen] canvas landing skipped (attachment unreadable):", item.galleryId, error);
115996
116010
  continue;
115997
116011
  }
@@ -116000,6 +116014,9 @@ img.ProseMirror-separator {
116000
116014
  landed.push({
116001
116015
  galleryId: item.galleryId,
116002
116016
  assetId,
116017
+ attachmentId: String(item.attachment.attachmentId),
116018
+ name: item.attachment.name ?? item.galleryId,
116019
+ fromConversation: item.fromConversation === true,
116003
116020
  w: size.w,
116004
116021
  h: size.h,
116005
116022
  ...prompt !== void 0 ? { prompt } : {},
@@ -116007,10 +116024,13 @@ img.ProseMirror-separator {
116007
116024
  ...typeof item.model === "string" && item.model.length > 0 ? { model: item.model } : {}
116008
116025
  });
116009
116026
  }
116010
- 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;
116011
116031
  if (assets.length > 0) editor.createAssets(assets);
116012
116032
  const rows = [];
116013
- 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));
116014
116034
  const rowSizes = rows.map((row) => ({
116015
116035
  width: row.reduce((sum, cell) => sum + cell.w, 0) + GRID_GAP * (row.length - 1),
116016
116036
  height: Math.max(...row.map((cell) => cell.h))
@@ -116045,7 +116065,7 @@ img.ProseMirror-separator {
116045
116065
  let cursorX = originX + (blockWidth - rowSize.width) / 2;
116046
116066
  for (const cell of row) {
116047
116067
  shapes.push({
116048
- id: createShapeId(),
116068
+ id: createShapeId(`ig-${cell.galleryId}`),
116049
116069
  type: "image",
116050
116070
  x: Math.round(cursorX),
116051
116071
  y: Math.round(cursorY),
@@ -116056,6 +116076,9 @@ img.ProseMirror-separator {
116056
116076
  },
116057
116077
  meta: {
116058
116078
  galleryId: cell.galleryId,
116079
+ attachmentId: cell.attachmentId,
116080
+ name: cell.name,
116081
+ fromConversation: cell.fromConversation,
116059
116082
  ...cell.prompt !== void 0 ? { prompt: cell.prompt } : {},
116060
116083
  ...cell.provider !== void 0 ? { provider: cell.provider } : {},
116061
116084
  ...cell.model !== void 0 ? { model: cell.model } : {}
@@ -116081,42 +116104,53 @@ img.ProseMirror-separator {
116081
116104
  inset: GRID_GAP
116082
116105
  });
116083
116106
  }
116107
+ return true;
116084
116108
  }
116085
116109
  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]);
116110
+ const sessionId = (0, react.useId)();
116106
116111
  return (0, react_jsx_runtime.jsx)("div", {
116107
116112
  className: "dsh-ig-tl-canvas",
116108
116113
  children: (0, react_jsx_runtime.jsx)(Tldraw, {
116114
+ persistenceKey: "dsh-image-gen-workbench-v1",
116115
+ sessionId,
116109
116116
  components: { Toolbar: StudioToolbar },
116110
116117
  onMount: (editor) => {
116111
- editorRef.current = editor;
116118
+ let active = true;
116112
116119
  applyBrandTheme(editor);
116113
116120
  editor.updateInstanceState({ isGridMode: true });
116114
116121
  console.info(`[dsh-image-gen] tldraw mounted (instance ${editor.id})`);
116115
- processQueue();
116116
- 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);
116117
116149
  return () => {
116150
+ active = false;
116151
+ mountedCanvases.delete(clear);
116152
+ stopLanding();
116118
116153
  stopSync();
116119
- editorRef.current = null;
116120
116154
  };
116121
116155
  }
116122
116156
  })
@@ -116227,7 +116261,10 @@ img.ProseMirror-separator {
116227
116261
  expandGenerate: "展开生成面板",
116228
116262
  generatePanel: "生成面板",
116229
116263
  findInspiration: "找灵感",
116230
- infiniteCanvasHint: "无限画布与侧边栏会话联动,才是完整功能。打开 DSH 右侧边栏的「图像工作台」即可解锁。"
116264
+ infiniteCanvasHint: "无限画布与侧边栏会话联动,才是完整功能。打开 DSH 右侧边栏的「图像工作台」即可解锁。",
116265
+ clearCanvas: "清空画布",
116266
+ clearCanvasDescription: "将永久清空所有画布页面的内容、图片资产和撤销历史。画廊已保存的卡片及原图不受影响,此操作无法撤销。",
116267
+ canvasCleared: "画布已清空,画廊不受影响"
116231
116268
  },
116232
116269
  en: {
116233
116270
  title: "Cloud Image Studio",
@@ -116329,7 +116366,10 @@ img.ProseMirror-separator {
116329
116366
  expandGenerate: "Expand generate panel",
116330
116367
  generatePanel: "Generate",
116331
116368
  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."
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."
116333
116373
  }
116334
116374
  };
116335
116375
  /** localStorage flag remembering that the one-time infinite-canvas hint was dismissed. */
@@ -116377,6 +116417,15 @@ img.ProseMirror-separator {
116377
116417
  const dragStartRef = (0, react.useRef)(null);
116378
116418
  const hasDraggedRef = (0, react.useRef)(false);
116379
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]);
116380
116429
  /** Generate-panel state from before the infinite canvas was opened, so the
116381
116430
  * panel is restored (not just forced open) when returning to preview. */
116382
116431
  const generatePanelBeforeInfiniteRef = (0, react.useRef)(false);
@@ -117214,6 +117263,12 @@ img.ProseMirror-separator {
117214
117263
  (0, react_jsx_runtime.jsxs)("div", {
117215
117264
  className: "dsh-ig-canvas-toolbar-right",
117216
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
+ }),
117217
117272
  canvasSurface === "preview" && selected !== null && (0, react_jsx_runtime.jsxs)("button", {
117218
117273
  type: "button",
117219
117274
  onClick: startNew,
@@ -117820,6 +117875,57 @@ img.ProseMirror-separator {
117820
117875
  })
117821
117876
  ]
117822
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
+ }),
117823
117929
  showDeleteModal && selected !== null && (0, react_jsx_runtime.jsx)("div", {
117824
117930
  className: "dsh-ig-workbench-modal-backdrop",
117825
117931
  onClick: () => setShowDeleteModal(false),
@@ -120709,6 +120815,266 @@ img.ProseMirror-separator {
120709
120815
  if (fromBlockContent?.type === "image") return fromBlockContent.attachment;
120710
120816
  }
120711
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
120712
121078
  //#region lib/types/client/studio-style.js
120713
121079
  const STUDIO_STYLE = `
120714
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}
@@ -121050,224 +121416,6 @@ img.ProseMirror-separator {
121050
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)}
121051
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}}
121052
121418
  `;
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
121419
  //#endregion
121272
121420
  //#region lib/types/client/conversation-image-revisions.js
121273
121421
  const STORAGE_PREFIX = "dsh_image_gen_conversation_revisions:";
@@ -121752,7 +121900,7 @@ img.ProseMirror-separator {
121752
121900
  `;
121753
121901
  //#endregion
121754
121902
  //#region lib/types/client/index.js
121755
- console.info(`[dsh-image-gen] canvas bundle 2026-09-14T00:33:35.649Z`);
121903
+ console.info(`[dsh-image-gen] canvas bundle 2026-09-14T07:49:16.664Z`);
121756
121904
  /** Right-sidebar tab-type registration id (DSH 0.1.5 `sidebarRightTabs`). */
121757
121905
  const SIDEBAR_STUDIO_TAB_ID = "dsh-image-gen/studio";
121758
121906
  /** The tab kind: a page type opened by kind, recognizing no resource address. */
@@ -122319,6 +122467,11 @@ img.ProseMirror-separator {
122319
122467
  const isCompactTranscript = () => chatScope.getSnapshot().value?.transcriptView !== "normal";
122320
122468
  const locale = ctx.get("locale");
122321
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
+ });
122322
122475
  ctx.effect(() => {
122323
122476
  const style = document.createElement("style");
122324
122477
  style.dataset.plugin = "dsh-image-gen";
@@ -124037,14 +124190,6 @@ img.ProseMirror-separator {
124037
124190
  };
124038
124191
  (0, react.useEffect)(() => {
124039
124192
  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
124193
  saveGalleryItem({
124049
124194
  id: result.attachment.attachmentId,
124050
124195
  attachment: result.attachment,