dsh-image-gen 0.2.3 → 0.2.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/lib/client.js CHANGED
@@ -19,12 +19,61 @@ window.__ModuleLoader__.load({
19
19
  const DEFAULT_DASHSCOPE_ENDPOINT = "https://dashscope.aliyuncs.com/api/v1";
20
20
  const DEFAULT_COMFYUI_BASE_URL = "http://127.0.0.1:8188";
21
21
  const DEFAULT_COMFYUI_TIMEOUT_MS = 3e5;
22
+ const DEFAULT_COMFYUI_WORKFLOW_LABEL = "API workflow";
23
+ /** Default model names. */
24
+ const DEFAULT_GOOGLE_MODEL = "gemini-3.1-flash-image";
25
+ const DEFAULT_OPENAI_MODEL = "gpt-image-2";
26
+ const DEFAULT_SEEDREAM_MODEL = "doubao-seedream-5-0-260128";
27
+ const DEFAULT_DASHSCOPE_MODEL = "qwen-image-3.0";
28
+ /** Named workflows, falling back to the legacy single-workflow fields when the list is empty. */
29
+ function resolveComfyUIWorkflows(source) {
30
+ const named = [];
31
+ for (const entry of source.comfyuiWorkflows ?? []) {
32
+ const name = typeof entry?.name === "string" ? entry.name.trim() : "";
33
+ const json = typeof entry?.json === "string" ? entry.json : "";
34
+ if (name.length > 0 && json.trim().length > 0) {
35
+ const presetPrompt = typeof entry.presetPrompt === "string" ? entry.presetPrompt.trim() : "";
36
+ named.push(presetPrompt.length > 0 ? {
37
+ name,
38
+ json,
39
+ presetPrompt
40
+ } : {
41
+ name,
42
+ json
43
+ });
44
+ }
45
+ }
46
+ if (named.length > 0) return named;
47
+ const legacyJson = typeof source.comfyuiWorkflowJson === "string" ? source.comfyuiWorkflowJson : "";
48
+ if (legacyJson.trim().length === 0) return [];
49
+ const legacyName = typeof source.comfyuiWorkflowName === "string" ? source.comfyuiWorkflowName.trim() : "";
50
+ return [{
51
+ name: legacyName.length > 0 ? legacyName : DEFAULT_COMFYUI_WORKFLOW_LABEL,
52
+ json: legacyJson
53
+ }];
54
+ }
55
+ /** The workflow ComfyUI calls use by default: the configured active name, else the first entry. */
56
+ function activeComfyUIWorkflow(source) {
57
+ const workflows = resolveComfyUIWorkflows(source);
58
+ if (workflows.length === 0) return void 0;
59
+ const activeName = typeof source.comfyuiActiveWorkflow === "string" ? source.comfyuiActiveWorkflow.trim() : "";
60
+ return workflows.find((workflow) => workflow.name === activeName) ?? workflows[0];
61
+ }
62
+ /** Derive a workflow label that does not collide with the given existing names. */
63
+ function uniqueComfyUIWorkflowName(name, existing) {
64
+ const base = name.trim().length > 0 ? name.trim() : DEFAULT_COMFYUI_WORKFLOW_LABEL;
65
+ if (!existing.includes(base)) return base;
66
+ for (let index = 2;; index += 1) {
67
+ const candidate = `${base} (${index})`;
68
+ if (!existing.includes(candidate)) return candidate;
69
+ }
70
+ }
22
71
  const DEFAULT_MODELS = {
23
- google: "gemini-3.1-flash-image",
24
- openai: "gpt-image-2",
25
- seedream: "doubao-seedream-5-0-260128",
26
- dashscope: "qwen-image-3.0",
27
- comfyui: "API workflow"
72
+ google: DEFAULT_GOOGLE_MODEL,
73
+ openai: DEFAULT_OPENAI_MODEL,
74
+ seedream: DEFAULT_SEEDREAM_MODEL,
75
+ dashscope: DEFAULT_DASHSCOPE_MODEL,
76
+ comfyui: DEFAULT_COMFYUI_WORKFLOW_LABEL
28
77
  };
29
78
  const DEFAULT_BASE_URLS = {
30
79
  google: DEFAULT_GOOGLE_ENDPOINT,
@@ -37,9 +86,16 @@ window.__ModuleLoader__.load({
37
86
  //#region lib/types/comfyui-workflow.js
38
87
  /** Pure ComfyUI API-workflow validation and placeholder injection. */
39
88
  const COMFYUI_PROMPT_PLACEHOLDER = "{{prompt}}";
89
+ const COMFYUI_IMAGE_PLACEHOLDER = "{{image}}";
90
+ /** Legacy single-percent placeholders from early releases, still accepted. */
91
+ const LEGACY_PROMPT_PLACEHOLDER = "%prompt%";
92
+ const LEGACY_IMAGE_PLACEHOLDER = "%image%";
93
+ /** LoadImage-style input key that receives the uploaded source image name. */
94
+ const IMAGE_INPUT_KEY = "image";
40
95
  /** Validate imported JSON without exposing workflow graph details to callers. */
41
96
  function validateComfyUIWorkflowJson(workflowJson) {
42
- parseWorkflow(workflowJson);
97
+ const imageInputs = countImagePlaceholders(parseWorkflow(workflowJson));
98
+ if (imageInputs > 1) throw new Error(`ComfyUI workflow must contain at most one ${COMFYUI_IMAGE_PLACEHOLDER} image input; found ${String(imageInputs)}`);
43
99
  }
44
100
  function parseWorkflow(workflowJson) {
45
101
  if (workflowJson.trim().length === 0) throw new Error("Import a ComfyUI API workflow JSON file in Settings before generating");
@@ -52,20 +108,32 @@ window.__ModuleLoader__.load({
52
108
  }
53
109
  if (!isRecord(value) || Object.keys(value).length === 0) throw new Error("ComfyUI workflow must be a non-empty API-format JSON object");
54
110
  if (!Object.values(value).some((node) => {
55
- const inputs = record(node)?.inputs;
111
+ const inputs = record$1(node)?.inputs;
56
112
  return inputs !== void 0 && containsPromptPlaceholder(inputs);
57
- })) throw new Error(`ComfyUI workflow must contain ${COMFYUI_PROMPT_PLACEHOLDER} in a text input`);
113
+ })) throw new Error(`ComfyUI workflow must contain ${COMFYUI_PROMPT_PLACEHOLDER} (or ${LEGACY_PROMPT_PLACEHOLDER}) in a text input`);
58
114
  return value;
59
115
  }
116
+ /** Count `inputs.image` fields that are exactly an image placeholder. */
117
+ function countImagePlaceholders(workflow) {
118
+ let count = 0;
119
+ for (const node of Object.values(workflow)) {
120
+ const inputs = record$1(record$1(node)?.inputs);
121
+ if (inputs !== void 0 && isImagePlaceholder(inputs[IMAGE_INPUT_KEY])) count += 1;
122
+ }
123
+ return count;
124
+ }
125
+ function isImagePlaceholder(value) {
126
+ return value === "{{image}}" || value === LEGACY_IMAGE_PLACEHOLDER;
127
+ }
60
128
  function containsPromptPlaceholder(value) {
61
- if (typeof value === "string") return value.includes(COMFYUI_PROMPT_PLACEHOLDER);
129
+ if (typeof value === "string") return value.includes("{{prompt}}") || value.includes(LEGACY_PROMPT_PLACEHOLDER);
62
130
  if (Array.isArray(value)) return value.some(containsPromptPlaceholder);
63
131
  return isRecord(value) && Object.values(value).some(containsPromptPlaceholder);
64
132
  }
65
133
  function isRecord(value) {
66
134
  return typeof value === "object" && value !== null && !Array.isArray(value);
67
135
  }
68
- function record(value) {
136
+ function record$1(value) {
69
137
  return isRecord(value) ? value : void 0;
70
138
  }
71
139
  //#endregion
@@ -223,6 +291,7 @@ window.__ModuleLoader__.load({
223
291
  filterOpenAI: "OpenAI / 中转站",
224
292
  filterSeedream: "字节 Seedream",
225
293
  filterDashScope: "阿里 DashScope",
294
+ filterComfyUI: "本地 ComfyUI",
226
295
  emptyTitle: "暂无生图记录",
227
296
  emptyDesc: "在对话中让 Agent 生图后,生成的图片会自动收录到这里。",
228
297
  noMatchTitle: "未找到匹配结果",
@@ -249,6 +318,7 @@ window.__ModuleLoader__.load({
249
318
  filterOpenAI: "OpenAI / Relay",
250
319
  filterSeedream: "ByteDance Seedream",
251
320
  filterDashScope: "Aliyun DashScope",
321
+ filterComfyUI: "Local ComfyUI",
252
322
  emptyTitle: "No images generated yet",
253
323
  emptyDesc: "Images generated during conversations will automatically appear here.",
254
324
  noMatchTitle: "No matching images",
@@ -416,6 +486,10 @@ window.__ModuleLoader__.load({
416
486
  (0, react_jsx_runtime.jsx)("option", {
417
487
  value: "dashscope",
418
488
  children: t("filterDashScope")
489
+ }),
490
+ (0, react_jsx_runtime.jsx)("option", {
491
+ value: "comfyui",
492
+ children: t("filterComfyUI")
419
493
  })
420
494
  ]
421
495
  })]
@@ -941,6 +1015,146 @@ window.__ModuleLoader__.load({
941
1015
  if (fromBlockContent?.type === "image") return fromBlockContent.attachment;
942
1016
  }
943
1017
  //#endregion
1018
+ //#region lib/types/client/image-result-node.js
1019
+ const IMAGE_RESULT_NODE_KIND = "dsh-image-result";
1020
+ /**
1021
+ * One image result row per Turn, anchored at its own tool/result position while
1022
+ * the Turn runs, then optionally re-anchored beside the final answer once the
1023
+ * Turn closes (only needed under the folding Compact transcript view).
1024
+ */
1025
+ function createImageResultDefinition(options = {}) {
1026
+ return {
1027
+ kind: IMAGE_RESULT_NODE_KIND,
1028
+ target: "chat",
1029
+ match: (event) => {
1030
+ const turn = eventTurn(event);
1031
+ if (turn === void 0) return null;
1032
+ if (event.type === "turn/start") return {
1033
+ id: String(turn),
1034
+ role: "start"
1035
+ };
1036
+ if (event.type === "turn/end") return {
1037
+ id: String(turn),
1038
+ role: "update"
1039
+ };
1040
+ if (event.type === "assistant/message") return {
1041
+ id: String(turn),
1042
+ role: "update"
1043
+ };
1044
+ if (event.type === "tool/result" && imageResultFromMeta(event.data.meta) !== void 0) return {
1045
+ id: String(turn),
1046
+ role: "update"
1047
+ };
1048
+ return null;
1049
+ },
1050
+ start: (_context, match) => {
1051
+ const turn = eventTurn(match.event);
1052
+ if (match.event.type !== "turn/start" || turn === void 0) throw new Error("dsh-image-result start requires turn/start");
1053
+ return {
1054
+ turn,
1055
+ results: []
1056
+ };
1057
+ },
1058
+ update: (context, match) => {
1059
+ if (match.event.type === "turn/end") return {
1060
+ ...context.state,
1061
+ endSeq: match.event.seq
1062
+ };
1063
+ if (match.event.type === "assistant/message") return {
1064
+ ...context.state,
1065
+ answerSeq: match.event.seq
1066
+ };
1067
+ if (match.event.type !== "tool/result") return context.state;
1068
+ const result = imageResultFromMeta(match.event.data.meta);
1069
+ if (result === void 0) return context.state;
1070
+ if (context.state.results.some((candidate) => candidate.attachment.attachmentId === result.attachment.attachmentId)) return context.state;
1071
+ return {
1072
+ ...context.state,
1073
+ results: [...context.state.results, {
1074
+ ...result,
1075
+ seq: match.event.seq
1076
+ }]
1077
+ };
1078
+ },
1079
+ buildViewNode: (context) => {
1080
+ const state = context.state;
1081
+ const last = state?.results.at(-1);
1082
+ if (state === void 0 || last === void 0) return null;
1083
+ const location = context.matches.at(-1)?.location ?? context.start?.location ?? { kind: "unresolved" };
1084
+ return {
1085
+ key: context.key,
1086
+ kind: IMAGE_RESULT_NODE_KIND,
1087
+ id: context.id,
1088
+ target: "chat",
1089
+ anchorSeq: state.endSeq !== void 0 && compactTranscript(options) ? state.answerSeq ?? state.endSeq ?? last.seq : last.seq,
1090
+ location,
1091
+ visibility: "visible",
1092
+ data: {
1093
+ turn: state.turn,
1094
+ results: state.results.map(({ seq: _seq, ...result }) => result)
1095
+ }
1096
+ };
1097
+ }
1098
+ };
1099
+ }
1100
+ function compactTranscript(options) {
1101
+ try {
1102
+ return options.isCompactTranscript?.() ?? true;
1103
+ } catch {
1104
+ return true;
1105
+ }
1106
+ }
1107
+ /** Parse the plugin-owned durable presentation metadata from a Tool result event. */
1108
+ function imageResultFromMeta(value) {
1109
+ const meta = record(value);
1110
+ if (meta?.kind !== "dsh-image-gen") return void 0;
1111
+ const attachment = imageAttachment(meta.attachment);
1112
+ if (attachment === void 0) return void 0;
1113
+ return {
1114
+ attachment,
1115
+ prompt: stringValue(meta.prompt, "Generated Image"),
1116
+ provider: stringValue(meta.provider, "google"),
1117
+ model: stringValue(meta.model, ""),
1118
+ output: stringValue(meta.output, ""),
1119
+ ...typeof meta.savedTo === "string" ? { savedTo: meta.savedTo } : {},
1120
+ ...typeof meta.seed === "number" ? { seed: meta.seed } : {}
1121
+ };
1122
+ }
1123
+ function eventTurn(event) {
1124
+ const turn = event.data.turn;
1125
+ return typeof turn === "number" && Number.isSafeInteger(turn) && turn >= 0 ? turn : void 0;
1126
+ }
1127
+ function imageAttachment(value) {
1128
+ const candidate = record(value);
1129
+ if (candidate === void 0 || typeof candidate.attachmentId !== "string" || !isImageMediaType(candidate.mediaType) || !positiveInteger(candidate.bytes) || !positiveInteger(candidate.width) || !positiveInteger(candidate.height)) return void 0;
1130
+ const original = record(candidate.originalDimensions);
1131
+ if (candidate.originalDimensions !== void 0 && (original === void 0 || !positiveInteger(original.width) || !positiveInteger(original.height))) return void 0;
1132
+ return {
1133
+ attachmentId: candidate.attachmentId,
1134
+ mediaType: candidate.mediaType,
1135
+ bytes: candidate.bytes,
1136
+ width: candidate.width,
1137
+ height: candidate.height,
1138
+ ...typeof candidate.name === "string" ? { name: candidate.name } : {},
1139
+ ...original === void 0 ? {} : { originalDimensions: {
1140
+ width: original.width,
1141
+ height: original.height
1142
+ } }
1143
+ };
1144
+ }
1145
+ function isImageMediaType(value) {
1146
+ return value === "image/png" || value === "image/jpeg" || value === "image/webp" || value === "image/gif";
1147
+ }
1148
+ function positiveInteger(value) {
1149
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
1150
+ }
1151
+ function stringValue(value, fallback) {
1152
+ return typeof value === "string" ? value : fallback;
1153
+ }
1154
+ function record(value) {
1155
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
1156
+ }
1157
+ //#endregion
944
1158
  //#region lib/types/client/index.js
945
1159
  const KEY_REF = {
946
1160
  google: "GEMINI_API_KEY",
@@ -970,13 +1184,18 @@ window.__ModuleLoader__.load({
970
1184
  endpointHintDashScope: "阿里云百炼 DashScope 官方接口地址。",
971
1185
  endpointHintComfyUI: "正在运行且 DSH Host 可以访问的 ComfyUI 地址,默认使用本机 8188 端口。",
972
1186
  model: "模型",
973
- workflow: "API Workflow",
1187
+ workflow: "API Workflow 工作流",
974
1188
  workflowImport: "导入 JSON 文件",
975
- workflowReplace: "替换 JSON 文件",
976
1189
  workflowMissing: "尚未导入工作流",
977
1190
  workflowImported: "已导入 {name}",
978
- workflowHint: "请从 ComfyUI 导出 API Format JSON,并在提示词输入位置写入 {{prompt}};可用 {{seed}} 作为随机种子。",
1191
+ workflowHint: "从 ComfyUI 导出 API Format JSON,在提示词输入写入 {{prompt}},种子可用 {{seed}};图生图工作流在 LoadImage 的 image 输入写入 {{image}}(仅一次)。可导入多个工作流,Agent 也能在调用时按名称指定。",
979
1192
  workflowTooLarge: "工作流文件不能超过 5 MB。",
1193
+ workflowActiveTitle: "设为当前使用的工作流",
1194
+ workflowRemove: "删除",
1195
+ workflowPresetPlaceholder: "预设提示词,留空则只用对话内容",
1196
+ workflowPresetTitle: "预设提示词:每次调用此工作流时自动加在用户提示词前面。",
1197
+ workflowNameRequired: "工作流名称不能为空。",
1198
+ workflowDuplicateName: "工作流名称不能重复。",
980
1199
  timeout: "生成超时(秒)",
981
1200
  timeoutHint: "包括提交、等待和下载图片;默认 300 秒。",
982
1201
  saveToWorkspace: "保存到工作区",
@@ -994,6 +1213,7 @@ window.__ModuleLoader__.load({
994
1213
  loading: "正在加载图片…",
995
1214
  loadFailed: "图片读取失败 ({status})",
996
1215
  generatedTitle: "已生成图片",
1216
+ resultShown: "图片结果已显示在对话中",
997
1217
  copyImg: "复制图片",
998
1218
  download: "下载图片",
999
1219
  openNewTab: "新标签页打开",
@@ -1021,13 +1241,18 @@ window.__ModuleLoader__.load({
1021
1241
  endpointHintDashScope: "Official Aliyun DashScope endpoint.",
1022
1242
  endpointHintComfyUI: "A running ComfyUI server reachable by the DSH Host; the default points to port 8188 on this computer.",
1023
1243
  model: "Model",
1024
- workflow: "API Workflow",
1244
+ workflow: "API Workflows",
1025
1245
  workflowImport: "Import JSON file",
1026
- workflowReplace: "Replace JSON file",
1027
1246
  workflowMissing: "No workflow imported",
1028
1247
  workflowImported: "Imported {name}",
1029
- workflowHint: "Export an API Format JSON from ComfyUI and place {{prompt}} in its prompt input. {{seed}} is available for a random seed.",
1248
+ workflowHint: "Export an API Format JSON from ComfyUI and place {{prompt}} in its prompt input; {{seed}} is available for a random seed. For image editing put {{image}} (exactly once) in the LoadImage image input. Import as many workflows as you need; the Agent can also pick one by name.",
1030
1249
  workflowTooLarge: "Workflow files must be no larger than 5 MB.",
1250
+ workflowActiveTitle: "Make this the active workflow",
1251
+ workflowRemove: "Remove",
1252
+ workflowPresetPlaceholder: "Preset prompt (optional)",
1253
+ workflowPresetTitle: "Preset prompt: automatically prepended to the user prompt on every call of this workflow.",
1254
+ workflowNameRequired: "Workflow names cannot be empty.",
1255
+ workflowDuplicateName: "Workflow names must be unique.",
1031
1256
  timeout: "Generation timeout (seconds)",
1032
1257
  timeoutHint: "Covers submission, waiting, and image download; defaults to 300 seconds.",
1033
1258
  saveToWorkspace: "Save to workspace",
@@ -1045,6 +1270,7 @@ window.__ModuleLoader__.load({
1045
1270
  loading: "Loading image…",
1046
1271
  loadFailed: "Failed to load image ({status})",
1047
1272
  generatedTitle: "Generated image",
1273
+ resultShown: "Image result is shown in the conversation",
1048
1274
  copyImg: "Copy Image",
1049
1275
  download: "Download Image",
1050
1276
  openNewTab: "Open in new tab",
@@ -1075,9 +1301,16 @@ window.__ModuleLoader__.load({
1075
1301
  .dsh-ig-file-button:hover{background:var(--dsw-alias-bg-layer-2,#edf0f3);border-color:var(--dsw-alias-label-dimmed,#9ca3af)}
1076
1302
  .dsh-ig-file-button:focus-within{outline:2px solid var(--dsw-alias-brand-primary,#4c78ff);outline-offset:2px}
1077
1303
  .dsh-ig-file-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-secondary,inherit);font-size:12px}
1304
+ .dsh-ig-workflow-list{list-style:none;margin:0;padding:0;display:grid;gap:8px}
1305
+ .dsh-ig-workflow-row{display:flex;flex-direction:column;gap:6px;padding:8px;border:1px solid var(--dsw-alias-border-l2,#eee);border-radius:8px}
1306
+ .dsh-ig-workflow-main{display:flex;align-items:center;gap:8px}
1307
+ .dsh-ig-workflow-active{display:inline-flex;align-items:center;cursor:pointer;flex:none}
1308
+ .dsh-ig-workflow-active input[type=radio]{width:15px;height:15px;accent-color:var(--dsw-alias-brand-primary,#4c78ff);margin:0;cursor:pointer}
1309
+ .dsh-ig-workflow-name{flex:1;min-width:0}
1078
1310
  .dsh-ig-btn-reset{appearance:none;border:1px solid var(--dsw-alias-border-l2,#d7dbe0);border-radius:8px;padding:7px 12px;background:var(--dsw-alias-bg-layer-3,#f9fafb);color:var(--dsw-alias-label-secondary,inherit);font:inherit;font-size:13px;cursor:pointer;white-space:nowrap;transition:background .15s,border-color .15s}
1079
1311
  .dsh-ig-btn-reset:hover{background:var(--dsw-alias-bg-layer-2,#edf0f3);border-color:var(--dsw-alias-label-dimmed,#9ca3af)}
1080
1312
  .dsh-ig-hint,.dsh-ig-status{margin:0;color:var(--dsw-alias-label-tertiary,#7b818b);font-size:12px;line-height:1.4}
1313
+ .dsh-ig-status-error{color:var(--dsw-alias-label-error,#d33);font-weight:500}
1081
1314
  .dsh-ig-actions{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-top:16px;padding-top:12px;border-top:1px solid var(--dsw-alias-border-l2,#eee)}
1082
1315
  .dsh-ig-check-row{display:flex;align-items:center;gap:8px;cursor:pointer}
1083
1316
  .dsh-ig-check-row input[type=checkbox]{width:15px;height:15px;accent-color:var(--dsw-alias-brand-primary,#4c78ff);margin:0}
@@ -1086,6 +1319,7 @@ window.__ModuleLoader__.load({
1086
1319
  .dsh-ig-save:disabled{opacity:.4;cursor:default}
1087
1320
 
1088
1321
  .dsh-ig-result{display:grid;gap:10px;max-width:520px}
1322
+ .dsh-ig-promoted-results{display:grid;gap:16px}
1089
1323
  .dsh-ig-result-title{font-size:14px;font-weight:600}
1090
1324
  .dsh-ig-container{position:relative;display:inline-block;width:fit-content;max-width:100%;justify-self:start;border-radius:12px;overflow:hidden;line-height:0}
1091
1325
  .dsh-ig-container:hover .dsh-ig-toolbar{opacity:1;pointer-events:auto}
@@ -1165,7 +1399,10 @@ window.__ModuleLoader__.load({
1165
1399
  /** Mount the settings card, generated-image card, and native conversation gallery view. */
1166
1400
  function apply(ctx) {
1167
1401
  const scope = ctx.settingsScope.bind({ namespace: IMAGE_GENERATION_NAMESPACE });
1402
+ const chatScope = ctx.settingsScope.bind({ namespace: "ui-chat" });
1403
+ const isCompactTranscript = () => chatScope.getSnapshot().value?.transcriptView !== "normal";
1168
1404
  const locale = ctx.get("locale");
1405
+ const promotion = { enabled: false };
1169
1406
  ctx.effect(() => {
1170
1407
  const style = document.createElement("style");
1171
1408
  style.dataset.plugin = "dsh-image-gen";
@@ -1176,6 +1413,18 @@ window.__ModuleLoader__.load({
1176
1413
  };
1177
1414
  }, "dsh-image-gen: styles");
1178
1415
  const register = ctx.slots.register.bind(ctx.slots);
1416
+ ctx.inject(["uiConversation"], (owner) => {
1417
+ const uiConversation = asModernUiConversation(owner.get("uiConversation"));
1418
+ if (uiConversation === void 0) throw new Error("dsh-image-gen: uiConversation has an incompatible interface");
1419
+ promotion.enabled = true;
1420
+ const ownerRegister = owner.slots.register.bind(owner.slots);
1421
+ owner.effect(() => uiConversation.events.register(createImageResultDefinition({ isCompactTranscript })), "dsh-image-gen: promoted image result node");
1422
+ owner.slots.inject("conversation.chat.node", () => ownerRegister({
1423
+ name: "conversation.chat.node",
1424
+ key: IMAGE_RESULT_NODE_KIND,
1425
+ inject: () => ({ locale })
1426
+ }, PromotedImageResultNode));
1427
+ });
1179
1428
  const injectSettingsItem = (owner, credentials) => {
1180
1429
  const ownerRegister = owner.slots.register.bind(owner.slots);
1181
1430
  owner.slots.inject("settings.plugin.item", () => ownerRegister({
@@ -1200,12 +1449,18 @@ window.__ModuleLoader__.load({
1200
1449
  ctx.slots.inject("tool.call.toolview", () => register({
1201
1450
  name: "tool.call.toolview",
1202
1451
  key: "generate_image",
1203
- inject: () => ({ locale })
1452
+ inject: () => ({
1453
+ locale,
1454
+ promoted: promotion.enabled
1455
+ })
1204
1456
  }, GeneratedImageCard));
1205
1457
  ctx.slots.inject("tool.call.toolview", () => register({
1206
1458
  name: "tool.call.toolview",
1207
1459
  key: "edit_image",
1208
- inject: () => ({ locale })
1460
+ inject: () => ({
1461
+ locale,
1462
+ promoted: promotion.enabled
1463
+ })
1209
1464
  }, GeneratedImageCard));
1210
1465
  ctx.slots.inject("conversation.view", () => register({
1211
1466
  name: "conversation.view",
@@ -1217,6 +1472,12 @@ window.__ModuleLoader__.load({
1217
1472
  inject: () => ({ locale })
1218
1473
  }, GalleryViewTab));
1219
1474
  }
1475
+ function asModernUiConversation(value) {
1476
+ if (value === null || typeof value !== "object") return void 0;
1477
+ const events = value.events;
1478
+ if (events === null || typeof events !== "object") return void 0;
1479
+ return typeof events.register === "function" ? value : void 0;
1480
+ }
1220
1481
  function asCredentialsRemote(value) {
1221
1482
  if (value === null || typeof value !== "object") return void 0;
1222
1483
  const candidate = value;
@@ -1255,8 +1516,8 @@ window.__ModuleLoader__.load({
1255
1516
  const [provider, setProvider] = (0, react.useState)("google");
1256
1517
  const [model, setModel] = (0, react.useState)("");
1257
1518
  const [baseURL, setBaseURL] = (0, react.useState)("");
1258
- const [workflowJson, setWorkflowJson] = (0, react.useState)("");
1259
- const [workflowName, setWorkflowName] = (0, react.useState)("");
1519
+ const [workflows, setWorkflows] = (0, react.useState)([]);
1520
+ const [activeWorkflow, setActiveWorkflow] = (0, react.useState)("");
1260
1521
  const [timeoutSeconds, setTimeoutSeconds] = (0, react.useState)(DEFAULT_COMFYUI_TIMEOUT_MS / 1e3);
1261
1522
  const [saveToWorkspace, setSaveToWorkspace] = (0, react.useState)(true);
1262
1523
  const [workspaceFolder, setWorkspaceFolder] = (0, react.useState)("dsh-image-gen");
@@ -1264,6 +1525,15 @@ window.__ModuleLoader__.load({
1264
1525
  const [configured, setConfigured] = (0, react.useState)();
1265
1526
  const [saving, setSaving] = (0, react.useState)(false);
1266
1527
  const [message, setMessage] = (0, react.useState)("");
1528
+ const [messageIsError, setMessageIsError] = (0, react.useState)(false);
1529
+ const reportMessage = (text) => {
1530
+ setMessage(text);
1531
+ setMessageIsError(false);
1532
+ };
1533
+ const reportError = (text) => {
1534
+ setMessage(text);
1535
+ setMessageIsError(true);
1536
+ };
1267
1537
  (0, react.useEffect)(() => props.scope.subscribe(() => {
1268
1538
  setSnapshot(props.scope.getSnapshot());
1269
1539
  }), [props.scope]);
@@ -1290,8 +1560,8 @@ window.__ModuleLoader__.load({
1290
1560
  setProvider(next);
1291
1561
  setModel(modelOf(next, value));
1292
1562
  setBaseURL(baseURLOf(next, value));
1293
- setWorkflowJson(value?.comfyuiWorkflowJson ?? "");
1294
- setWorkflowName(value?.comfyuiWorkflowName ?? "");
1563
+ setWorkflows(resolveComfyUIWorkflows(value ?? {}));
1564
+ setActiveWorkflow(activeComfyUIWorkflow(value ?? {})?.name ?? "");
1295
1565
  setTimeoutSeconds(Math.max(1, Math.round((value?.comfyuiTimeoutMs ?? 3e5) / 1e3)));
1296
1566
  setSaveToWorkspace(value?.saveToWorkspace ?? true);
1297
1567
  setWorkspaceFolder(value?.workspaceFolder ?? "dsh-image-gen");
@@ -1319,10 +1589,22 @@ window.__ModuleLoader__.load({
1319
1589
  try {
1320
1590
  await props.scope.set("provider", provider);
1321
1591
  if (provider === "comfyui") {
1322
- validateComfyUIWorkflowJson(workflowJson);
1592
+ const entries = workflows.map((entry) => ({
1593
+ name: entry.name.trim(),
1594
+ json: entry.json,
1595
+ presetPrompt: (entry.presetPrompt ?? "").trim()
1596
+ }));
1597
+ for (const entry of entries) {
1598
+ if (entry.name.length === 0) throw new Error(t("workflowNameRequired"));
1599
+ validateComfyUIWorkflowJson(entry.json);
1600
+ }
1601
+ if (new Set(entries.map((entry) => entry.name)).size !== entries.length) throw new Error(t("workflowDuplicateName"));
1602
+ const activeEntry = entries.find((entry) => entry.name === activeWorkflow) ?? entries[0];
1323
1603
  await props.scope.set("comfyuiBaseURL", baseURL);
1324
- await props.scope.set("comfyuiWorkflowJson", workflowJson);
1325
- await props.scope.set("comfyuiWorkflowName", workflowName);
1604
+ await props.scope.set("comfyuiWorkflows", entries);
1605
+ await props.scope.set("comfyuiActiveWorkflow", activeEntry === void 0 ? "" : activeEntry.name);
1606
+ await props.scope.set("comfyuiWorkflowJson", activeEntry === void 0 ? "" : activeEntry.json);
1607
+ await props.scope.set("comfyuiWorkflowName", activeEntry === void 0 ? "" : activeEntry.name);
1326
1608
  await props.scope.set("comfyuiTimeoutMs", Math.max(1, Math.round(timeoutSeconds)) * 1e3);
1327
1609
  } else {
1328
1610
  await props.scope.set(provider === "google" ? "googleModel" : provider === "openai" ? "openaiModel" : provider === "seedream" ? "seedreamModel" : "dashscopeModel", model);
@@ -1338,31 +1620,58 @@ window.__ModuleLoader__.load({
1338
1620
  setKey("");
1339
1621
  setConfigured(true);
1340
1622
  }
1341
- setMessage(t("saved"));
1623
+ reportMessage(t("saved"));
1342
1624
  } catch (cause) {
1343
- setMessage(cause instanceof Error ? cause.message : String(cause));
1625
+ reportError(cause instanceof Error ? cause.message : String(cause));
1344
1626
  } finally {
1345
1627
  setSaving(false);
1346
1628
  }
1347
1629
  };
1348
1630
  const keyStatus = configured === void 0 ? t("checkingKey") : configured ? t("keyConfigured") : t("keyNotConfigured");
1349
- const workflowStatus = workflowName.length > 0 ? t("workflowImported", { name: workflowName }) : t("workflowMissing");
1631
+ const workflowStatus = activeWorkflow.length > 0 ? t("workflowImported", { name: activeWorkflow }) : t("workflowMissing");
1350
1632
  const importWorkflow = async (event) => {
1351
1633
  const file = event.target.files?.[0];
1352
1634
  event.target.value = "";
1353
1635
  if (file === void 0) return;
1354
- setMessage("");
1636
+ reportMessage("");
1355
1637
  try {
1356
1638
  if (file.size > 5242880) throw new Error(t("workflowTooLarge"));
1357
1639
  const json = await file.text();
1358
1640
  validateComfyUIWorkflowJson(json);
1359
- setWorkflowJson(json);
1360
- setWorkflowName(file.name);
1361
- setMessage(t("workflowImported", { name: file.name }));
1641
+ const name = uniqueComfyUIWorkflowName(file.name, workflows.map((entry) => entry.name));
1642
+ setWorkflows((current) => [...current, {
1643
+ name,
1644
+ json
1645
+ }]);
1646
+ setActiveWorkflow((current) => current.length > 0 ? current : name);
1647
+ reportMessage(t("workflowImported", { name }));
1362
1648
  } catch (cause) {
1363
- setMessage(cause instanceof Error ? cause.message : String(cause));
1649
+ reportError(cause instanceof Error ? cause.message : String(cause));
1364
1650
  }
1365
1651
  };
1652
+ /** Renaming the active entry keeps the active selection following its new name. */
1653
+ const renameWorkflow = (index, name) => {
1654
+ const previous = workflows[index];
1655
+ setWorkflows((current) => current.map((entry, position) => position === index ? {
1656
+ ...entry,
1657
+ name
1658
+ } : entry));
1659
+ if (previous !== void 0 && previous.name === activeWorkflow) setActiveWorkflow(name);
1660
+ };
1661
+ /** Removing the active entry moves the selection to the first remaining workflow. */
1662
+ const removeWorkflow = (index) => {
1663
+ const previous = workflows[index];
1664
+ const next = workflows.filter((_entry, position) => position !== index);
1665
+ setWorkflows(next);
1666
+ if (previous !== void 0 && previous.name === activeWorkflow) setActiveWorkflow(next[0]?.name ?? "");
1667
+ };
1668
+ /** Editing one entry's preset leaves the rest of the entry untouched. */
1669
+ const setWorkflowPreset = (index, presetPrompt) => {
1670
+ setWorkflows((current) => current.map((entry, position) => position === index ? {
1671
+ ...entry,
1672
+ presetPrompt
1673
+ } : entry));
1674
+ };
1366
1675
  return (0, react_jsx_runtime.jsxs)("li", {
1367
1676
  className: `dsh-ig-card ${open ? "dsh-ig-card-open" : ""}`,
1368
1677
  children: [(0, react_jsx_runtime.jsxs)("button", {
@@ -1535,13 +1844,60 @@ window.__ModuleLoader__.load({
1535
1844
  onChange: (event) => {
1536
1845
  importWorkflow(event);
1537
1846
  }
1538
- }), workflowName.length > 0 ? t("workflowReplace") : t("workflowImport")]
1539
- }), (0, react_jsx_runtime.jsx)("span", {
1847
+ }), t("workflowImport")]
1848
+ }), workflows.length === 0 ? (0, react_jsx_runtime.jsx)("span", {
1540
1849
  className: "dsh-ig-file-name",
1541
- title: workflowName,
1542
- children: workflowName || t("workflowMissing")
1543
- })]
1850
+ children: t("workflowMissing")
1851
+ }) : null]
1544
1852
  }),
1853
+ workflows.length > 0 ? (0, react_jsx_runtime.jsx)("ul", {
1854
+ className: "dsh-ig-workflow-list",
1855
+ children: workflows.map((entry, index) => (0, react_jsx_runtime.jsxs)("li", {
1856
+ className: "dsh-ig-workflow-row",
1857
+ children: [(0, react_jsx_runtime.jsxs)("div", {
1858
+ className: "dsh-ig-workflow-main",
1859
+ children: [
1860
+ (0, react_jsx_runtime.jsx)("label", {
1861
+ className: "dsh-ig-workflow-active",
1862
+ title: t("workflowActiveTitle"),
1863
+ children: (0, react_jsx_runtime.jsx)("input", {
1864
+ type: "radio",
1865
+ name: "dsh-ig-active-workflow",
1866
+ "aria-label": t("workflowActiveTitle"),
1867
+ checked: entry.name === activeWorkflow,
1868
+ onChange: () => {
1869
+ setActiveWorkflow(entry.name);
1870
+ }
1871
+ })
1872
+ }),
1873
+ (0, react_jsx_runtime.jsx)("input", {
1874
+ className: "dsh-ig-input dsh-ig-workflow-name",
1875
+ value: entry.name,
1876
+ title: entry.name,
1877
+ onChange: (event) => {
1878
+ renameWorkflow(index, event.target.value);
1879
+ }
1880
+ }),
1881
+ (0, react_jsx_runtime.jsx)("button", {
1882
+ type: "button",
1883
+ className: "dsh-ig-btn-reset",
1884
+ onClick: () => {
1885
+ removeWorkflow(index);
1886
+ },
1887
+ children: t("workflowRemove")
1888
+ })
1889
+ ]
1890
+ }), (0, react_jsx_runtime.jsx)("input", {
1891
+ className: "dsh-ig-input dsh-ig-workflow-preset",
1892
+ value: entry.presetPrompt ?? "",
1893
+ placeholder: t("workflowPresetPlaceholder"),
1894
+ title: t("workflowPresetTitle"),
1895
+ onChange: (event) => {
1896
+ setWorkflowPreset(index, event.target.value);
1897
+ }
1898
+ })]
1899
+ }, String(index)))
1900
+ }) : null,
1545
1901
  (0, react_jsx_runtime.jsx)("span", {
1546
1902
  className: "dsh-ig-hint",
1547
1903
  children: t("workflowHint")
@@ -1615,13 +1971,13 @@ window.__ModuleLoader__.load({
1615
1971
  (0, react_jsx_runtime.jsxs)("div", {
1616
1972
  className: "dsh-ig-actions",
1617
1973
  children: [(0, react_jsx_runtime.jsx)("p", {
1618
- className: "dsh-ig-status",
1974
+ className: `dsh-ig-status${messageIsError ? " dsh-ig-status-error" : ""}`,
1619
1975
  role: "status",
1620
1976
  children: message || (provider === "comfyui" ? workflowStatus : keyStatus)
1621
1977
  }), (0, react_jsx_runtime.jsx)("button", {
1622
1978
  className: "dsh-ig-save",
1623
1979
  type: "submit",
1624
- disabled: saving || !snapshot.writable || provider === "comfyui" && workflowJson.length === 0,
1980
+ disabled: saving || !snapshot.writable || provider === "comfyui" && workflows.length === 0,
1625
1981
  children: saving ? t("saving") : t("save")
1626
1982
  })]
1627
1983
  })
@@ -1629,43 +1985,58 @@ window.__ModuleLoader__.load({
1629
1985
  }) : null]
1630
1986
  });
1631
1987
  }
1632
- /** Render the durable attachment referenced by a completed image tool call. */
1988
+ /** Keep the legacy Tool row for old DSH and hand modern results to the independent Chat node. */
1633
1989
  function GeneratedImageCard(props) {
1634
- const attachment = imageRef(props.block);
1635
- const savedTo = imageSavedTo(props.block);
1990
+ const result = imageResultFromBlock(props.block);
1991
+ if (props.promoted && result !== void 0) return (0, react_jsx_runtime.jsx)(PromotedResultNotice, { locale: props.locale });
1992
+ return (0, react_jsx_runtime.jsx)(ImageResultCard, {
1993
+ result,
1994
+ locale: props.locale
1995
+ });
1996
+ }
1997
+ /** Render modern image artifacts as final conversation output instead of Tool process content. */
1998
+ function PromotedImageResultNode(props) {
1999
+ return (0, react_jsx_runtime.jsx)("div", {
2000
+ className: "dsh-ig-promoted-results",
2001
+ children: props.node.data.results.map((result) => (0, react_jsx_runtime.jsx)(ImageResultCard, {
2002
+ result,
2003
+ locale: props.locale
2004
+ }, result.attachment.attachmentId))
2005
+ });
2006
+ }
2007
+ function PromotedResultNotice({ locale }) {
2008
+ const lang = usePluginLanguage(locale);
2009
+ return (0, react_jsx_runtime.jsx)("div", {
2010
+ className: "dsh-ig-loading",
2011
+ children: DICT[lang].resultShown
2012
+ });
2013
+ }
2014
+ function ImageResultCard({ result, locale }) {
2015
+ const attachment = result?.attachment;
2016
+ const savedTo = result?.savedTo;
1636
2017
  const [url, setUrl] = (0, react.useState)();
1637
2018
  const [blob, setBlob] = (0, react.useState)();
1638
2019
  const [error, setError] = (0, react.useState)();
1639
2020
  const [previewOpen, setPreviewOpen] = (0, react.useState)(false);
1640
2021
  const [toast, setToast] = (0, react.useState)();
1641
- const [lang, setLang] = (0, react.useState)(() => props.locale?.getSnapshot?.()?.active?.startsWith("en") ? "en" : "zh");
1642
- (0, react.useEffect)(() => {
1643
- return props.locale?.subscribe?.(() => {
1644
- setLang(props.locale?.getSnapshot?.()?.active?.startsWith("en") ? "en" : "zh");
1645
- });
1646
- }, [props.locale]);
2022
+ const lang = usePluginLanguage(locale);
1647
2023
  const t = (keyName, params) => {
1648
2024
  let text = (lang === "en" ? DICT.en : DICT.zh)[keyName] || DICT.zh[keyName] || keyName;
1649
2025
  if (params) for (const [k, v] of Object.entries(params)) text = text.replace(`{${k}}`, v);
1650
2026
  return text;
1651
2027
  };
1652
2028
  (0, react.useEffect)(() => {
1653
- if (attachment === void 0) return;
1654
- const blockAny = props.block;
1655
- const meta = blockAny.meta ?? blockAny.resultView?.meta;
1656
- const prompt = typeof meta?.prompt === "string" ? meta.prompt : blockAny.call?.args?.prompt ?? "Generated Image";
1657
- const provider = typeof meta?.provider === "string" ? meta.provider : "google";
1658
- const model = typeof meta?.model === "string" ? meta.model : "";
1659
- const output = typeof meta?.output === "string" ? meta.output : "";
2029
+ if (result === void 0) return;
1660
2030
  saveGalleryItem({
1661
- id: attachment.attachmentId,
1662
- attachment,
1663
- prompt,
1664
- provider,
1665
- model,
1666
- output
2031
+ id: result.attachment.attachmentId,
2032
+ attachment: result.attachment,
2033
+ prompt: result.prompt,
2034
+ provider: result.provider,
2035
+ model: result.model,
2036
+ output: result.output,
2037
+ ...result.seed !== void 0 ? { seed: result.seed } : {}
1667
2038
  });
1668
- }, [attachment?.attachmentId]);
2039
+ }, [result?.attachment.attachmentId]);
1669
2040
  (0, react.useEffect)(() => {
1670
2041
  if (!previewOpen) return;
1671
2042
  const onKeyDown = (e) => {
@@ -1869,23 +2240,40 @@ window.__ModuleLoader__.load({
1869
2240
  ]
1870
2241
  });
1871
2242
  }
2243
+ function usePluginLanguage(locale) {
2244
+ const [lang, setLang] = (0, react.useState)(() => locale?.getSnapshot?.()?.active?.startsWith("en") ? "en" : "zh");
2245
+ (0, react.useEffect)(() => locale?.subscribe?.(() => {
2246
+ setLang(locale?.getSnapshot?.()?.active?.startsWith("en") ? "en" : "zh");
2247
+ }), [locale]);
2248
+ return lang;
2249
+ }
2250
+ function imageResultFromBlock(block) {
2251
+ const attachment = imageRef(block);
2252
+ if (attachment === void 0) return void 0;
2253
+ const blockValue = block;
2254
+ const meta = blockValue.meta ?? blockValue.resultView?.meta;
2255
+ return {
2256
+ attachment,
2257
+ prompt: typeof meta?.prompt === "string" ? meta.prompt : blockValue.call?.args?.prompt ?? "Generated Image",
2258
+ provider: typeof meta?.provider === "string" ? meta.provider : "google",
2259
+ model: typeof meta?.model === "string" ? meta.model : "",
2260
+ output: typeof meta?.output === "string" ? meta.output : "",
2261
+ ...typeof meta?.savedTo === "string" ? { savedTo: meta.savedTo } : {},
2262
+ ...typeof meta?.seed === "number" ? { seed: meta.seed } : {}
2263
+ };
2264
+ }
1872
2265
  function modelOf(provider, value) {
1873
- const stored = provider === "google" ? value?.googleModel : provider === "openai" ? value?.openaiModel : provider === "seedream" ? value?.seedreamModel : provider === "dashscope" ? value?.dashscopeModel : value?.comfyuiWorkflowName;
2266
+ const stored = provider === "google" ? value?.googleModel : provider === "openai" ? value?.openaiModel : provider === "seedream" ? value?.seedreamModel : provider === "dashscope" ? value?.dashscopeModel : activeComfyUIWorkflow(value ?? {})?.name;
1874
2267
  return typeof stored === "string" && stored.length > 0 ? stored : DEFAULT_MODELS[provider];
1875
2268
  }
1876
2269
  function baseURLOf(provider, value) {
1877
2270
  const stored = provider === "google" ? value?.googleEndpoint : provider === "openai" ? value?.openaiBaseURL : provider === "seedream" ? value?.seedreamBaseURL : provider === "dashscope" ? value?.dashscopeEndpoint : value?.comfyuiBaseURL;
1878
2271
  return typeof stored === "string" && stored.length > 0 ? stored : DEFAULT_BASE_URLS[provider];
1879
2272
  }
1880
- /** The workspace file path a completed image call saved, when the result meta carries one. */
1881
- function imageSavedTo(block) {
1882
- if (!("kind" in block)) return void 0;
1883
- const meta = block.meta ?? block.resultView?.meta;
1884
- return typeof meta?.savedTo === "string" ? meta.savedTo : void 0;
1885
- }
1886
2273
  //#endregion
1887
2274
  exports.GeneratedImageCard = GeneratedImageCard;
1888
2275
  exports.ImageGenerationSettingsCard = ImageGenerationSettingsCard;
2276
+ exports.PromotedImageResultNode = PromotedImageResultNode;
1889
2277
  exports.apply = apply;
1890
2278
  exports.inject = inject;
1891
2279
  return module.exports;