dsh-image-gen 0.2.4 → 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");
@@ -54,11 +110,23 @@ window.__ModuleLoader__.load({
54
110
  if (!Object.values(value).some((node) => {
55
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
  }
@@ -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
  })]
@@ -943,80 +1017,93 @@ window.__ModuleLoader__.load({
943
1017
  //#endregion
944
1018
  //#region lib/types/client/image-result-node.js
945
1019
  const IMAGE_RESULT_NODE_KIND = "dsh-image-result";
946
- /** One image result row per Turn, re-anchored to Turn end after the final answer settles. */
947
- const imageResultDefinition = {
948
- kind: IMAGE_RESULT_NODE_KIND,
949
- target: "chat",
950
- match: (event) => {
951
- const turn = eventTurn(event);
952
- if (turn === void 0) return null;
953
- if (event.type === "turn/start") return {
954
- id: String(turn),
955
- role: "start"
956
- };
957
- if (event.type === "turn/end") return {
958
- id: String(turn),
959
- role: "update"
960
- };
961
- if (event.type === "assistant/message") return {
962
- id: String(turn),
963
- role: "update"
964
- };
965
- if (event.type === "tool/result" && imageResultFromMeta(event.data.meta) !== void 0) return {
966
- id: String(turn),
967
- role: "update"
968
- };
969
- return null;
970
- },
971
- start: (_context, match) => {
972
- const turn = eventTurn(match.event);
973
- if (match.event.type !== "turn/start" || turn === void 0) throw new Error("dsh-image-result start requires turn/start");
974
- return {
975
- turn,
976
- results: []
977
- };
978
- },
979
- update: (context, match) => {
980
- if (match.event.type === "turn/end") return {
981
- ...context.state,
982
- endSeq: match.event.seq
983
- };
984
- if (match.event.type === "assistant/message") return {
985
- ...context.state,
986
- answerSeq: match.event.seq
987
- };
988
- if (match.event.type !== "tool/result") return context.state;
989
- const result = imageResultFromMeta(match.event.data.meta);
990
- if (result === void 0) return context.state;
991
- if (context.state.results.some((candidate) => candidate.attachment.attachmentId === result.attachment.attachmentId)) return context.state;
992
- return {
993
- ...context.state,
994
- results: [...context.state.results, {
995
- ...result,
996
- seq: match.event.seq
997
- }]
998
- };
999
- },
1000
- buildViewNode: (context) => {
1001
- const state = context.state;
1002
- const last = state?.results.at(-1);
1003
- if (state === void 0 || last === void 0) return null;
1004
- const location = context.matches.at(-1)?.location ?? context.start?.location ?? { kind: "unresolved" };
1005
- return {
1006
- key: context.key,
1007
- kind: IMAGE_RESULT_NODE_KIND,
1008
- id: context.id,
1009
- target: "chat",
1010
- anchorSeq: state.answerSeq ?? state.endSeq ?? last.seq,
1011
- location,
1012
- visibility: "visible",
1013
- data: {
1014
- turn: state.turn,
1015
- results: state.results.map(({ seq: _seq, ...result }) => result)
1016
- }
1017
- };
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;
1018
1105
  }
1019
- };
1106
+ }
1020
1107
  /** Parse the plugin-owned durable presentation metadata from a Tool result event. */
1021
1108
  function imageResultFromMeta(value) {
1022
1109
  const meta = record(value);
@@ -1029,7 +1116,8 @@ window.__ModuleLoader__.load({
1029
1116
  provider: stringValue(meta.provider, "google"),
1030
1117
  model: stringValue(meta.model, ""),
1031
1118
  output: stringValue(meta.output, ""),
1032
- ...typeof meta.savedTo === "string" ? { savedTo: meta.savedTo } : {}
1119
+ ...typeof meta.savedTo === "string" ? { savedTo: meta.savedTo } : {},
1120
+ ...typeof meta.seed === "number" ? { seed: meta.seed } : {}
1033
1121
  };
1034
1122
  }
1035
1123
  function eventTurn(event) {
@@ -1096,13 +1184,18 @@ window.__ModuleLoader__.load({
1096
1184
  endpointHintDashScope: "阿里云百炼 DashScope 官方接口地址。",
1097
1185
  endpointHintComfyUI: "正在运行且 DSH Host 可以访问的 ComfyUI 地址,默认使用本机 8188 端口。",
1098
1186
  model: "模型",
1099
- workflow: "API Workflow",
1187
+ workflow: "API Workflow 工作流",
1100
1188
  workflowImport: "导入 JSON 文件",
1101
- workflowReplace: "替换 JSON 文件",
1102
1189
  workflowMissing: "尚未导入工作流",
1103
1190
  workflowImported: "已导入 {name}",
1104
- workflowHint: "请从 ComfyUI 导出 API Format JSON,并在提示词输入位置写入 {{prompt}};可用 {{seed}} 作为随机种子。",
1191
+ workflowHint: "从 ComfyUI 导出 API Format JSON,在提示词输入写入 {{prompt}},种子可用 {{seed}};图生图工作流在 LoadImage 的 image 输入写入 {{image}}(仅一次)。可导入多个工作流,Agent 也能在调用时按名称指定。",
1105
1192
  workflowTooLarge: "工作流文件不能超过 5 MB。",
1193
+ workflowActiveTitle: "设为当前使用的工作流",
1194
+ workflowRemove: "删除",
1195
+ workflowPresetPlaceholder: "预设提示词,留空则只用对话内容",
1196
+ workflowPresetTitle: "预设提示词:每次调用此工作流时自动加在用户提示词前面。",
1197
+ workflowNameRequired: "工作流名称不能为空。",
1198
+ workflowDuplicateName: "工作流名称不能重复。",
1106
1199
  timeout: "生成超时(秒)",
1107
1200
  timeoutHint: "包括提交、等待和下载图片;默认 300 秒。",
1108
1201
  saveToWorkspace: "保存到工作区",
@@ -1148,13 +1241,18 @@ window.__ModuleLoader__.load({
1148
1241
  endpointHintDashScope: "Official Aliyun DashScope endpoint.",
1149
1242
  endpointHintComfyUI: "A running ComfyUI server reachable by the DSH Host; the default points to port 8188 on this computer.",
1150
1243
  model: "Model",
1151
- workflow: "API Workflow",
1244
+ workflow: "API Workflows",
1152
1245
  workflowImport: "Import JSON file",
1153
- workflowReplace: "Replace JSON file",
1154
1246
  workflowMissing: "No workflow imported",
1155
1247
  workflowImported: "Imported {name}",
1156
- 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.",
1157
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.",
1158
1256
  timeout: "Generation timeout (seconds)",
1159
1257
  timeoutHint: "Covers submission, waiting, and image download; defaults to 300 seconds.",
1160
1258
  saveToWorkspace: "Save to workspace",
@@ -1203,9 +1301,16 @@ window.__ModuleLoader__.load({
1203
1301
  .dsh-ig-file-button:hover{background:var(--dsw-alias-bg-layer-2,#edf0f3);border-color:var(--dsw-alias-label-dimmed,#9ca3af)}
1204
1302
  .dsh-ig-file-button:focus-within{outline:2px solid var(--dsw-alias-brand-primary,#4c78ff);outline-offset:2px}
1205
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}
1206
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}
1207
1311
  .dsh-ig-btn-reset:hover{background:var(--dsw-alias-bg-layer-2,#edf0f3);border-color:var(--dsw-alias-label-dimmed,#9ca3af)}
1208
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}
1209
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)}
1210
1315
  .dsh-ig-check-row{display:flex;align-items:center;gap:8px;cursor:pointer}
1211
1316
  .dsh-ig-check-row input[type=checkbox]{width:15px;height:15px;accent-color:var(--dsw-alias-brand-primary,#4c78ff);margin:0}
@@ -1294,6 +1399,8 @@ window.__ModuleLoader__.load({
1294
1399
  /** Mount the settings card, generated-image card, and native conversation gallery view. */
1295
1400
  function apply(ctx) {
1296
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";
1297
1404
  const locale = ctx.get("locale");
1298
1405
  const promotion = { enabled: false };
1299
1406
  ctx.effect(() => {
@@ -1311,7 +1418,7 @@ window.__ModuleLoader__.load({
1311
1418
  if (uiConversation === void 0) throw new Error("dsh-image-gen: uiConversation has an incompatible interface");
1312
1419
  promotion.enabled = true;
1313
1420
  const ownerRegister = owner.slots.register.bind(owner.slots);
1314
- owner.effect(() => uiConversation.events.register(imageResultDefinition), "dsh-image-gen: promoted image result node");
1421
+ owner.effect(() => uiConversation.events.register(createImageResultDefinition({ isCompactTranscript })), "dsh-image-gen: promoted image result node");
1315
1422
  owner.slots.inject("conversation.chat.node", () => ownerRegister({
1316
1423
  name: "conversation.chat.node",
1317
1424
  key: IMAGE_RESULT_NODE_KIND,
@@ -1409,8 +1516,8 @@ window.__ModuleLoader__.load({
1409
1516
  const [provider, setProvider] = (0, react.useState)("google");
1410
1517
  const [model, setModel] = (0, react.useState)("");
1411
1518
  const [baseURL, setBaseURL] = (0, react.useState)("");
1412
- const [workflowJson, setWorkflowJson] = (0, react.useState)("");
1413
- const [workflowName, setWorkflowName] = (0, react.useState)("");
1519
+ const [workflows, setWorkflows] = (0, react.useState)([]);
1520
+ const [activeWorkflow, setActiveWorkflow] = (0, react.useState)("");
1414
1521
  const [timeoutSeconds, setTimeoutSeconds] = (0, react.useState)(DEFAULT_COMFYUI_TIMEOUT_MS / 1e3);
1415
1522
  const [saveToWorkspace, setSaveToWorkspace] = (0, react.useState)(true);
1416
1523
  const [workspaceFolder, setWorkspaceFolder] = (0, react.useState)("dsh-image-gen");
@@ -1418,6 +1525,15 @@ window.__ModuleLoader__.load({
1418
1525
  const [configured, setConfigured] = (0, react.useState)();
1419
1526
  const [saving, setSaving] = (0, react.useState)(false);
1420
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
+ };
1421
1537
  (0, react.useEffect)(() => props.scope.subscribe(() => {
1422
1538
  setSnapshot(props.scope.getSnapshot());
1423
1539
  }), [props.scope]);
@@ -1444,8 +1560,8 @@ window.__ModuleLoader__.load({
1444
1560
  setProvider(next);
1445
1561
  setModel(modelOf(next, value));
1446
1562
  setBaseURL(baseURLOf(next, value));
1447
- setWorkflowJson(value?.comfyuiWorkflowJson ?? "");
1448
- setWorkflowName(value?.comfyuiWorkflowName ?? "");
1563
+ setWorkflows(resolveComfyUIWorkflows(value ?? {}));
1564
+ setActiveWorkflow(activeComfyUIWorkflow(value ?? {})?.name ?? "");
1449
1565
  setTimeoutSeconds(Math.max(1, Math.round((value?.comfyuiTimeoutMs ?? 3e5) / 1e3)));
1450
1566
  setSaveToWorkspace(value?.saveToWorkspace ?? true);
1451
1567
  setWorkspaceFolder(value?.workspaceFolder ?? "dsh-image-gen");
@@ -1473,10 +1589,22 @@ window.__ModuleLoader__.load({
1473
1589
  try {
1474
1590
  await props.scope.set("provider", provider);
1475
1591
  if (provider === "comfyui") {
1476
- 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];
1477
1603
  await props.scope.set("comfyuiBaseURL", baseURL);
1478
- await props.scope.set("comfyuiWorkflowJson", workflowJson);
1479
- 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);
1480
1608
  await props.scope.set("comfyuiTimeoutMs", Math.max(1, Math.round(timeoutSeconds)) * 1e3);
1481
1609
  } else {
1482
1610
  await props.scope.set(provider === "google" ? "googleModel" : provider === "openai" ? "openaiModel" : provider === "seedream" ? "seedreamModel" : "dashscopeModel", model);
@@ -1492,31 +1620,58 @@ window.__ModuleLoader__.load({
1492
1620
  setKey("");
1493
1621
  setConfigured(true);
1494
1622
  }
1495
- setMessage(t("saved"));
1623
+ reportMessage(t("saved"));
1496
1624
  } catch (cause) {
1497
- setMessage(cause instanceof Error ? cause.message : String(cause));
1625
+ reportError(cause instanceof Error ? cause.message : String(cause));
1498
1626
  } finally {
1499
1627
  setSaving(false);
1500
1628
  }
1501
1629
  };
1502
1630
  const keyStatus = configured === void 0 ? t("checkingKey") : configured ? t("keyConfigured") : t("keyNotConfigured");
1503
- const workflowStatus = workflowName.length > 0 ? t("workflowImported", { name: workflowName }) : t("workflowMissing");
1631
+ const workflowStatus = activeWorkflow.length > 0 ? t("workflowImported", { name: activeWorkflow }) : t("workflowMissing");
1504
1632
  const importWorkflow = async (event) => {
1505
1633
  const file = event.target.files?.[0];
1506
1634
  event.target.value = "";
1507
1635
  if (file === void 0) return;
1508
- setMessage("");
1636
+ reportMessage("");
1509
1637
  try {
1510
1638
  if (file.size > 5242880) throw new Error(t("workflowTooLarge"));
1511
1639
  const json = await file.text();
1512
1640
  validateComfyUIWorkflowJson(json);
1513
- setWorkflowJson(json);
1514
- setWorkflowName(file.name);
1515
- 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 }));
1516
1648
  } catch (cause) {
1517
- setMessage(cause instanceof Error ? cause.message : String(cause));
1649
+ reportError(cause instanceof Error ? cause.message : String(cause));
1518
1650
  }
1519
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
+ };
1520
1675
  return (0, react_jsx_runtime.jsxs)("li", {
1521
1676
  className: `dsh-ig-card ${open ? "dsh-ig-card-open" : ""}`,
1522
1677
  children: [(0, react_jsx_runtime.jsxs)("button", {
@@ -1689,13 +1844,60 @@ window.__ModuleLoader__.load({
1689
1844
  onChange: (event) => {
1690
1845
  importWorkflow(event);
1691
1846
  }
1692
- }), workflowName.length > 0 ? t("workflowReplace") : t("workflowImport")]
1693
- }), (0, react_jsx_runtime.jsx)("span", {
1847
+ }), t("workflowImport")]
1848
+ }), workflows.length === 0 ? (0, react_jsx_runtime.jsx)("span", {
1694
1849
  className: "dsh-ig-file-name",
1695
- title: workflowName,
1696
- children: workflowName || t("workflowMissing")
1697
- })]
1850
+ children: t("workflowMissing")
1851
+ }) : null]
1698
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,
1699
1901
  (0, react_jsx_runtime.jsx)("span", {
1700
1902
  className: "dsh-ig-hint",
1701
1903
  children: t("workflowHint")
@@ -1769,13 +1971,13 @@ window.__ModuleLoader__.load({
1769
1971
  (0, react_jsx_runtime.jsxs)("div", {
1770
1972
  className: "dsh-ig-actions",
1771
1973
  children: [(0, react_jsx_runtime.jsx)("p", {
1772
- className: "dsh-ig-status",
1974
+ className: `dsh-ig-status${messageIsError ? " dsh-ig-status-error" : ""}`,
1773
1975
  role: "status",
1774
1976
  children: message || (provider === "comfyui" ? workflowStatus : keyStatus)
1775
1977
  }), (0, react_jsx_runtime.jsx)("button", {
1776
1978
  className: "dsh-ig-save",
1777
1979
  type: "submit",
1778
- disabled: saving || !snapshot.writable || provider === "comfyui" && workflowJson.length === 0,
1980
+ disabled: saving || !snapshot.writable || provider === "comfyui" && workflows.length === 0,
1779
1981
  children: saving ? t("saving") : t("save")
1780
1982
  })]
1781
1983
  })
@@ -1831,7 +2033,8 @@ window.__ModuleLoader__.load({
1831
2033
  prompt: result.prompt,
1832
2034
  provider: result.provider,
1833
2035
  model: result.model,
1834
- output: result.output
2036
+ output: result.output,
2037
+ ...result.seed !== void 0 ? { seed: result.seed } : {}
1835
2038
  });
1836
2039
  }, [result?.attachment.attachmentId]);
1837
2040
  (0, react.useEffect)(() => {
@@ -2055,11 +2258,12 @@ window.__ModuleLoader__.load({
2055
2258
  provider: typeof meta?.provider === "string" ? meta.provider : "google",
2056
2259
  model: typeof meta?.model === "string" ? meta.model : "",
2057
2260
  output: typeof meta?.output === "string" ? meta.output : "",
2058
- ...typeof meta?.savedTo === "string" ? { savedTo: meta.savedTo } : {}
2261
+ ...typeof meta?.savedTo === "string" ? { savedTo: meta.savedTo } : {},
2262
+ ...typeof meta?.seed === "number" ? { seed: meta.seed } : {}
2059
2263
  };
2060
2264
  }
2061
2265
  function modelOf(provider, value) {
2062
- 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;
2063
2267
  return typeof stored === "string" && stored.length > 0 ? stored : DEFAULT_MODELS[provider];
2064
2268
  }
2065
2269
  function baseURLOf(provider, value) {