dsh-comfyui 0.5.3 → 0.5.5

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/client/client.js CHANGED
@@ -52,6 +52,11 @@ window.__ModuleLoader__.load({
52
52
  cardWorkflow: "工作流",
53
53
  cardMode: "模式",
54
54
  cardListed: "已列出 {runs} 个运行工作流,{graphs} 个图工作流",
55
+ cardHandWritten: "手写工作流",
56
+ cardWaiting: "等待 ComfyUI…",
57
+ cardQueued: "排队中(前面还有 {n} 个)",
58
+ cardStep: "步数 {value}/{max}",
59
+ cardInReply: "结果图见对话回复,文件:",
55
60
  panelTitle: "ComfyUI 面板",
56
61
  panelReset: "复位位置与大小",
57
62
  close: "关闭",
@@ -317,6 +322,11 @@ window.__ModuleLoader__.load({
317
322
  cardWorkflow: "Workflow",
318
323
  cardMode: "Mode",
319
324
  cardListed: "Listed {runs} runnable workflows, {graphs} graph workflows",
325
+ cardHandWritten: "Hand-written workflow",
326
+ cardWaiting: "Waiting for ComfyUI…",
327
+ cardQueued: "Queued ({n} ahead)",
328
+ cardStep: "Step {value}/{max}",
329
+ cardInReply: "Results are shown in the reply. Files:",
320
330
  panelTitle: "ComfyUI Panel",
321
331
  panelReset: "Reset position & size",
322
332
  close: "Close",
@@ -572,90 +582,15 @@ window.__ModuleLoader__.load({
572
582
  };
573
583
  }
574
584
  //#endregion
575
- //#region src/client/lightbox.tsx
576
- /**
577
- * Shared lightbox: opens a generated image full-screen with prev/next
578
- * navigation, keyboard support, and a download pill. Used by both the panel
579
- * (assets/queue previews) and the tool card (result images).
580
- */
581
- /** Full-screen media overlay with prev/next navigation. */
582
- function Lightbox({ t, images, kinds, index, onClose, onIndex }) {
583
- const count = images.length;
584
- (0, react.useEffect)(() => {
585
- const onKey = (event) => {
586
- if (event.key === "Escape") onClose();
587
- else if (event.key === "ArrowLeft") onIndex((index - 1 + count) % count);
588
- else if (event.key === "ArrowRight") onIndex((index + 1) % count);
589
- };
590
- document.addEventListener("keydown", onKey);
591
- return () => document.removeEventListener("keydown", onKey);
592
- }, [
593
- index,
594
- count,
595
- onClose,
596
- onIndex
597
- ]);
598
- const src = images[index];
599
- if (src === void 0) return null;
600
- const kind = kinds?.[index] ?? "image";
601
- const media = kind === "video" ? (0, react.createElement)("video", {
602
- className: "dsc-lightbox-media",
603
- src,
604
- controls: true,
605
- autoPlay: true
606
- }) : kind === "audio" ? (0, react.createElement)("audio", {
607
- className: "dsc-lightbox-media",
608
- src,
609
- controls: true,
610
- autoPlay: true
611
- }) : kind === "image" ? (0, react.createElement)("img", {
612
- className: "dsc-lightbox-img",
613
- src,
614
- alt: ""
615
- }) : (0, react.createElement)("div", { className: "dsc-lightbox-media" }, src);
616
- return (0, react.createElement)("div", {
617
- className: "dsc-lightbox",
618
- onClick: onClose
619
- }, (0, react.createElement)("div", {
620
- className: "dsc-lightbox-body",
621
- onClick: (event) => event.stopPropagation()
622
- }, (0, react.createElement)("button", {
623
- className: "dsc-lightbox-close",
624
- "aria-label": t("close"),
625
- onClick: (event) => {
626
- event.stopPropagation();
627
- onClose();
628
- }
629
- }, "✕"), count > 1 ? (0, react.createElement)("button", {
630
- className: "dsc-lightbox-nav dsc-lightbox-nav--prev",
631
- "aria-label": t("lbPrev"),
632
- onClick: (event) => {
633
- event.stopPropagation();
634
- onIndex((index - 1 + count) % count);
635
- }
636
- }, "‹") : null, media, (0, react.createElement)("div", { className: "dsc-lightbox-meta" }, (0, react.createElement)("span", null, `${index + 1} / ${count}`), (0, react.createElement)("a", {
637
- className: "dsc-lightbox-download",
638
- href: src,
639
- download: "",
640
- target: "_blank",
641
- rel: "noreferrer"
642
- }, t("cardDownload"))), count > 1 ? (0, react.createElement)("button", {
643
- className: "dsc-lightbox-nav dsc-lightbox-nav--next",
644
- "aria-label": t("lbNext"),
645
- onClick: (event) => {
646
- event.stopPropagation();
647
- onIndex((index + 1) % count);
648
- }
649
- }, "›") : null));
650
- }
651
- //#endregion
652
585
  //#region src/client/card.tsx
653
586
  /**
654
587
  * Tool cards for comfyui_run and comfyui_workflow (tool.call.toolview, keys
655
- * 'comfyui_run' / 'comfyui_workflow'). A pure function of the frozen tool-call
656
- * block: running calls show a generating state, settled calls render the
657
- * presentationMeta payload (media wall with size labels, click-to-zoom
658
- * lightbox, status) that the host threaded into the session log.
588
+ * 'comfyui_run' / 'comfyui_workflow'). Running calls show the workflow name
589
+ * and live progress (polled from /comfyui/queue); settled calls show status
590
+ * and the output files as links. The media itself is NOT rendered here: the
591
+ * model pastes it into its reply (see CLAUDE.md contract 18) — dsh 0.2
592
+ * collapses finished tool rows and PTC mode shows no card at all, so an image
593
+ * wall here was either invisible or a duplicate of the reply.
659
594
  */
660
595
  /** Discriminate the settled result node from a running call. */
661
596
  function isResultNode(block) {
@@ -669,73 +604,96 @@ window.__ModuleLoader__.load({
669
604
  return {};
670
605
  }
671
606
  }
672
- function argsSummary(args) {
673
- const parts = [];
674
- if (typeof args.template === "string") parts.push(`template=${args.template}`);
675
- if (args.mode !== void 0 && args.mode !== "sync") parts.push(`mode=${String(args.mode)}`);
676
- if (parts.length === 0) parts.push("custom workflow");
677
- return parts.join(" · ");
678
- }
679
- function aspectLabel(width, height) {
680
- const gcd = (a, b) => b === 0 ? a : gcd(b, a % b);
681
- const g = gcd(width, height);
682
- const rw = width / g;
683
- const rh = height / g;
684
- return rw <= 32 && rh <= 32 ? `${rw}:${rh}` : "";
685
- }
686
- function MediaItem({ item, t, onOpen }) {
687
- const [size, setSize] = (0, react.useState)(null);
688
- const [failed, setFailed] = (0, react.useState)(false);
689
- const media = item.kind === "video" ? (0, react.createElement)("video", {
690
- src: item.url,
691
- controls: true,
692
- preload: "metadata"
693
- }) : item.kind === "audio" ? (0, react.createElement)("audio", {
694
- src: item.url,
695
- controls: true,
696
- preload: "metadata"
697
- }) : item.kind === "image" ? failed ? (0, react.createElement)("span", { className: "dsc-media-other" }, `${item.filename}(${t("cardLoadFailed")})`) : (0, react.createElement)("img", {
698
- src: item.url,
699
- alt: item.filename,
700
- loading: "lazy",
701
- className: "dsc-media-img dsc-media-img--clickable",
702
- onClick: onOpen,
703
- onLoad: (event) => {
704
- const width = event.target.naturalWidth;
705
- const height = event.target.naturalHeight;
706
- if (typeof width === "number" && typeof height === "number" && width > 0 && height > 0) setSize({
707
- width,
708
- height
709
- });
710
- },
711
- onError: () => setFailed(true)
712
- }) : (0, react.createElement)("span", { className: "dsc-media-other" }, item.filename);
713
- const ratio = size === null ? null : aspectLabel(size.width, size.height);
714
- return (0, react.createElement)("div", { className: "dsc-media" }, media, (0, react.createElement)("div", { className: "dsc-media-meta" }, size !== null ? (0, react.createElement)("span", { className: "dsc-media-size" }, `${size.width}×${size.height}${ratio !== "" ? ` · ${ratio}` : ""}`) : null, (0, react.createElement)("a", {
607
+ /** The output files as links (the images themselves are in the model's reply). */
608
+ function MediaLinks({ media, t }) {
609
+ if (media.length === 0) return (0, react.createElement)("div", { className: "dsc-meta" }, t("cardEmpty"));
610
+ return (0, react.createElement)("div", { className: "dsc-files" }, (0, react.createElement)("span", { className: "dsc-meta" }, t("cardInReply")), media.map((item) => (0, react.createElement)("a", {
611
+ key: `${item.node}-${item.index}-${item.filename}`,
612
+ className: "dsc-file",
715
613
  href: item.url,
716
- download: item.filename,
717
614
  target: "_blank",
718
- rel: "noreferrer"
719
- }, t("cardDownload"))));
615
+ rel: "noreferrer",
616
+ title: item.filename
617
+ }, item.filename)));
618
+ }
619
+ /**
620
+ * Poll /comfyui/queue (every 2 s while `active`) for one prompt's queue
621
+ * position and sampler progress. A running sync call does not know its
622
+ * prompt id yet, so without one this follows the first prompt of ours that
623
+ * is running (else pending) — sync calls block, so that is the call's own.
624
+ */
625
+ function useQueueState(promptId, active) {
626
+ const [state, setState] = (0, react.useState)(null);
627
+ (0, react.useEffect)(() => {
628
+ if (!active) return void 0;
629
+ let stopped = false;
630
+ let timer;
631
+ const poll = async () => {
632
+ try {
633
+ const data = await (await fetch("/comfyui/queue", { headers: { accept: "application/json" } })).json();
634
+ if (stopped) return;
635
+ const running = data.running ?? [];
636
+ const pending = data.pending ?? [];
637
+ const match = (entry) => promptId !== void 0 ? entry.promptId === promptId : entry.ours;
638
+ const run = running.find(match);
639
+ const waitIndex = pending.findIndex(match);
640
+ if (run !== void 0) setState({
641
+ phase: "running",
642
+ value: run.progress?.value ?? 0,
643
+ max: run.progress?.max ?? 0
644
+ });
645
+ else if (waitIndex >= 0) setState({
646
+ phase: "pending",
647
+ ahead: running.length + waitIndex
648
+ });
649
+ else setState(null);
650
+ } catch {}
651
+ if (!stopped) timer = window.setTimeout(() => {
652
+ poll();
653
+ }, 2e3);
654
+ };
655
+ poll();
656
+ return () => {
657
+ stopped = true;
658
+ if (timer !== void 0) window.clearTimeout(timer);
659
+ };
660
+ }, [promptId, active]);
661
+ return state;
662
+ }
663
+ function ProgressLine({ state, t }) {
664
+ if (state === null) return (0, react.createElement)("div", { className: "dsc-meta" }, t("cardWaiting"));
665
+ if (state.phase === "pending") return (0, react.createElement)("div", { className: "dsc-meta" }, t("cardQueued", { n: state.ahead }));
666
+ const percent = state.max > 0 ? Math.min(100, Math.round(state.value / state.max * 100)) : 0;
667
+ return (0, react.createElement)("div", { className: "dsc-progress" }, (0, react.createElement)("div", { className: "dsc-progress-track" }, (0, react.createElement)("div", {
668
+ className: "dsc-progress-fill",
669
+ style: { width: `${percent}%` }
670
+ })), (0, react.createElement)("span", { className: "dsc-meta" }, state.max > 0 ? t("cardStep", {
671
+ value: state.value,
672
+ max: state.max
673
+ }) : t("cardWaiting")));
674
+ }
675
+ /** A saved workflow's name by id, for the running card of comfyui_workflow run. */
676
+ function useWorkflowName(id) {
677
+ const [name, setName] = (0, react.useState)(void 0);
678
+ (0, react.useEffect)(() => {
679
+ if (id === void 0) return void 0;
680
+ let stopped = false;
681
+ fetch("/comfyui/workflows", { headers: { accept: "application/json" } }).then(async (response) => await response.json()).then((data) => {
682
+ const found = data.workflows?.find((workflow) => workflow.id === id);
683
+ if (!stopped && found !== void 0) setName(found.name);
684
+ }).catch(() => void 0);
685
+ return () => {
686
+ stopped = true;
687
+ };
688
+ }, [id]);
689
+ return name;
720
690
  }
721
691
  function ResultCard({ title, result, t }) {
722
692
  const failed = result.status === "interrupted";
723
- const [lightbox, setLightbox] = (0, react.useState)(null);
724
- const urls = result.media.map((item) => item.url);
725
- const kinds = result.media.map((item) => item.kind);
726
- return (0, react.createElement)("div", { className: "dsc-card" }, (0, react.createElement)("div", { className: "dsc-card-head" }, title !== "" ? (0, react.createElement)("span", { className: "dsc-badge" }, title) : null, (0, react.createElement)("span", { className: failed ? "dsc-badge dsc-badge--err" : "dsc-badge dsc-badge--ok" }, failed ? t("cardInterrupted") : result.status), (0, react.createElement)("span", { className: "dsc-meta" }, `${t("cardPrompt")} ${result.promptId}`), (0, react.createElement)("span", { className: "dsc-meta" }, `${t("cardElapsed")} ${result.elapsedMs} ms`)), (0, react.createElement)("div", { className: "dsc-meta" }, result.summary), result.media.length > 0 ? (0, react.createElement)("div", { className: "dsc-grid" }, result.media.map((item, index) => (0, react.createElement)(MediaItem, {
727
- key: `${item.node}-${item.index}`,
728
- item,
729
- t,
730
- onOpen: () => setLightbox(index)
731
- }))) : (0, react.createElement)("div", { className: "dsc-meta" }, t("cardEmpty")), lightbox !== null ? (0, react.createElement)(Lightbox, {
732
- t,
733
- images: urls,
734
- kinds,
735
- index: lightbox,
736
- onClose: () => setLightbox(null),
737
- onIndex: setLightbox
738
- }) : null);
693
+ return (0, react.createElement)("div", { className: "dsc-card" }, (0, react.createElement)("div", { className: "dsc-card-head" }, title !== "" ? (0, react.createElement)("span", { className: "dsc-badge" }, title) : null, (0, react.createElement)("span", { className: failed ? "dsc-badge dsc-badge--err" : "dsc-badge dsc-badge--ok" }, failed ? t("cardInterrupted") : result.status), (0, react.createElement)("span", { className: "dsc-meta" }, `${t("cardPrompt")} ${result.promptId}`), (0, react.createElement)("span", { className: "dsc-meta" }, `${t("cardElapsed")} ${result.elapsedMs} ms`)), (0, react.createElement)("div", { className: "dsc-meta" }, result.summary), (0, react.createElement)(MediaLinks, {
694
+ media: result.media,
695
+ t
696
+ }));
739
697
  }
740
698
  /** Transient-failure retry budget for the jobs/media poll (backoff steps). */
741
699
  const POLL_MAX_FAILURES = 20;
@@ -745,7 +703,7 @@ window.__ModuleLoader__.load({
745
703
  const POLL_UNKNOWN_GRACE = 100;
746
704
  function BackgroundCard({ label, promptId, t }) {
747
705
  const [result, setResult] = (0, react.useState)(null);
748
- const [lightbox, setLightbox] = (0, react.useState)(null);
706
+ const queue = useQueueState(promptId, result === null);
749
707
  (0, react.useEffect)(() => {
750
708
  let stopped = false;
751
709
  let timer;
@@ -833,23 +791,15 @@ window.__ModuleLoader__.load({
833
791
  }, [promptId, t]);
834
792
  if (result !== null && (result.status === "completed" || result.status === "failed")) {
835
793
  const media = result.media ?? [];
836
- const urls = media.map((item) => item.url);
837
- const kinds = media.map((item) => item.kind);
838
- return (0, react.createElement)("div", { className: "dsc-card" }, (0, react.createElement)("div", { className: "dsc-card-head" }, (0, react.createElement)("span", { className: result.status === "failed" ? "dsc-badge dsc-badge--err" : "dsc-badge dsc-badge--ok" }, result.status === "failed" ? t("cardFailed") : t("cardBackgroundDone")), (0, react.createElement)("span", { className: "dsc-meta" }, `${label} · ${promptId}`)), result.status !== "failed" && result.recovered === true ? (0, react.createElement)("div", { className: "dsc-meta" }, t("cardRecovered")) : null, result.status === "failed" ? (0, react.createElement)("div", { className: "dsc-meta dsc-job-error" }, result.error ?? t("cardFailed")) : media.length > 0 ? (0, react.createElement)("div", { className: "dsc-grid" }, media.map((item, index) => (0, react.createElement)(MediaItem, {
839
- key: `${item.node}-${item.index}`,
840
- item,
841
- t,
842
- onOpen: () => setLightbox(index)
843
- }))) : (0, react.createElement)("div", { className: "dsc-meta" }, t("cardEmpty")), lightbox !== null ? (0, react.createElement)(Lightbox, {
844
- t,
845
- images: urls,
846
- kinds,
847
- index: lightbox,
848
- onClose: () => setLightbox(null),
849
- onIndex: setLightbox
850
- }) : null);
794
+ return (0, react.createElement)("div", { className: "dsc-card" }, (0, react.createElement)("div", { className: "dsc-card-head" }, (0, react.createElement)("span", { className: result.status === "failed" ? "dsc-badge dsc-badge--err" : "dsc-badge dsc-badge--ok" }, result.status === "failed" ? t("cardFailed") : t("cardBackgroundDone")), (0, react.createElement)("span", { className: "dsc-meta" }, `${label} · ${promptId}`)), result.status !== "failed" && result.recovered === true ? (0, react.createElement)("div", { className: "dsc-meta" }, t("cardRecovered")) : null, result.status === "failed" ? (0, react.createElement)("div", { className: "dsc-meta dsc-job-error" }, result.error ?? t("cardFailed")) : (0, react.createElement)(MediaLinks, {
795
+ media,
796
+ t
797
+ }));
851
798
  }
852
- return (0, react.createElement)("div", { className: "dsc-card" }, (0, react.createElement)("div", { className: "dsc-card-head" }, (0, react.createElement)("span", { className: "dsc-badge" }, t("cardBackground")), (0, react.createElement)("span", { className: "dsc-meta" }, `${label} · ${promptId}`)), (0, react.createElement)("div", { className: "dsc-meta" }, t("cardCollect")));
799
+ return (0, react.createElement)("div", { className: "dsc-card" }, (0, react.createElement)("div", { className: "dsc-card-head" }, (0, react.createElement)("span", { className: "dsc-badge" }, t("cardBackground")), (0, react.createElement)("span", { className: "dsc-meta" }, `${label} · ${promptId}`)), (0, react.createElement)(ProgressLine, {
800
+ state: queue,
801
+ t
802
+ }));
853
803
  }
854
804
  function SettledCard({ block, t }) {
855
805
  if (block.isError === true) return (0, react.createElement)("div", { className: "dsc-card" }, (0, react.createElement)("div", { className: "dsc-card-head" }, (0, react.createElement)("span", { className: "dsc-badge dsc-badge--err" }, t("cardFailed"))), (0, react.createElement)("div", { className: "dsc-meta" }, block.error?.name ?? "error"));
@@ -890,14 +840,30 @@ window.__ModuleLoader__.load({
890
840
  t
891
841
  });
892
842
  }
843
+ /** A call still running: what is generating, and how far along it is. */
844
+ function RunningCard({ block, t }) {
845
+ const args = parseArgs(block.argsRaw);
846
+ const isRun = block.name === "comfyui_run" || args.action === "run";
847
+ const workflowId = block.name === "comfyui_workflow" && typeof args.id === "string" ? args.id : void 0;
848
+ const savedName = useWorkflowName(isRun ? workflowId : void 0);
849
+ const queue = useQueueState(void 0, isRun && args.mode !== "async");
850
+ if (!isRun) return (0, react.createElement)("div", { className: "dsc-card" }, (0, react.createElement)("div", { className: "dsc-card-head" }, (0, react.createElement)("span", { className: "dsc-badge" }, block.name), typeof args.action === "string" ? (0, react.createElement)("span", { className: "dsc-meta" }, args.action) : null));
851
+ const title = block.name === "comfyui_run" ? t("cardHandWritten") : savedName ?? workflowId ?? "";
852
+ return (0, react.createElement)("div", { className: "dsc-card" }, (0, react.createElement)("div", { className: "dsc-card-head" }, (0, react.createElement)("span", { className: "dsc-badge" }, t("cardGenerating")), title !== "" ? (0, react.createElement)("span", { className: "dsc-meta" }, `${t("cardWorkflow")} ${title}`) : null, (0, react.createElement)("span", { className: "dsc-meta" }, `${t("cardMode")} ${String(args.mode ?? "sync")}`)), args.mode === "async" ? null : (0, react.createElement)(ProgressLine, {
853
+ state: queue,
854
+ t
855
+ }));
856
+ }
893
857
  /** The card component: picks running vs settled rendering from the block. */
894
858
  function ComfyUICard({ t, block }) {
895
859
  if (isResultNode(block)) return (0, react.createElement)(SettledCard, {
896
860
  block,
897
861
  t
898
862
  });
899
- const args = parseArgs(block.argsRaw);
900
- return (0, react.createElement)("div", { className: "dsc-card" }, (0, react.createElement)("div", { className: "dsc-card-head" }, (0, react.createElement)("span", { className: "dsc-badge" }, t("cardGenerating")), (0, react.createElement)("span", { className: "dsc-meta" }, `${t("cardWorkflow")} ${argsSummary(args)} · ${t("cardMode")} ${String(args.mode ?? "sync")}`)));
863
+ return (0, react.createElement)(RunningCard, {
864
+ block,
865
+ t
866
+ });
901
867
  }
902
868
  //#endregion
903
869
  //#region src/client/api.ts
@@ -1354,6 +1320,83 @@ window.__ModuleLoader__.load({
1354
1320
  }, (0, react.createElement)("span", { className: "dsc-trigger-glyph" }, (0, react.createElement)(ComfyUIIcon)), (0, react.createElement)("span", null, t("panelTitle"))), (0, react.createElement)(ConnectionToast, { t }));
1355
1321
  }
1356
1322
  //#endregion
1323
+ //#region src/client/lightbox.tsx
1324
+ /**
1325
+ * Shared lightbox: opens a generated image full-screen with prev/next
1326
+ * navigation, keyboard support, and a download pill. Used by both the panel
1327
+ * (assets/queue previews) and the tool card (result images).
1328
+ */
1329
+ /** Full-screen media overlay with prev/next navigation. */
1330
+ function Lightbox({ t, images, kinds, index, onClose, onIndex }) {
1331
+ const count = images.length;
1332
+ (0, react.useEffect)(() => {
1333
+ const onKey = (event) => {
1334
+ if (event.key === "Escape") onClose();
1335
+ else if (event.key === "ArrowLeft") onIndex((index - 1 + count) % count);
1336
+ else if (event.key === "ArrowRight") onIndex((index + 1) % count);
1337
+ };
1338
+ document.addEventListener("keydown", onKey);
1339
+ return () => document.removeEventListener("keydown", onKey);
1340
+ }, [
1341
+ index,
1342
+ count,
1343
+ onClose,
1344
+ onIndex
1345
+ ]);
1346
+ const src = images[index];
1347
+ if (src === void 0) return null;
1348
+ const kind = kinds?.[index] ?? "image";
1349
+ const media = kind === "video" ? (0, react.createElement)("video", {
1350
+ className: "dsc-lightbox-media",
1351
+ src,
1352
+ controls: true,
1353
+ autoPlay: true
1354
+ }) : kind === "audio" ? (0, react.createElement)("audio", {
1355
+ className: "dsc-lightbox-media",
1356
+ src,
1357
+ controls: true,
1358
+ autoPlay: true
1359
+ }) : kind === "image" ? (0, react.createElement)("img", {
1360
+ className: "dsc-lightbox-img",
1361
+ src,
1362
+ alt: ""
1363
+ }) : (0, react.createElement)("div", { className: "dsc-lightbox-media" }, src);
1364
+ return (0, react.createElement)("div", {
1365
+ className: "dsc-lightbox",
1366
+ onClick: onClose
1367
+ }, (0, react.createElement)("div", {
1368
+ className: "dsc-lightbox-body",
1369
+ onClick: (event) => event.stopPropagation()
1370
+ }, (0, react.createElement)("button", {
1371
+ className: "dsc-lightbox-close",
1372
+ "aria-label": t("close"),
1373
+ onClick: (event) => {
1374
+ event.stopPropagation();
1375
+ onClose();
1376
+ }
1377
+ }, "✕"), count > 1 ? (0, react.createElement)("button", {
1378
+ className: "dsc-lightbox-nav dsc-lightbox-nav--prev",
1379
+ "aria-label": t("lbPrev"),
1380
+ onClick: (event) => {
1381
+ event.stopPropagation();
1382
+ onIndex((index - 1 + count) % count);
1383
+ }
1384
+ }, "‹") : null, media, (0, react.createElement)("div", { className: "dsc-lightbox-meta" }, (0, react.createElement)("span", null, `${index + 1} / ${count}`), (0, react.createElement)("a", {
1385
+ className: "dsc-lightbox-download",
1386
+ href: src,
1387
+ download: "",
1388
+ target: "_blank",
1389
+ rel: "noreferrer"
1390
+ }, t("cardDownload"))), count > 1 ? (0, react.createElement)("button", {
1391
+ className: "dsc-lightbox-nav dsc-lightbox-nav--next",
1392
+ "aria-label": t("lbNext"),
1393
+ onClick: (event) => {
1394
+ event.stopPropagation();
1395
+ onIndex((index + 1) % count);
1396
+ }
1397
+ }, "›") : null));
1398
+ }
1399
+ //#endregion
1357
1400
  //#region src/client/panel.tsx
1358
1401
  /**
1359
1402
  * The ComfyUI panel (shell.overlay, id 'comfyui.panel'): a floating panel
@@ -4275,7 +4318,10 @@ window.__ModuleLoader__.load({
4275
4318
  if (first === void 0 || broken) return (0, react.createElement)("div", {
4276
4319
  className: "dsc-asset dsc-asset--empty",
4277
4320
  onClick: props.onClick
4278
- }, (0, react.createElement)("div", null, props.t("assetGone")), (0, react.createElement)("div", { className: "dsc-meta" }, props.asset.workflowName ?? "—"), trash);
4321
+ }, (0, react.createElement)("div", null, props.t("assetGone")), (0, react.createElement)("div", { className: "dsc-meta" }, props.asset.workflowName ?? "—"), first !== void 0 ? (0, react.createElement)("div", {
4322
+ className: "dsc-meta dsc-asset-file",
4323
+ title: first.filename
4324
+ }, first.filename) : null, trash);
4279
4325
  return (0, react.createElement)("div", {
4280
4326
  className: "dsc-asset",
4281
4327
  onClick: props.onClick
@@ -4405,7 +4451,10 @@ window.__ModuleLoader__.load({
4405
4451
  alt: first.filename,
4406
4452
  loading: "lazy",
4407
4453
  onError: () => setBroken(true)
4408
- }), (0, react.createElement)("div", { className: "dsc-asset-meta" }, props.asset.workflowName ?? "—"), trash);
4454
+ }), (0, react.createElement)("div", { className: "dsc-asset-meta" }, props.asset.workflowName ?? "—"), (0, react.createElement)("div", {
4455
+ className: "dsc-asset-meta dsc-asset-file",
4456
+ title: first.filename
4457
+ }, first.filename), trash);
4409
4458
  }
4410
4459
  /**
4411
4460
  * Tab 3: the task center — active tasks (pending / generating) shown as
@@ -4618,16 +4667,13 @@ window.__ModuleLoader__.load({
4618
4667
  }
4619
4668
  .dsc-badge--ok { background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 16%, transparent); }
4620
4669
  .dsc-badge--err { background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 16%, transparent); }
4621
- .dsc-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 8px; margin-top: 6px; }
4622
- .dsc-media { border-radius: 6px; overflow: hidden; background: var(--dsw-alias-bg-layer-1); }
4623
- .dsc-media img, .dsc-media video, .dsc-media audio { display: block; width: 100%; max-height: 320px; object-fit: contain; background: #000; }
4624
- .dsc-media audio { height: 44px; }
4625
- .dsc-media-img--clickable { cursor: zoom-in; }
4626
- .dsc-media-other { display: block; padding: 8px 6px; font-size: 12px; color: var(--dsw-alias-label-secondary); word-break: break-all; }
4627
- .dsc-media-meta { display: flex; align-items: center; justify-content: space-between; gap: 6px; padding: 3px 6px; }
4628
- .dsc-media-size { font-size: 11px; color: var(--dsw-alias-label-secondary); white-space: nowrap; }
4629
- .dsc-media-meta a { display: inline; padding: 0; font-size: 11px; color: var(--dsw-alias-label-secondary); text-decoration: none; }
4630
- .dsc-media-meta a:hover { color: var(--dsw-alias-label-primary); }
4670
+ /* Inline players inserted after video/audio links in chat replies (inline-media.ts). */
4671
+ .dsc-inline-player { display: block; margin: 6px 0; }
4672
+ .dsc-inline-player video { display: block; max-width: 100%; max-height: 360px; border-radius: 8px; background: transparent; }
4673
+ .dsc-inline-player audio { display: block; width: min(100%, 480px); height: 40px; }
4674
+ .dsc-files { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px; margin-top: 4px; }
4675
+ .dsc-file { font-size: 12px; color: var(--dsw-alias-label-secondary); text-decoration: none; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
4676
+ .dsc-file:hover { color: var(--dsw-alias-label-primary); text-decoration: underline; }
4631
4677
  .dsc-meta { color: var(--dsw-alias-label-secondary); font-size: 12px; }
4632
4678
  .dsc-err { color: var(--dsw-alias-state-error-primary); font-size: 12px; margin: 4px 0; }
4633
4679
  .dsc-ok { color: var(--dsw-alias-state-success-primary); font-size: 12px; margin: 4px 0; }
@@ -4836,6 +4882,9 @@ select.dsc-input option { background: var(--dsw-alias-bg-layer-1); color: var(--
4836
4882
  font-size: 11px; color: var(--dsw-alias-label-secondary); text-align: center;
4837
4883
  }
4838
4884
  .dsc-asset-meta { padding: 3px 6px; font-size: 11px; color: var(--dsw-alias-label-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
4885
+ /* Second meta line: the output filename, under the workflow name. */
4886
+ .dsc-asset-file { padding-top: 0; font-size: 10px; opacity: 0.8; }
4887
+ .dsc-asset--empty .dsc-asset-file { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
4839
4888
  /* confirmation modal (asset deletion) */
4840
4889
  .dsc-confirm { width: min(360px, 88vw); display: flex; flex-direction: column; gap: 8px; padding: 14px 16px; border-radius: 12px; border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-primary); box-shadow: 0 12px 40px rgba(0, 0, 0, 0.32); }
4841
4890
  .dsc-confirm-title { font-weight: 600; font-size: 14px; }
@@ -5032,6 +5081,111 @@ video.dsc-picker-player { max-height: 180px; }
5032
5081
  document.head.append(style);
5033
5082
  }
5034
5083
  //#endregion
5084
+ //#region src/client/inline-media.ts
5085
+ /**
5086
+ * Inline players for generated video/audio in chat replies.
5087
+ *
5088
+ * The model shows results by pasting Markdown into its reply (CLAUDE.md
5089
+ * contract 18). Images embed natively, but dsh's chat Markdown has no video or
5090
+ * audio syntax and renders raw HTML as literal text, so a video/audio result
5091
+ * can only be a link there. This module watches the page and, after every
5092
+ * link to this plugin's own `/comfyui/media` route whose file is a video or
5093
+ * audio file, inserts a native <video>/<audio> player.
5094
+ *
5095
+ * It only ever ADDS sibling nodes next to dsh-rendered links — it never moves
5096
+ * or removes dsh's own nodes — so React's reconciliation of the message keeps
5097
+ * working. Streaming re-renders and dsh updates simply trigger a re-scan: a
5098
+ * player is added only when the link is not already followed by its player,
5099
+ * and players whose link has gone are removed. The plugin's own UI (card file
5100
+ * links, panel) is skipped. Everything is undone when the plugin unloads.
5101
+ */
5102
+ const VIDEO_EXTENSIONS = /* @__PURE__ */ new Set([
5103
+ "mp4",
5104
+ "webm",
5105
+ "mov",
5106
+ "m4v",
5107
+ "mkv"
5108
+ ]);
5109
+ const AUDIO_EXTENSIONS = /* @__PURE__ */ new Set([
5110
+ "mp3",
5111
+ "wav",
5112
+ "flac",
5113
+ "ogg",
5114
+ "oga",
5115
+ "opus",
5116
+ "m4a",
5117
+ "aac"
5118
+ ]);
5119
+ const PLAYER_CLASS = "dsc-inline-player";
5120
+ /** The player kind for a /comfyui/media link, or undefined for anything else. */
5121
+ function mediaKind(href) {
5122
+ let url;
5123
+ try {
5124
+ url = new URL(href, window.location.href);
5125
+ } catch {
5126
+ return;
5127
+ }
5128
+ if (!url.pathname.endsWith("/comfyui/media")) return void 0;
5129
+ const file = url.searchParams.get("file") ?? "";
5130
+ const dot = file.lastIndexOf(".");
5131
+ if (dot < 0) return void 0;
5132
+ const ext = file.slice(dot + 1).toLowerCase();
5133
+ if (VIDEO_EXTENSIONS.has(ext)) return "video";
5134
+ if (AUDIO_EXTENSIONS.has(ext)) return "audio";
5135
+ }
5136
+ function isPlayerFor(node, href) {
5137
+ return node !== null && node.classList.contains(PLAYER_CLASS) && node.getAttribute("data-src") === href;
5138
+ }
5139
+ function makePlayer(kind, href) {
5140
+ const wrap = document.createElement("span");
5141
+ wrap.className = `${PLAYER_CLASS} ${PLAYER_CLASS}--${kind}`;
5142
+ wrap.setAttribute("data-src", href);
5143
+ const player = document.createElement(kind);
5144
+ player.src = href;
5145
+ player.controls = true;
5146
+ player.preload = "metadata";
5147
+ wrap.appendChild(player);
5148
+ return wrap;
5149
+ }
5150
+ function scan() {
5151
+ for (const player of Array.from(document.querySelectorAll(`.${PLAYER_CLASS}`))) {
5152
+ const prev = player.previousElementSibling;
5153
+ const src = player.getAttribute("data-src");
5154
+ if (!(prev instanceof HTMLAnchorElement) || src === null || prev.href !== src) player.remove();
5155
+ }
5156
+ for (const link of Array.from(document.querySelectorAll("a[href*=\"/comfyui/media\"]"))) {
5157
+ if (link.closest("[class^=\"dsc-\"], [class*=\" dsc-\"]") !== null) continue;
5158
+ const kind = mediaKind(link.href);
5159
+ if (kind === void 0) continue;
5160
+ if (isPlayerFor(link.nextElementSibling, link.href)) continue;
5161
+ link.after(makePlayer(kind, link.href));
5162
+ }
5163
+ }
5164
+ /** Start watching the page; returns the disposer that stops and cleans up. */
5165
+ function installInlineMedia() {
5166
+ let frame;
5167
+ const schedule = () => {
5168
+ if (frame !== void 0) return;
5169
+ frame = window.requestAnimationFrame(() => {
5170
+ frame = void 0;
5171
+ scan();
5172
+ });
5173
+ };
5174
+ const observer = new MutationObserver((mutations) => {
5175
+ if (mutations.some((mutation) => [...Array.from(mutation.addedNodes), ...Array.from(mutation.removedNodes)].some((node) => !(node instanceof HTMLElement && node.classList.contains(PLAYER_CLASS))))) schedule();
5176
+ });
5177
+ observer.observe(document.body, {
5178
+ childList: true,
5179
+ subtree: true
5180
+ });
5181
+ schedule();
5182
+ return () => {
5183
+ observer.disconnect();
5184
+ if (frame !== void 0) window.cancelAnimationFrame(frame);
5185
+ for (const player of Array.from(document.querySelectorAll(`.${PLAYER_CLASS}`))) player.remove();
5186
+ };
5187
+ }
5188
+ //#endregion
5035
5189
  //#region src/client/index.ts
5036
5190
  /**
5037
5191
  * dsh-comfyui client half: registers the comfyui_run tool card and the
@@ -5043,6 +5197,7 @@ video.dsc-picker-player { max-height: 180px; }
5043
5197
  function apply(ctx) {
5044
5198
  const t = makeT(getLang());
5045
5199
  ctx.effect(() => injectStyles(), "dsh-comfyui: styles");
5200
+ ctx.effect(() => installInlineMedia(), "dsh-comfyui: inline video/audio players");
5046
5201
  ctx.slots.inject("tool.call.toolview", () => ctx.slots.register({
5047
5202
  name: "tool.call.toolview",
5048
5203
  key: "comfyui_run"