dsh-image-gen 0.4.1 → 0.5.1

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
@@ -17,20 +17,52 @@ window.__ModuleLoader__.load({
17
17
  const INSPIRATION_ROUTE = "/plugins/dsh-image-gen/inspiration";
18
18
  /** Browser route used for saving generated images to workspace on demand. */
19
19
  const SAVE_WORKSPACE_ROUTE = "/plugins/dsh-image-gen/save-workspace";
20
+ /** Browser route the settings card probes provider connectivity through. */
21
+ const TEST_CONNECTION_ROUTE = "/plugins/dsh-image-gen/test";
20
22
  /** Namespace persisted through DSH Settings. */
21
23
  const IMAGE_GENERATION_NAMESPACE = "image-generation";
24
+ /** Supported providers. */
25
+ const IMAGE_PROVIDERS = [
26
+ "google",
27
+ "openai",
28
+ "openai-compat",
29
+ "seedream",
30
+ "dashscope",
31
+ "xai",
32
+ "zhipu",
33
+ "comfyui"
34
+ ];
22
35
  /** Providers supported by the first browser workbench release. */
23
36
  const CLOUD_IMAGE_PROVIDERS = [
24
37
  "google",
25
38
  "openai",
39
+ "openai-compat",
26
40
  "seedream",
27
- "dashscope"
41
+ "dashscope",
42
+ "xai",
43
+ "zhipu"
28
44
  ];
45
+ /** The credential reference each cloud provider's API key is stored under. */
46
+ const CLOUD_CREDENTIAL_REFS = {
47
+ google: "GEMINI_API_KEY",
48
+ openai: "OPENAI_API_KEY",
49
+ "openai-compat": "DSH_IMAGE_GEN_OPENAI_COMPAT_KEY",
50
+ seedream: "ARK_API_KEY",
51
+ dashscope: "DASHSCOPE_API_KEY",
52
+ xai: "XAI_API_KEY",
53
+ zhipu: "ZHIPUAI_API_KEY"
54
+ };
55
+ /** The credential reference storing this provider's API key, when it uses one. */
56
+ function cloudCredentialRef(provider) {
57
+ return CLOUD_IMAGE_PROVIDERS.includes(provider) ? CLOUD_CREDENTIAL_REFS[provider] : void 0;
58
+ }
29
59
  /** Default endpoints and base URLs. */
30
60
  const DEFAULT_GOOGLE_ENDPOINT = "https://generativelanguage.googleapis.com/v1beta/interactions";
31
61
  const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
32
62
  const DEFAULT_SEEDREAM_BASE_URL = "https://ark.cn-beijing.volces.com/api/v3";
33
63
  const DEFAULT_DASHSCOPE_ENDPOINT = "https://dashscope.aliyuncs.com/api/v1";
64
+ const DEFAULT_XAI_BASE_URL = "https://api.x.ai/v1";
65
+ const DEFAULT_ZHIPU_BASE_URL = "https://open.bigmodel.cn/api/paas/v4";
34
66
  const DEFAULT_COMFYUI_BASE_URL = "http://127.0.0.1:8188";
35
67
  const DEFAULT_COMFYUI_TIMEOUT_MS = 3e5;
36
68
  const DEFAULT_COMFYUI_WORKFLOW_LABEL = "API workflow";
@@ -39,6 +71,8 @@ window.__ModuleLoader__.load({
39
71
  const DEFAULT_OPENAI_MODEL = "gpt-image-2";
40
72
  const DEFAULT_SEEDREAM_MODEL = "doubao-seedream-5-0-260128";
41
73
  const DEFAULT_DASHSCOPE_MODEL = "qwen-image-3.0";
74
+ const DEFAULT_XAI_MODEL = "grok-imagine-image";
75
+ const DEFAULT_ZHIPU_MODEL = "glm-image";
42
76
  /** Named workflows, falling back to the legacy single-workflow fields when the list is empty. */
43
77
  function resolveComfyUIWorkflows(source) {
44
78
  const named = [];
@@ -85,15 +119,21 @@ window.__ModuleLoader__.load({
85
119
  const DEFAULT_MODELS = {
86
120
  google: DEFAULT_GOOGLE_MODEL,
87
121
  openai: DEFAULT_OPENAI_MODEL,
122
+ "openai-compat": "",
88
123
  seedream: DEFAULT_SEEDREAM_MODEL,
89
124
  dashscope: DEFAULT_DASHSCOPE_MODEL,
125
+ xai: DEFAULT_XAI_MODEL,
126
+ zhipu: DEFAULT_ZHIPU_MODEL,
90
127
  comfyui: DEFAULT_COMFYUI_WORKFLOW_LABEL
91
128
  };
92
129
  const DEFAULT_BASE_URLS = {
93
130
  google: DEFAULT_GOOGLE_ENDPOINT,
94
131
  openai: DEFAULT_OPENAI_BASE_URL,
132
+ "openai-compat": "",
95
133
  seedream: DEFAULT_SEEDREAM_BASE_URL,
96
134
  dashscope: DEFAULT_DASHSCOPE_ENDPOINT,
135
+ xai: DEFAULT_XAI_BASE_URL,
136
+ zhipu: DEFAULT_ZHIPU_BASE_URL,
97
137
  comfyui: DEFAULT_COMFYUI_BASE_URL
98
138
  };
99
139
  //#endregion
@@ -4267,7 +4307,7 @@ window.__ModuleLoader__.load({
4267
4307
  ratio: "auto",
4268
4308
  quality: normalized === "1K" || normalized === "4K" ? normalized : "2K"
4269
4309
  };
4270
- if (provider === "openai") return {
4310
+ if (provider === "openai" || provider === "openai-compat" || provider === "xai" || provider === "zhipu") return {
4271
4311
  ratio: ratioFromSize(normalized, "x"),
4272
4312
  quality: "standard"
4273
4313
  };
@@ -4309,9 +4349,12 @@ window.__ModuleLoader__.load({
4309
4349
  tabTasks: "任务",
4310
4350
  filterAllProviders: "全部提供商",
4311
4351
  filterGoogle: "Google Gemini",
4312
- filterOpenAI: "OpenAI / 中转站",
4352
+ filterOpenAI: "OpenAI",
4353
+ filterOpenAICompat: "OpenAI 兼容",
4313
4354
  filterSeedream: "字节 Seedream",
4314
4355
  filterDashScope: "阿里 DashScope",
4356
+ filterXAI: "xAI Grok",
4357
+ filterZhipu: "智谱 GLM",
4315
4358
  filterComfyUI: "本地 ComfyUI",
4316
4359
  filterAllModels: "全部模型",
4317
4360
  filterAllRatios: "全部比例",
@@ -4391,9 +4434,12 @@ window.__ModuleLoader__.load({
4391
4434
  tabTasks: "Tasks",
4392
4435
  filterAllProviders: "All Providers",
4393
4436
  filterGoogle: "Google Gemini",
4394
- filterOpenAI: "OpenAI / Relay",
4437
+ filterOpenAI: "OpenAI",
4438
+ filterOpenAICompat: "OpenAI-compatible",
4395
4439
  filterSeedream: "ByteDance Seedream",
4396
4440
  filterDashScope: "Aliyun DashScope",
4441
+ filterXAI: "xAI Grok",
4442
+ filterZhipu: "Zhipu GLM",
4397
4443
  filterComfyUI: "Local ComfyUI",
4398
4444
  filterAllModels: "All Models",
4399
4445
  filterAllRatios: "All Ratios",
@@ -5079,6 +5125,10 @@ window.__ModuleLoader__.load({
5079
5125
  value: "openai",
5080
5126
  children: t("filterOpenAI")
5081
5127
  }),
5128
+ (0, react_jsx_runtime.jsx)("option", {
5129
+ value: "openai-compat",
5130
+ children: t("filterOpenAICompat")
5131
+ }),
5082
5132
  (0, react_jsx_runtime.jsx)("option", {
5083
5133
  value: "seedream",
5084
5134
  children: t("filterSeedream")
@@ -5087,6 +5137,14 @@ window.__ModuleLoader__.load({
5087
5137
  value: "dashscope",
5088
5138
  children: t("filterDashScope")
5089
5139
  }),
5140
+ (0, react_jsx_runtime.jsx)("option", {
5141
+ value: "xai",
5142
+ children: t("filterXAI")
5143
+ }),
5144
+ (0, react_jsx_runtime.jsx)("option", {
5145
+ value: "zhipu",
5146
+ children: t("filterZhipu")
5147
+ }),
5090
5148
  (0, react_jsx_runtime.jsx)("option", {
5091
5149
  value: "comfyui",
5092
5150
  children: t("filterComfyUI")
@@ -6137,7 +6195,7 @@ window.__ModuleLoader__.load({
6137
6195
  function isRevision(value) {
6138
6196
  if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
6139
6197
  const candidate = value;
6140
- return isAttachment(candidate.attachment) && (candidate.provider === "google" || candidate.provider === "openai" || candidate.provider === "seedream" || candidate.provider === "dashscope") && typeof candidate.prompt === "string" && typeof candidate.model === "string" && typeof candidate.output === "string" && typeof candidate.createdAt === "number" && Number.isFinite(candidate.createdAt) && typeof candidate.ratio === "string" && typeof candidate.quality === "string";
6198
+ return isAttachment(candidate.attachment) && typeof candidate.provider === "string" && CLOUD_IMAGE_PROVIDERS.includes(candidate.provider) && typeof candidate.prompt === "string" && typeof candidate.model === "string" && typeof candidate.output === "string" && typeof candidate.createdAt === "number" && Number.isFinite(candidate.createdAt) && typeof candidate.ratio === "string" && typeof candidate.quality === "string";
6141
6199
  }
6142
6200
  function isAttachment(value) {
6143
6201
  if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
@@ -6145,33 +6203,390 @@ window.__ModuleLoader__.load({
6145
6203
  return typeof candidate.attachmentId === "string" && (candidate.mediaType === "image/png" || candidate.mediaType === "image/jpeg" || candidate.mediaType === "image/webp" || candidate.mediaType === "image/gif") && typeof candidate.bytes === "number" && typeof candidate.width === "number" && typeof candidate.height === "number";
6146
6204
  }
6147
6205
  //#endregion
6148
- //#region lib/types/client/index.js
6149
- const KEY_REF = {
6150
- google: "GEMINI_API_KEY",
6151
- openai: "OPENAI_API_KEY",
6152
- seedream: "ARK_API_KEY",
6153
- dashscope: "DASHSCOPE_API_KEY"
6206
+ //#region lib/types/client/provider-pill.js
6207
+ /**
6208
+ * Composer tool-row pill: switch the default image provider without leaving
6209
+ * the chat. Registered into the official 'conversation.input.right' slot; the
6210
+ * write path is the same `provider` settings field the settings card uses, so
6211
+ * the two views can never disagree.
6212
+ */
6213
+ const PILL_DICT = {
6214
+ zh: {
6215
+ pillLabel: "生图",
6216
+ menuTitle: "生图模型",
6217
+ keyConfigured: "Key 已配置",
6218
+ keyMissing: "未配置 Key",
6219
+ noKeyNeeded: "无需 Key",
6220
+ noWorkflow: "未导入工作流",
6221
+ readOnly: "图像设置为只读,无法在此切换",
6222
+ switchFailed: "切换失败"
6223
+ },
6224
+ en: {
6225
+ pillLabel: "Image",
6226
+ menuTitle: "Image model",
6227
+ keyConfigured: "Key configured",
6228
+ keyMissing: "No key",
6229
+ noKeyNeeded: "No key needed",
6230
+ noWorkflow: "No workflow",
6231
+ readOnly: "Image settings are read-only; switch them in the config source",
6232
+ switchFailed: "Switch failed"
6233
+ }
6234
+ };
6235
+ /** Short row labels; the settings card keeps the longer descriptive names. */
6236
+ const PILL_PROVIDER_LABELS = {
6237
+ google: "Gemini",
6238
+ openai: "OpenAI",
6239
+ "openai-compat": "OpenAI 兼容",
6240
+ seedream: "Seedream",
6241
+ dashscope: "DashScope",
6242
+ xai: "Grok",
6243
+ zhipu: "智谱 GLM",
6244
+ comfyui: "ComfyUI"
6154
6245
  };
6246
+ /** Whether the user opted into the composer pill; hidden (not merely inert) when off. */
6247
+ function pillVisible(value) {
6248
+ return value?.showProviderPill === true;
6249
+ }
6250
+ /** Model (or ComfyUI workflow) a provider row will use with the persisted settings. */
6251
+ function pillModelOf(provider, value) {
6252
+ if (provider === "comfyui") {
6253
+ const workflow = activeComfyUIWorkflow(value ?? {});
6254
+ return workflow === void 0 ? "" : workflow.name;
6255
+ }
6256
+ const stored = provider === "google" ? value?.googleModel : provider === "openai" ? value?.openaiModel : provider === "openai-compat" ? value?.openaiCompatModel : provider === "seedream" ? value?.seedreamModel : provider === "dashscope" ? value?.dashscopeModel : provider === "xai" ? value?.xaiModel : value?.zhipuModel;
6257
+ return typeof stored === "string" && stored.length > 0 ? stored : DEFAULT_MODELS[provider];
6258
+ }
6259
+ /** One fixed-position provider menu anchored to the pill button. */
6260
+ function ImageProviderPill(props) {
6261
+ const [snapshot, setSnapshot] = (0, react.useState)(() => props.scope.getSnapshot());
6262
+ const [lang, setLang] = (0, react.useState)(() => props.locale?.getSnapshot?.()?.active?.startsWith("en") ? "en" : "zh");
6263
+ const [open, setOpen] = (0, react.useState)(false);
6264
+ const [pending, setPending] = (0, react.useState)(void 0);
6265
+ const [error, setError] = (0, react.useState)("");
6266
+ const [dots, setDots] = (0, react.useState)({
6267
+ google: "unknown",
6268
+ openai: "unknown",
6269
+ "openai-compat": "unknown",
6270
+ seedream: "unknown",
6271
+ dashscope: "unknown",
6272
+ xai: "unknown",
6273
+ zhipu: "unknown"
6274
+ });
6275
+ const [keyTick, setKeyTick] = (0, react.useState)(0);
6276
+ const buttonRef = (0, react.useRef)(null);
6277
+ const [menuStyle, setMenuStyle] = (0, react.useState)(null);
6278
+ (0, react.useEffect)(() => props.scope.subscribe(() => {
6279
+ setSnapshot(props.scope.getSnapshot());
6280
+ }), [props.scope]);
6281
+ (0, react.useEffect)(() => props.locale?.subscribe?.(() => {
6282
+ setLang(props.locale?.getSnapshot?.()?.active?.startsWith("en") ? "en" : "zh");
6283
+ }), [props.locale]);
6284
+ (0, react.useEffect)(() => props.credentialEvents?.listen(() => {
6285
+ setKeyTick((tick) => tick + 1);
6286
+ }), [props.credentialEvents]);
6287
+ (0, react.useEffect)(() => {
6288
+ let active = true;
6289
+ const refs = [];
6290
+ for (const provider of CLOUD_IMAGE_PROVIDERS) {
6291
+ const ref = cloudCredentialRef(provider);
6292
+ if (ref !== void 0) refs.push(ref);
6293
+ }
6294
+ props.credentials.describe(refs).then((response) => {
6295
+ if (!active) return;
6296
+ const next = {};
6297
+ for (const provider of CLOUD_IMAGE_PROVIDERS) {
6298
+ const info = response.ok ? response.value?.[cloudCredentialRef(provider)] : void 0;
6299
+ next[provider] = response.ok ? info?.configured ? "configured" : "missing" : "unknown";
6300
+ }
6301
+ setDots(next);
6302
+ }).catch(() => {
6303
+ if (!active) return;
6304
+ setDots({
6305
+ google: "unknown",
6306
+ openai: "unknown",
6307
+ "openai-compat": "unknown",
6308
+ seedream: "unknown",
6309
+ dashscope: "unknown",
6310
+ xai: "unknown",
6311
+ zhipu: "unknown"
6312
+ });
6313
+ });
6314
+ return () => {
6315
+ active = false;
6316
+ };
6317
+ }, [props.credentials, keyTick]);
6318
+ const t = (keyName) => (lang === "en" ? PILL_DICT.en : PILL_DICT.zh)[keyName];
6319
+ const current = snapshot.value?.provider ?? "google";
6320
+ const writable = snapshot.writable;
6321
+ (0, react.useEffect)(() => {
6322
+ if (!open) {
6323
+ setMenuStyle(null);
6324
+ return;
6325
+ }
6326
+ const rect = buttonRef.current?.getBoundingClientRect();
6327
+ if (rect !== void 0) setMenuStyle({
6328
+ left: rect.left,
6329
+ bottom: window.innerHeight - rect.top + 6
6330
+ });
6331
+ }, [open]);
6332
+ (0, react.useEffect)(() => {
6333
+ if (!open) return;
6334
+ const onPointerDown = (event) => {
6335
+ const target = event.target;
6336
+ if (target instanceof Node && buttonRef.current !== null && !buttonRef.current.contains(target) && !(target instanceof Element && target.closest(".dsh-ig-pill-menu") !== null)) setOpen(false);
6337
+ };
6338
+ const onKeyDown = (event) => {
6339
+ if (event.key === "Escape") setOpen(false);
6340
+ };
6341
+ const onReflow = () => {
6342
+ setOpen(false);
6343
+ };
6344
+ document.addEventListener("mousedown", onPointerDown, true);
6345
+ document.addEventListener("keydown", onKeyDown, true);
6346
+ window.addEventListener("resize", onReflow);
6347
+ window.addEventListener("scroll", onReflow, true);
6348
+ return () => {
6349
+ document.removeEventListener("mousedown", onPointerDown, true);
6350
+ document.removeEventListener("keydown", onKeyDown, true);
6351
+ window.removeEventListener("resize", onReflow);
6352
+ window.removeEventListener("scroll", onReflow, true);
6353
+ };
6354
+ }, [open]);
6355
+ if (!pillVisible(snapshot.value)) return null;
6356
+ /** Same write path as the settings card: one field, one disposer, no implicit side effects. */
6357
+ const select = (provider) => {
6358
+ if (pending !== void 0 || provider === current) {
6359
+ setOpen(false);
6360
+ return;
6361
+ }
6362
+ setError("");
6363
+ setPending(provider);
6364
+ props.scope.set("provider", provider).then(() => {
6365
+ setOpen(false);
6366
+ }).catch((cause) => {
6367
+ setError(cause instanceof Error ? cause.message : String(cause));
6368
+ }).finally(() => {
6369
+ setPending(void 0);
6370
+ });
6371
+ };
6372
+ const dotClassOf = (provider) => {
6373
+ if (provider === "comfyui") return "dsh-ig-pill-dot-neutral";
6374
+ const dot = dots[provider];
6375
+ return dot === "configured" ? "dsh-ig-pill-dot-ok" : dot === "missing" ? "dsh-ig-pill-dot-missing" : "dsh-ig-pill-dot-neutral";
6376
+ };
6377
+ const subtitleOf = (provider) => {
6378
+ if (provider === "comfyui") {
6379
+ const workflow = pillModelOf(provider, snapshot.value);
6380
+ return workflow.length > 0 ? `${t("noKeyNeeded")} · ${workflow}` : `${t("noKeyNeeded")} · ${t("noWorkflow")}`;
6381
+ }
6382
+ const dot = dots[provider];
6383
+ const model = pillModelOf(provider, snapshot.value);
6384
+ if (dot === "configured") return `${t("keyConfigured")} · ${model}`;
6385
+ if (dot === "missing") return `${t("keyMissing")} · ${model}`;
6386
+ return model;
6387
+ };
6388
+ return (0, react_jsx_runtime.jsxs)("div", {
6389
+ className: "dsh-ig-pill-root",
6390
+ children: [(0, react_jsx_runtime.jsxs)("button", {
6391
+ type: "button",
6392
+ ref: buttonRef,
6393
+ className: `dsh-ig-pill-button${open ? " dsh-ig-pill-button-open" : ""}`,
6394
+ "aria-haspopup": "menu",
6395
+ "aria-expanded": open,
6396
+ title: writable ? t("menuTitle") : t("readOnly"),
6397
+ disabled: !writable,
6398
+ onClick: () => {
6399
+ setOpen((value) => !value);
6400
+ },
6401
+ children: [
6402
+ (0, react_jsx_runtime.jsxs)("svg", {
6403
+ className: "dsh-ig-pill-icon",
6404
+ width: "13",
6405
+ height: "13",
6406
+ viewBox: "0 0 16 16",
6407
+ fill: "none",
6408
+ stroke: "currentColor",
6409
+ strokeWidth: "1.6",
6410
+ strokeLinecap: "round",
6411
+ strokeLinejoin: "round",
6412
+ "aria-hidden": "true",
6413
+ children: [
6414
+ (0, react_jsx_runtime.jsx)("rect", {
6415
+ x: "1.5",
6416
+ y: "2.5",
6417
+ width: "13",
6418
+ height: "11",
6419
+ rx: "2"
6420
+ }),
6421
+ (0, react_jsx_runtime.jsx)("circle", {
6422
+ cx: "5.5",
6423
+ cy: "6.5",
6424
+ r: "1.3"
6425
+ }),
6426
+ (0, react_jsx_runtime.jsx)("path", { d: "M14.5 10.5l-3.2-3.2-6.3 6.2" })
6427
+ ]
6428
+ }),
6429
+ (0, react_jsx_runtime.jsx)("span", {
6430
+ className: "dsh-ig-pill-provider",
6431
+ children: PILL_PROVIDER_LABELS[current]
6432
+ }),
6433
+ (0, react_jsx_runtime.jsx)("span", {
6434
+ className: `dsh-ig-pill-caret${open ? " dsh-ig-pill-caret-open" : ""}`,
6435
+ "aria-hidden": "true",
6436
+ children: (0, react_jsx_runtime.jsx)("svg", {
6437
+ width: "10",
6438
+ height: "10",
6439
+ viewBox: "0 0 16 16",
6440
+ fill: "none",
6441
+ stroke: "currentColor",
6442
+ strokeWidth: "2",
6443
+ strokeLinecap: "round",
6444
+ strokeLinejoin: "round",
6445
+ children: (0, react_jsx_runtime.jsx)("path", { d: "M4 10l4-4 4 4" })
6446
+ })
6447
+ })
6448
+ ]
6449
+ }), open && menuStyle !== null ? (0, react_jsx_runtime.jsxs)("div", {
6450
+ className: "dsh-ig-pill-menu",
6451
+ role: "menu",
6452
+ "aria-label": t("menuTitle"),
6453
+ style: menuStyle,
6454
+ children: [
6455
+ (0, react_jsx_runtime.jsx)("div", {
6456
+ className: "dsh-ig-pill-menu-title",
6457
+ children: t("menuTitle")
6458
+ }),
6459
+ IMAGE_PROVIDERS.map((provider) => (0, react_jsx_runtime.jsxs)("button", {
6460
+ type: "button",
6461
+ role: "menuitemradio",
6462
+ "aria-checked": provider === current,
6463
+ className: `dsh-ig-pill-option${provider === current ? " dsh-ig-pill-option-active" : ""}`,
6464
+ disabled: pending !== void 0,
6465
+ onClick: () => {
6466
+ select(provider);
6467
+ },
6468
+ children: [
6469
+ (0, react_jsx_runtime.jsx)("span", {
6470
+ className: `dsh-ig-pill-dot ${dotClassOf(provider)}`,
6471
+ "aria-hidden": "true"
6472
+ }),
6473
+ (0, react_jsx_runtime.jsxs)("span", {
6474
+ className: "dsh-ig-pill-option-text",
6475
+ children: [(0, react_jsx_runtime.jsx)("span", {
6476
+ className: "dsh-ig-pill-option-name",
6477
+ children: PILL_PROVIDER_LABELS[provider]
6478
+ }), (0, react_jsx_runtime.jsx)("span", {
6479
+ className: "dsh-ig-pill-option-sub",
6480
+ children: subtitleOf(provider)
6481
+ })]
6482
+ }),
6483
+ provider === current ? (0, react_jsx_runtime.jsx)("span", {
6484
+ className: "dsh-ig-pill-check",
6485
+ "aria-hidden": "true",
6486
+ children: (0, react_jsx_runtime.jsx)("svg", {
6487
+ width: "13",
6488
+ height: "13",
6489
+ viewBox: "0 0 16 16",
6490
+ fill: "none",
6491
+ stroke: "currentColor",
6492
+ strokeWidth: "2.2",
6493
+ strokeLinecap: "round",
6494
+ strokeLinejoin: "round",
6495
+ children: (0, react_jsx_runtime.jsx)("path", { d: "M2.5 8.5l3.5 3.5 7-8" })
6496
+ })
6497
+ }) : null
6498
+ ]
6499
+ }, provider)),
6500
+ error.length > 0 ? (0, react_jsx_runtime.jsxs)("div", {
6501
+ className: "dsh-ig-pill-error",
6502
+ role: "alert",
6503
+ children: [
6504
+ t("switchFailed"),
6505
+ ":",
6506
+ error
6507
+ ]
6508
+ }) : null
6509
+ ]
6510
+ }) : null]
6511
+ });
6512
+ }
6513
+ /** Styles for the pill and its fixed-position menu; injected by the client entry. */
6514
+ const PROVIDER_PILL_STYLE = `
6515
+ .dsh-ig-pill-root{position:relative;display:inline-flex;align-items:center;height:26px}
6516
+ .dsh-ig-pill-button{display:inline-flex;align-items:center;gap:5px;height:26px;padding:0 8px 0 7px;border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:13px;background:var(--dsw-alias-bg-layer-3,#fff);color:var(--dsw-alias-label-secondary,#4b5563);font:inherit;font-size:12px;line-height:1;cursor:pointer;transition:border-color .15s,background .15s,color .15s;-webkit-user-select:none;user-select:none}
6517
+ .dsh-ig-pill-button:hover:not(:disabled){border-color:var(--dsw-alias-label-dimmed,#9ca3af);color:var(--dsw-alias-label-primary,#111827)}
6518
+ .dsh-ig-pill-button:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4c78ff);outline-offset:1px}
6519
+ .dsh-ig-pill-button:disabled{opacity:.5;cursor:not-allowed}
6520
+ .dsh-ig-pill-button-open{border-color:var(--dsw-alias-brand-primary,#4c78ff);color:var(--dsw-alias-label-primary,#111827)}
6521
+ .dsh-ig-pill-icon{flex:none;color:var(--dsw-alias-brand-primary,#4c78ff)}
6522
+ .dsh-ig-pill-provider{max-width:96px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
6523
+ .dsh-ig-pill-caret{display:inline-flex;flex:none;transition:transform .15s}
6524
+ .dsh-ig-pill-caret-open{transform:rotate(180deg)}
6525
+ .dsh-ig-pill-menu{position:fixed;z-index:1000;min-width:264px;max-width:min(320px,calc(100vw - 24px));padding:6px;border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:12px;background:var(--dsw-alias-bg-layer-3,#fff);box-shadow:0 12px 32px rgba(0,0,0,.16),0 2px 8px rgba(0,0,0,.08);max-height:min(320px,calc(100vh - 48px));overflow-y:auto}
6526
+ .dsh-ig-pill-menu-title{padding:6px 8px 4px;font-size:11px;font-weight:600;color:var(--dsw-alias-label-tertiary,#6b7280)}
6527
+ .dsh-ig-pill-option{display:flex;width:100%;align-items:center;gap:9px;padding:7px 8px;border:0;border-radius:8px;background:none;font:inherit;color:inherit;text-align:left;cursor:pointer}
6528
+ .dsh-ig-pill-option:hover:not(:disabled){background:var(--dsw-alias-bg-layer-2,#f3f4f6)}
6529
+ .dsh-ig-pill-option:disabled{opacity:.6;cursor:default}
6530
+ .dsh-ig-pill-option:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4c78ff);outline-offset:-2px}
6531
+ .dsh-ig-pill-option-active{background:var(--dsw-alias-brand-primary-soft,rgba(76,120,255,.10))}
6532
+ .dsh-ig-pill-dot{flex:none;width:7px;height:7px;border-radius:50%}
6533
+ .dsh-ig-pill-dot-ok{background:#22c55e}
6534
+ .dsh-ig-pill-dot-missing{background:var(--dsw-alias-border-l2,#d1d5db)}
6535
+ .dsh-ig-pill-dot-neutral{background:var(--dsw-alias-brand-primary,#4c78ff)}
6536
+ .dsh-ig-pill-option-text{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
6537
+ .dsh-ig-pill-option-name{font-size:12.5px;font-weight:500;color:var(--dsw-alias-label-primary,#111827);line-height:1.2}
6538
+ .dsh-ig-pill-option-sub{font-size:11px;color:var(--dsw-alias-label-tertiary,#6b7280);line-height:1.2;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
6539
+ .dsh-ig-pill-check{flex:none;color:var(--dsw-alias-brand-primary,#4c78ff)}
6540
+ .dsh-ig-pill-error{margin-top:4px;padding:6px 8px;border-radius:8px;background:rgba(239,68,68,.08);color:#ef4444;font-size:11.5px;line-height:1.4;word-break:break-all}
6541
+ `;
6542
+ //#endregion
6543
+ //#region lib/types/client/index.js
6155
6544
  const DICT = {
6156
6545
  zh: {
6157
6546
  title: "图像生成",
6158
- description: "选择厂商并配置生图模型。",
6159
- provider: "Provider",
6547
+ description: "配置各 Provider 的 Key 与模型,并选择默认 Provider。",
6548
+ defaultProvider: "默认 Provider",
6549
+ defaultProviderHint: "Agent 生图默认使用;Studio 与工具调用可临时指定其他 Provider。",
6550
+ settingsReadOnly: "设置由配置文件提供,只读;如需修改请编辑对应的配置来源。",
6160
6551
  providerGoogle: "Google Gemini",
6161
- providerOpenAI: "OpenAI / 中转站",
6552
+ providerOpenAI: "OpenAI",
6553
+ providerOpenAICompat: "OpenAI 兼容(中转站)",
6162
6554
  providerSeedream: "字节 Seedream",
6163
6555
  providerDashScope: "阿里 DashScope (通义万相 / Qwen)",
6556
+ providerXAI: "xAI Grok Imagine",
6557
+ providerZhipu: "智谱 GLM-Image",
6164
6558
  providerComfyUI: "本地 ComfyUI",
6165
6559
  apiKeyLabel: "{provider} API Key",
6166
6560
  apiKeyPlaceholder: "留空即可保留已配置的 Key",
6167
6561
  apiKeyHint: "安全保存为 {key};页面不会读回明文。",
6562
+ keyReadOnly: "Key 由 {source} 提供且只读;请在该来源中修改。",
6563
+ credentialsUnavailable: "凭据服务不可用,无法保存 API Key;建议升级 DSH 到稳定版本。",
6564
+ badgeUnavailable: "凭据服务不可用",
6565
+ badgeChecking: "检查中…",
6566
+ badgeConfigured: "Key 已配置",
6567
+ badgeMissing: "Key 未配置",
6568
+ badgeUnknown: "状态未知",
6569
+ comfyuiNoKey: "无需 API Key",
6570
+ testConnection: "测试连接",
6571
+ testing: "正在测试…",
6572
+ testOk: "连接成功",
6573
+ testFailed: "连接失败",
6574
+ testUnauthorized: "Key 无效或无权限",
6575
+ clearKey: "清除 Key",
6576
+ keyCleared: "已清除 Key",
6577
+ clearKeyFailed: "清除 Key 失败",
6578
+ saveKeyFailed: "保存 Key 失败",
6579
+ clearKeyUnsupported: "当前版本 DSH 不支持在此清除 Key,请到凭据管理中删除。",
6168
6580
  endpoint: "接口地址",
6169
6581
  reset: "重置",
6170
6582
  resetTitle: "重置为默认官方地址",
6171
6583
  endpointHintGoogle: "Google 官方地址或反代端点(全路径)。",
6172
- endpointHintOpenAI: "中转站请填其 OpenAI 兼容的 /v1 地址。",
6584
+ endpointHintOpenAI: "官方 api.openai.com 的 /v1 地址;中转站请使用下方「OpenAI 兼容」行。",
6585
+ endpointHintOpenAICompat: "中转站/自建服务的 OpenAI 兼容 /v1 地址(必填),例如 https://your-relay.example.com/v1。",
6173
6586
  endpointHintSeedream: "火山方舟兼容的 /api/v3 地址。",
6174
6587
  endpointHintDashScope: "阿里云百炼 DashScope 官方接口地址。",
6588
+ endpointHintXAI: "xAI 官方 api.x.ai 的 /v1 地址。",
6589
+ endpointHintZhipu: "智谱开放平台 open.bigmodel.cn 的 /api/paas/v4 地址。",
6175
6590
  endpointHintComfyUI: "正在运行且 DSH Host 可以访问的 ComfyUI 地址,默认使用本机 8188 端口。",
6176
6591
  model: "模型",
6177
6592
  workflow: "API Workflow 工作流",
@@ -6188,17 +6603,23 @@ window.__ModuleLoader__.load({
6188
6603
  workflowDuplicateName: "工作流名称不能重复。",
6189
6604
  timeout: "生成超时(秒)",
6190
6605
  timeoutHint: "包括提交、等待和下载图片;默认 300 秒。",
6606
+ workspaceSection: "工作区",
6191
6607
  saveToWorkspace: "保存到工作区",
6192
6608
  saveToWorkspaceHint: "每次生成后,把图片文件保存到当前会话工作区。",
6193
6609
  folder: "工作区文件夹",
6194
6610
  folderHint: "相对当前会话工作区的子目录;留空表示工作区根目录。",
6611
+ uiSection: "界面",
6612
+ showPill: "在对话输入栏显示生图切换胶囊",
6613
+ showPillHint: "开启后,输入框工具行会显示一枚胶囊,随手切换默认生图 Provider,无需进入设置;默认关闭。",
6614
+ fetchModels: "拉取模型",
6615
+ fetchingModels: "拉取中…",
6616
+ fetchModelsHint: "点击「拉取模型」用当前 Key 获取可用生图模型列表;也可手动输入。",
6617
+ modelsFound: "找到 {n} 个生图模型,点击模型框选择",
6618
+ modelsNone: "未筛出生图模型,可手动输入模型名",
6195
6619
  saving: "保存中…",
6196
6620
  save: "保存",
6197
6621
  saved: "已保存",
6198
6622
  savedToPath: "已保存到",
6199
- checkingKey: "正在检查 API Key…",
6200
- keyConfigured: "已配置 API Key",
6201
- keyNotConfigured: "尚未配置 API Key",
6202
6623
  generating: "正在生成图片…",
6203
6624
  loading: "正在加载图片…",
6204
6625
  loadFailed: "图片读取失败 ({status})",
@@ -6223,23 +6644,49 @@ window.__ModuleLoader__.load({
6223
6644
  },
6224
6645
  en: {
6225
6646
  title: "Image Generation",
6226
- description: "Select provider and configure image generation models.",
6227
- provider: "Provider",
6647
+ description: "Configure each provider key and model, then pick the default provider.",
6648
+ defaultProvider: "Default provider",
6649
+ defaultProviderHint: "Used by the Agent by default; the Studio and tool calls can switch per call.",
6650
+ settingsReadOnly: "Settings come from a profile file and are read-only; edit that source to change them.",
6228
6651
  providerGoogle: "Google Gemini",
6229
- providerOpenAI: "OpenAI / Relay",
6652
+ providerOpenAI: "OpenAI",
6653
+ providerOpenAICompat: "OpenAI-compatible (relay)",
6230
6654
  providerSeedream: "ByteDance Seedream",
6231
6655
  providerDashScope: "Aliyun DashScope (Wanx / Qwen)",
6656
+ providerXAI: "xAI Grok Imagine",
6657
+ providerZhipu: "Zhipu GLM-Image",
6232
6658
  providerComfyUI: "Local ComfyUI",
6233
6659
  apiKeyLabel: "{provider} API Key",
6234
6660
  apiKeyPlaceholder: "Leave empty to keep configured key",
6235
6661
  apiKeyHint: "Securely saved as {key}; never read back in plaintext.",
6662
+ keyReadOnly: "Key is supplied read-only by {source}; update it there.",
6663
+ credentialsUnavailable: "Credentials service unavailable: API keys cannot be saved; please upgrade DSH to a stable release.",
6664
+ badgeUnavailable: "Credentials unavailable",
6665
+ badgeChecking: "Checking…",
6666
+ badgeConfigured: "Key set",
6667
+ badgeMissing: "Key missing",
6668
+ badgeUnknown: "Unknown",
6669
+ comfyuiNoKey: "No API key needed",
6670
+ testConnection: "Test connection",
6671
+ testing: "Testing…",
6672
+ testOk: "Connection OK",
6673
+ testFailed: "Connection failed",
6674
+ testUnauthorized: "API key rejected",
6675
+ clearKey: "Clear key",
6676
+ keyCleared: "Key cleared",
6677
+ clearKeyFailed: "Failed to clear key",
6678
+ saveKeyFailed: "Failed to save the key",
6679
+ clearKeyUnsupported: "This DSH build cannot clear keys here; remove it from credential management instead.",
6236
6680
  endpoint: "Endpoint / Base URL",
6237
6681
  reset: "Reset",
6238
6682
  resetTitle: "Reset to official default URL",
6239
6683
  endpointHintGoogle: "Official Google endpoint or reverse proxy (full path).",
6240
- endpointHintOpenAI: "OpenAI-compatible /v1 base URL for relays.",
6684
+ endpointHintOpenAI: "Official api.openai.com /v1 base URL; for relays use the \"OpenAI-compatible\" row below.",
6685
+ endpointHintOpenAICompat: "OpenAI-compatible /v1 base URL of your relay or self-hosted service (required), e.g. https://your-relay.example.com/v1.",
6241
6686
  endpointHintSeedream: "Volcengine Ark compatible /api/v3 base URL.",
6242
6687
  endpointHintDashScope: "Official Aliyun DashScope endpoint.",
6688
+ endpointHintXAI: "Official xAI api.x.ai /v1 base URL.",
6689
+ endpointHintZhipu: "Zhipu open.bigmodel.cn /api/paas/v4 base URL.",
6243
6690
  endpointHintComfyUI: "A running ComfyUI server reachable by the DSH Host; the default points to port 8188 on this computer.",
6244
6691
  model: "Model",
6245
6692
  workflow: "API Workflows",
@@ -6256,17 +6703,23 @@ window.__ModuleLoader__.load({
6256
6703
  workflowDuplicateName: "Workflow names must be unique.",
6257
6704
  timeout: "Generation timeout (seconds)",
6258
6705
  timeoutHint: "Covers submission, waiting, and image download; defaults to 300 seconds.",
6706
+ workspaceSection: "Workspace",
6259
6707
  saveToWorkspace: "Save to workspace",
6260
6708
  saveToWorkspaceHint: "Write each generated image as a file into the session workspace.",
6261
6709
  folder: "Workspace folder",
6262
6710
  folderHint: "Subdirectory of the session workspace; empty means the workspace root.",
6711
+ uiSection: "Interface",
6712
+ showPill: "Show the image provider pill in the chat input bar",
6713
+ showPillHint: "Adds a small pill to the composer tool row for switching the default image provider without opening settings; off by default.",
6714
+ fetchModels: "Fetch models",
6715
+ fetchingModels: "Fetching…",
6716
+ fetchModelsHint: "Click \"Fetch models\" to list image-capable models with the stored key; manual input still works.",
6717
+ modelsFound: "{n} image models found; open the model field to pick one",
6718
+ modelsNone: "No image models found; type the model name manually",
6263
6719
  saving: "Saving…",
6264
6720
  save: "Save",
6265
6721
  saved: "Saved",
6266
6722
  savedToPath: "Saved to",
6267
- checkingKey: "Checking API Key…",
6268
- keyConfigured: "API Key configured",
6269
- keyNotConfigured: "API Key not configured",
6270
6723
  generating: "Generating image…",
6271
6724
  loading: "Loading image…",
6272
6725
  loadFailed: "Failed to load image ({status})",
@@ -6322,6 +6775,7 @@ window.__ModuleLoader__.load({
6322
6775
  .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}
6323
6776
  .dsh-ig-btn-reset:hover{background:var(--dsw-alias-bg-layer-2,#edf0f3);border-color:var(--dsw-alias-label-dimmed,#9ca3af)}
6324
6777
  .dsh-ig-hint,.dsh-ig-status{margin:0;color:var(--dsw-alias-label-tertiary,#7b818b);font-size:12px;line-height:1.4}
6778
+ .dsh-ig-hint-error{color:var(--dsw-alias-label-error,#d33)}
6325
6779
  .dsh-ig-status-error{color:var(--dsw-alias-label-error,#d33);font-weight:500}
6326
6780
  .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)}
6327
6781
  .dsh-ig-check-row{display:flex;align-items:center;gap:8px;cursor:pointer}
@@ -6330,6 +6784,45 @@ window.__ModuleLoader__.load({
6330
6784
  .dsh-ig-save{appearance:none;border:0;border-radius:8px;padding:6px 16px;background:var(--dsw-alias-label-primary,#111827);color:var(--dsw-alias-bg-layer-3,#fff);font:inherit;font-size:13px;font-weight:500;cursor:pointer;transition:opacity .15s}
6331
6785
  .dsh-ig-save:disabled{opacity:.4;cursor:default}
6332
6786
 
6787
+ /* Provider list: one expandable row per provider, each saving independently. */
6788
+ .dsh-ig-providers{display:grid;gap:10px;margin-top:14px}
6789
+ .dsh-ig-provider-row{border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:10px;background:var(--dsw-alias-bg-layer-3,transparent);overflow:hidden;transition:border-color .16s}
6790
+ .dsh-ig-provider-row-open{border-color:var(--dsw-alias-label-dimmed,#9ca3af)}
6791
+ .dsh-ig-provider-head{width:100%;appearance:none;border:0;background:none;font:inherit;color:inherit;text-align:left;cursor:pointer;display:flex;align-items:center;gap:10px;padding:11px 12px}
6792
+ .dsh-ig-provider-head:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4c78ff);outline-offset:-2px}
6793
+ .dsh-ig-provider-name{flex:1;min-width:0;font-size:13.5px;font-weight:550;color:var(--dsw-alias-label-primary,inherit)}
6794
+ .dsh-ig-provider-chevron{flex:none;color:var(--dsw-alias-label-tertiary,#7b818b);transition:transform .16s;display:inline-flex;align-items:center}
6795
+ .dsh-ig-provider-chevron-open{transform:rotate(180deg)}
6796
+ .dsh-ig-provider-body{border-top:1px solid var(--dsw-alias-border-l2,#eee);padding:2px 12px 14px}
6797
+ .dsh-ig-badge{flex:none;display:inline-flex;align-items:center;gap:5px;font-size:11.5px;line-height:1.6;padding:2px 9px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2,#d7dbe0);color:var(--dsw-alias-label-secondary,inherit);white-space:nowrap;max-width:60%;overflow:hidden;text-overflow:ellipsis}
6798
+ .dsh-ig-badge-dot{width:6px;height:6px;border-radius:50%;background:currentColor;flex:none}
6799
+ .dsh-ig-badge-ok{border-color:rgba(34,197,94,.45);color:#15803d;background:rgba(34,197,94,.08)}
6800
+ .dsh-ig-badge-missing{border-color:rgba(239,68,68,.4);color:#b91c1c;background:rgba(239,68,68,.06)}
6801
+ .dsh-ig-badge-neutral{color:var(--dsw-alias-label-tertiary,#7b818b)}
6802
+ .dsh-ig-badge-checking .dsh-ig-badge-dot{animation:dsh-ig-pulse 1s ease-in-out infinite}
6803
+ @keyframes dsh-ig-pulse{50%{opacity:.25}}
6804
+
6805
+ /* Default provider radio pills. */
6806
+ .dsh-ig-radios{display:flex;flex-wrap:wrap;gap:8px}
6807
+ .dsh-ig-radio{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--dsw-alias-border-l2,#d7dbe0);border-radius:999px;padding:5px 12px;cursor:pointer;font-size:12.5px;color:var(--dsw-alias-label-secondary,inherit);transition:border-color .15s,background .15s}
6808
+ .dsh-ig-radio:hover{border-color:var(--dsw-alias-label-dimmed,#9ca3af)}
6809
+ .dsh-ig-radio-checked{border-color:var(--dsw-alias-brand-primary,#4c78ff);background:rgba(76,120,255,.08);color:var(--dsw-alias-label-primary,inherit)}
6810
+ .dsh-ig-radio input[type=radio]{width:14px;height:14px;accent-color:var(--dsw-alias-brand-primary,#4c78ff);margin:0;cursor:pointer}
6811
+
6812
+ /* Row-level actions: test connection, clear key, save. */
6813
+ .dsh-ig-row-actions{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:14px;flex-wrap:wrap}
6814
+ .dsh-ig-row-buttons{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
6815
+ .dsh-ig-btn-secondary{appearance:none;border:1px solid var(--dsw-alias-border-l2,#d7dbe0);border-radius:8px;padding:6px 14px;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,opacity .15s}
6816
+ .dsh-ig-btn-secondary:hover:not(:disabled){background:var(--dsw-alias-bg-layer-2,#edf0f3);border-color:var(--dsw-alias-label-dimmed,#9ca3af)}
6817
+ .dsh-ig-btn-secondary:disabled{opacity:.45;cursor:default}
6818
+ .dsh-ig-btn-danger{color:#b91c1c;border-color:rgba(239,68,68,.4)}
6819
+ .dsh-ig-btn-danger:hover:not(:disabled){background:rgba(239,68,68,.08);border-color:rgba(239,68,68,.6)}
6820
+
6821
+ /* Workspace section within the settings card. */
6822
+ .dsh-ig-section{display:grid;gap:6px;margin-top:16px;padding-top:14px;border-top:1px solid var(--dsw-alias-border-l2,#eee)}
6823
+ .dsh-ig-section-title{font-size:12px;font-weight:600;letter-spacing:.02em;color:var(--dsw-alias-label-tertiary,#7b818b);text-transform:uppercase}
6824
+ .dsh-ig-status-readonly{color:var(--dsw-alias-label-tertiary,#7b818b);font-style:italic}
6825
+
6333
6826
  .dsh-ig-result{display:grid;gap:10px;max-width:520px}
6334
6827
  .dsh-ig-promoted-results{display:grid;gap:16px}
6335
6828
  .dsh-ig-result-title{font-size:14px;font-weight:600}
@@ -6551,7 +7044,7 @@ window.__ModuleLoader__.load({
6551
7044
  ctx.effect(() => {
6552
7045
  const style = document.createElement("style");
6553
7046
  style.dataset.plugin = "dsh-image-gen";
6554
- style.textContent = `${STYLE}\n${STUDIO_STYLE}\n${INSPIRATION_STYLE}`;
7047
+ style.textContent = `${STYLE}\n${STUDIO_STYLE}\n${INSPIRATION_STYLE}\n${PROVIDER_PILL_STYLE}`;
6555
7048
  document.head.appendChild(style);
6556
7049
  return () => {
6557
7050
  style.remove();
@@ -6570,26 +7063,80 @@ window.__ModuleLoader__.load({
6570
7063
  inject: () => ({ locale })
6571
7064
  }, PromotedImageResultNode));
6572
7065
  });
6573
- const injectSettingsItem = (owner, credentials) => {
7066
+ const credentialListeners = /* @__PURE__ */ new Set();
7067
+ const notifyCredentialsUpdated = () => {
7068
+ for (const listener of credentialListeners) listener();
7069
+ };
7070
+ ctx.effect(() => {
7071
+ const remote = ctx.get("remote");
7072
+ if (typeof remote?.$on !== "function") return () => {};
7073
+ return remote.$on("credentials/reference-updated", notifyCredentialsUpdated);
7074
+ }, "dsh-image-gen: credential events");
7075
+ const credentialEvents = { listen(callback) {
7076
+ credentialListeners.add(callback);
7077
+ return () => {
7078
+ credentialListeners.delete(callback);
7079
+ };
7080
+ } };
7081
+ const injectSettingsItem = (owner) => {
6574
7082
  const ownerRegister = owner.slots.register.bind(owner.slots);
6575
7083
  owner.slots.inject("settings.plugin.item", () => ownerRegister({
6576
7084
  name: "settings.plugin.item",
6577
7085
  key: IMAGE_GENERATION_NAMESPACE,
6578
7086
  inject: () => ({
6579
7087
  scope,
6580
- credentials,
6581
- locale
7088
+ credentials: credentialsProxy,
7089
+ credentialsAvailable,
7090
+ locale,
7091
+ credentialEvents
6582
7092
  })
6583
7093
  }, ImageGenerationSettingsCard));
6584
7094
  };
6585
- const remoteCredentials = asCredentialsRemote(ctx.get("remote.credentials"));
6586
- const legacyCredentials = credentialsFromLegacyConnection(ctx.get("connection"));
6587
- if (remoteCredentials !== void 0) injectSettingsItem(ctx, remoteCredentials);
6588
- else if (legacyCredentials !== void 0) injectSettingsItem(ctx, legacyCredentials);
6589
- else ctx.inject(["remote.credentials"], (remoteCtx) => {
7095
+ const injectComposerPill = (owner) => {
7096
+ const ownerRegister = owner.slots.register.bind(owner.slots);
7097
+ owner.slots.inject("conversation.input.right", () => ownerRegister({
7098
+ name: "conversation.input.right",
7099
+ id: "image-provider",
7100
+ order: 10,
7101
+ inject: () => ({
7102
+ scope,
7103
+ credentials: credentialsProxy,
7104
+ locale,
7105
+ credentialEvents
7106
+ })
7107
+ }, ImageProviderPill));
7108
+ };
7109
+ const credentialsRef = { current: asCredentialsRemote(ctx.get("remote.credentials")) ?? credentialsFromLegacyConnection(ctx.get("connection")) };
7110
+ /** Stable delegating remote so the host-cached inject face never goes stale. */
7111
+ const credentialsProxy = {
7112
+ describe(refs) {
7113
+ const remote = credentialsRef.current;
7114
+ return remote === void 0 ? Promise.resolve({ ok: false }) : remote.describe(refs);
7115
+ },
7116
+ set(ref, value) {
7117
+ const remote = credentialsRef.current;
7118
+ return remote === void 0 ? Promise.resolve({
7119
+ ok: false,
7120
+ error: { message: "credentials service unavailable" }
7121
+ }) : remote.set(ref, value);
7122
+ },
7123
+ get unset() {
7124
+ const remote = credentialsRef.current;
7125
+ return remote?.unset?.bind(remote);
7126
+ }
7127
+ };
7128
+ /** False while the host core exposes no credentials service (preview cores). */
7129
+ const credentialsAvailable = () => credentialsRef.current !== void 0;
7130
+ injectSettingsItem(ctx);
7131
+ injectComposerPill(ctx);
7132
+ ctx.inject(["remote.credentials"], (remoteCtx) => {
6590
7133
  const credentials = asCredentialsRemote(remoteCtx.get("remote.credentials"));
6591
- if (credentials === void 0) throw new Error("dsh-image-gen: remote.credentials has an incompatible interface");
6592
- injectSettingsItem(remoteCtx, credentials);
7134
+ if (credentials === void 0) {
7135
+ console.warn("dsh-image-gen: remote.credentials resolved with an incompatible interface; key settings stay degraded");
7136
+ return;
7137
+ }
7138
+ credentialsRef.current = credentials;
7139
+ notifyCredentialsUpdated();
6593
7140
  });
6594
7141
  ctx.slots.inject("tool.call.toolview", () => register({
6595
7142
  name: "tool.call.toolview",
@@ -6653,32 +7200,102 @@ window.__ModuleLoader__.load({
6653
7200
  }
6654
7201
  };
6655
7202
  }
6656
- /** Edit provider settings and its write-only API credential. */
7203
+ function emptyProviderRow() {
7204
+ return {
7205
+ expanded: false,
7206
+ model: "",
7207
+ baseURL: "",
7208
+ keyInput: "",
7209
+ keyStatus: "checking",
7210
+ keyInfo: void 0,
7211
+ testing: false,
7212
+ testResult: void 0,
7213
+ fetchingModels: false,
7214
+ modelOptions: [],
7215
+ modelFetchMessage: "",
7216
+ modelFetchIsError: false,
7217
+ saving: false,
7218
+ message: "",
7219
+ messageIsError: false,
7220
+ workflows: [],
7221
+ activeWorkflow: "",
7222
+ timeoutSeconds: DEFAULT_COMFYUI_TIMEOUT_MS / 1e3
7223
+ };
7224
+ }
7225
+ /** Settings field each cloud provider persists its model under. */
7226
+ const CLOUD_MODEL_FIELDS = {
7227
+ google: "googleModel",
7228
+ openai: "openaiModel",
7229
+ "openai-compat": "openaiCompatModel",
7230
+ seedream: "seedreamModel",
7231
+ dashscope: "dashscopeModel",
7232
+ xai: "xaiModel",
7233
+ zhipu: "zhipuModel"
7234
+ };
7235
+ /** Settings field each cloud provider persists its endpoint or base URL under. */
7236
+ const CLOUD_URL_FIELDS = {
7237
+ google: "googleEndpoint",
7238
+ openai: "openaiBaseURL",
7239
+ "openai-compat": "openaiCompatBaseURL",
7240
+ seedream: "seedreamBaseURL",
7241
+ dashscope: "dashscopeEndpoint",
7242
+ xai: "xaiBaseURL",
7243
+ zhipu: "zhipuBaseURL"
7244
+ };
7245
+ /** Dictionary key of each cloud provider's endpoint hint. */
7246
+ const CLOUD_HINT_KEYS = {
7247
+ google: "endpointHintGoogle",
7248
+ openai: "endpointHintOpenAI",
7249
+ "openai-compat": "endpointHintOpenAICompat",
7250
+ seedream: "endpointHintSeedream",
7251
+ dashscope: "endpointHintDashScope",
7252
+ xai: "endpointHintXAI",
7253
+ zhipu: "endpointHintZhipu"
7254
+ };
7255
+ /** Config field a cloud provider persists its model under. */
7256
+ function modelFieldOf(provider) {
7257
+ return CLOUD_MODEL_FIELDS[provider];
7258
+ }
7259
+ /** Config field a cloud provider persists its endpoint or base URL under. */
7260
+ function baseURLFieldOf(provider) {
7261
+ return CLOUD_URL_FIELDS[provider];
7262
+ }
7263
+ /** Providers whose settings row offers the "pull models" button. */
7264
+ function modelPullSupported(_provider) {
7265
+ return true;
7266
+ }
7267
+ /** Build one row per provider from persisted settings, including ComfyUI extras. */
7268
+ function rowsFromSettings(value) {
7269
+ const rows = {};
7270
+ for (const provider of IMAGE_PROVIDERS) rows[provider] = {
7271
+ ...emptyProviderRow(),
7272
+ model: modelOf(provider, value),
7273
+ baseURL: baseURLOf(provider, value),
7274
+ ...provider === "comfyui" ? {
7275
+ workflows: resolveComfyUIWorkflows(value ?? {}),
7276
+ activeWorkflow: activeComfyUIWorkflow(value ?? {})?.name ?? "",
7277
+ timeoutSeconds: Math.max(1, Math.round((value?.comfyuiTimeoutMs ?? 3e5) / 1e3))
7278
+ } : {}
7279
+ };
7280
+ return rows;
7281
+ }
7282
+ /** Edit each provider independently, pick an explicit default, and verify keys inline. */
6657
7283
  function ImageGenerationSettingsCard(props) {
6658
7284
  const [open, setOpen] = (0, react.useState)(false);
6659
7285
  const [snapshot, setSnapshot] = (0, react.useState)(() => props.scope.getSnapshot());
6660
7286
  const [lang, setLang] = (0, react.useState)(() => props.locale?.getSnapshot?.()?.active?.startsWith("en") ? "en" : "zh");
6661
- const [provider, setProvider] = (0, react.useState)("google");
6662
- const [model, setModel] = (0, react.useState)("");
6663
- const [baseURL, setBaseURL] = (0, react.useState)("");
6664
- const [workflows, setWorkflows] = (0, react.useState)([]);
6665
- const [activeWorkflow, setActiveWorkflow] = (0, react.useState)("");
6666
- const [timeoutSeconds, setTimeoutSeconds] = (0, react.useState)(DEFAULT_COMFYUI_TIMEOUT_MS / 1e3);
6667
- const [saveToWorkspace, setSaveToWorkspace] = (0, react.useState)(true);
6668
- const [workspaceFolder, setWorkspaceFolder] = (0, react.useState)("dsh-image-gen");
6669
- const [key, setKey] = (0, react.useState)("");
6670
- const [configured, setConfigured] = (0, react.useState)();
6671
- const [saving, setSaving] = (0, react.useState)(false);
6672
- const [message, setMessage] = (0, react.useState)("");
6673
- const [messageIsError, setMessageIsError] = (0, react.useState)(false);
6674
- const reportMessage = (text) => {
6675
- setMessage(text);
6676
- setMessageIsError(false);
6677
- };
6678
- const reportError = (text) => {
6679
- setMessage(text);
6680
- setMessageIsError(true);
6681
- };
7287
+ const [defaultProvider, setDefaultProvider] = (0, react.useState)(() => props.scope.getSnapshot().value?.provider ?? "google");
7288
+ const [providerMessage, setProviderMessage] = (0, react.useState)("");
7289
+ const [providerMessageIsError, setProviderMessageIsError] = (0, react.useState)(false);
7290
+ const [saveToWorkspace, setSaveToWorkspace] = (0, react.useState)(() => props.scope.getSnapshot().value?.saveToWorkspace ?? true);
7291
+ const [workspaceFolder, setWorkspaceFolder] = (0, react.useState)(() => props.scope.getSnapshot().value?.workspaceFolder ?? "dsh-image-gen");
7292
+ const [showPill, setShowPill] = (0, react.useState)(() => props.scope.getSnapshot().value?.showProviderPill === true);
7293
+ const [uiMessage, setUiMessage] = (0, react.useState)("");
7294
+ const [uiMessageIsError, setUiMessageIsError] = (0, react.useState)(false);
7295
+ const [workspaceMessage, setWorkspaceMessage] = (0, react.useState)("");
7296
+ const [workspaceMessageIsError, setWorkspaceMessageIsError] = (0, react.useState)(false);
7297
+ const [rows, setRows] = (0, react.useState)(() => rowsFromSettings(props.scope.getSnapshot().value));
7298
+ const [keyTick, setKeyTick] = (0, react.useState)(0);
6682
7299
  (0, react.useEffect)(() => props.scope.subscribe(() => {
6683
7300
  setSnapshot(props.scope.getSnapshot());
6684
7301
  }), [props.scope]);
@@ -6687,6 +7304,10 @@ window.__ModuleLoader__.load({
6687
7304
  setLang(props.locale?.getSnapshot?.()?.active?.startsWith("en") ? "en" : "zh");
6688
7305
  });
6689
7306
  }, [props.locale]);
7307
+ const credentialEvents = props.credentialEvents;
7308
+ (0, react.useEffect)(() => credentialEvents?.listen(() => {
7309
+ setKeyTick((tick) => tick + 1);
7310
+ }), [credentialEvents]);
6690
7311
  const t = (keyName, params) => {
6691
7312
  let text = (lang === "en" ? DICT.en : DICT.zh)[keyName] || DICT.zh[keyName] || keyName;
6692
7313
  if (params) for (const [k, v] of Object.entries(params)) text = text.replace(`{${k}}`, v);
@@ -6695,46 +7316,122 @@ window.__ModuleLoader__.load({
6695
7316
  const providerLabels = {
6696
7317
  google: t("providerGoogle"),
6697
7318
  openai: t("providerOpenAI"),
7319
+ "openai-compat": t("providerOpenAICompat"),
6698
7320
  seedream: t("providerSeedream"),
6699
7321
  dashscope: t("providerDashScope"),
7322
+ xai: t("providerXAI"),
7323
+ zhipu: t("providerZhipu"),
6700
7324
  comfyui: t("providerComfyUI")
6701
7325
  };
6702
7326
  (0, react.useEffect)(() => {
6703
7327
  const value = snapshot.value;
6704
- const next = value?.provider ?? "google";
6705
- setProvider(next);
6706
- setModel(modelOf(next, value));
6707
- setBaseURL(baseURLOf(next, value));
6708
- setWorkflows(resolveComfyUIWorkflows(value ?? {}));
6709
- setActiveWorkflow(activeComfyUIWorkflow(value ?? {})?.name ?? "");
6710
- setTimeoutSeconds(Math.max(1, Math.round((value?.comfyuiTimeoutMs ?? 3e5) / 1e3)));
7328
+ setDefaultProvider(value?.provider ?? "google");
6711
7329
  setSaveToWorkspace(value?.saveToWorkspace ?? true);
6712
7330
  setWorkspaceFolder(value?.workspaceFolder ?? "dsh-image-gen");
7331
+ setShowPill(value?.showProviderPill === true);
7332
+ setRows((current) => {
7333
+ const next = {};
7334
+ for (const provider of IMAGE_PROVIDERS) next[provider] = {
7335
+ ...current[provider],
7336
+ model: modelOf(provider, value),
7337
+ baseURL: baseURLOf(provider, value),
7338
+ ...provider === "comfyui" ? {
7339
+ workflows: resolveComfyUIWorkflows(value ?? {}),
7340
+ activeWorkflow: activeComfyUIWorkflow(value ?? {})?.name ?? "",
7341
+ timeoutSeconds: Math.max(1, Math.round((value?.comfyuiTimeoutMs ?? 3e5) / 1e3))
7342
+ } : {}
7343
+ };
7344
+ return next;
7345
+ });
6713
7346
  }, [snapshot]);
7347
+ const credentials = props.credentials;
7348
+ const credentialsAvailable = props.credentialsAvailable;
6714
7349
  (0, react.useEffect)(() => {
6715
- const keyRef = KEY_REF[provider];
6716
- if (keyRef === void 0) {
6717
- setConfigured(void 0);
6718
- return;
6719
- }
6720
7350
  let active = true;
6721
- props.credentials.describe([keyRef]).then((response) => {
6722
- if (active) setConfigured(response.ok ? response.value?.[keyRef]?.configured ?? false : void 0);
6723
- }).catch(() => {
6724
- if (active) setConfigured(void 0);
6725
- });
7351
+ if (!credentialsAvailable()) {
7352
+ setRows((current) => {
7353
+ const next = { ...current };
7354
+ for (const provider of CLOUD_IMAGE_PROVIDERS) next[provider] = {
7355
+ ...current[provider],
7356
+ keyStatus: "unavailable",
7357
+ keyInfo: void 0
7358
+ };
7359
+ return next;
7360
+ });
7361
+ return () => {
7362
+ active = false;
7363
+ };
7364
+ }
7365
+ for (const provider of CLOUD_IMAGE_PROVIDERS) {
7366
+ const keyRef = cloudCredentialRef(provider);
7367
+ if (keyRef === void 0) continue;
7368
+ setRows((current) => ({
7369
+ ...current,
7370
+ [provider]: {
7371
+ ...current[provider],
7372
+ keyStatus: "checking"
7373
+ }
7374
+ }));
7375
+ credentials.describe([keyRef]).then((response) => {
7376
+ if (!active) return;
7377
+ const info = response.ok ? response.value?.[keyRef] : void 0;
7378
+ setRows((current) => ({
7379
+ ...current,
7380
+ [provider]: {
7381
+ ...current[provider],
7382
+ keyStatus: response.ok ? info?.configured ? "configured" : "missing" : "unknown",
7383
+ keyInfo: info
7384
+ }
7385
+ }));
7386
+ }).catch(() => {
7387
+ if (!active) return;
7388
+ setRows((current) => ({
7389
+ ...current,
7390
+ [provider]: {
7391
+ ...current[provider],
7392
+ keyStatus: "unknown"
7393
+ }
7394
+ }));
7395
+ });
7396
+ }
6726
7397
  return () => {
6727
7398
  active = false;
6728
7399
  };
6729
- }, [props.credentials, provider]);
6730
- const save = async (event) => {
6731
- event.preventDefault();
6732
- setSaving(true);
6733
- setMessage("");
7400
+ }, [
7401
+ credentials,
7402
+ credentialsAvailable,
7403
+ keyTick
7404
+ ]);
7405
+ const updateRow = (provider, patch) => {
7406
+ setRows((current) => ({
7407
+ ...current,
7408
+ [provider]: {
7409
+ ...current[provider],
7410
+ ...patch
7411
+ }
7412
+ }));
7413
+ };
7414
+ /** The only action that ever changes the default provider; saving a key never does. */
7415
+ const selectDefaultProvider = (provider) => {
7416
+ setDefaultProvider(provider);
7417
+ setProviderMessage("");
7418
+ setProviderMessageIsError(false);
7419
+ props.scope.set("provider", provider).catch((cause) => {
7420
+ setDefaultProvider(snapshot.value?.provider ?? "google");
7421
+ setProviderMessage(cause instanceof Error ? cause.message : String(cause));
7422
+ setProviderMessageIsError(true);
7423
+ });
7424
+ };
7425
+ const saveProviderRow = async (provider) => {
7426
+ const row = rows[provider];
7427
+ updateRow(provider, {
7428
+ saving: true,
7429
+ message: "",
7430
+ messageIsError: false
7431
+ });
6734
7432
  try {
6735
- await props.scope.set("provider", provider);
6736
7433
  if (provider === "comfyui") {
6737
- const entries = workflows.map((entry) => ({
7434
+ const entries = row.workflows.map((entry) => ({
6738
7435
  name: entry.name.trim(),
6739
7436
  json: entry.json,
6740
7437
  presetPrompt: (entry.presetPrompt ?? "").trim()
@@ -6744,83 +7441,693 @@ window.__ModuleLoader__.load({
6744
7441
  validateComfyUIWorkflowJson(entry.json);
6745
7442
  }
6746
7443
  if (new Set(entries.map((entry) => entry.name)).size !== entries.length) throw new Error(t("workflowDuplicateName"));
6747
- const activeEntry = entries.find((entry) => entry.name === activeWorkflow) ?? entries[0];
6748
- await props.scope.set("comfyuiBaseURL", baseURL);
7444
+ const activeEntry = entries.find((entry) => entry.name === row.activeWorkflow) ?? entries[0];
7445
+ await props.scope.set("comfyuiBaseURL", row.baseURL);
6749
7446
  await props.scope.set("comfyuiWorkflows", entries);
6750
7447
  await props.scope.set("comfyuiActiveWorkflow", activeEntry === void 0 ? "" : activeEntry.name);
6751
7448
  await props.scope.set("comfyuiWorkflowJson", activeEntry === void 0 ? "" : activeEntry.json);
6752
7449
  await props.scope.set("comfyuiWorkflowName", activeEntry === void 0 ? "" : activeEntry.name);
6753
- await props.scope.set("comfyuiTimeoutMs", Math.max(1, Math.round(timeoutSeconds)) * 1e3);
7450
+ await props.scope.set("comfyuiTimeoutMs", Math.max(1, Math.round(row.timeoutSeconds)) * 1e3);
6754
7451
  } else {
6755
- await props.scope.set(provider === "google" ? "googleModel" : provider === "openai" ? "openaiModel" : provider === "seedream" ? "seedreamModel" : "dashscopeModel", model);
6756
- await props.scope.set(provider === "google" ? "googleEndpoint" : provider === "openai" ? "openaiBaseURL" : provider === "seedream" ? "seedreamBaseURL" : "dashscopeEndpoint", baseURL);
6757
- }
6758
- await props.scope.set("saveToWorkspace", saveToWorkspace);
6759
- await props.scope.set("workspaceFolder", workspaceFolder.trim());
6760
- if (key.trim().length > 0) {
6761
- const keyRef = KEY_REF[provider];
6762
- if (keyRef === void 0) throw new Error("ComfyUI does not use an API key in this version");
6763
- const response = await props.credentials.set(keyRef, key.trim());
6764
- if (!response.ok) throw new Error(response.error?.message ?? "Failed to save API key");
6765
- setKey("");
6766
- setConfigured(true);
7452
+ await props.scope.set(modelFieldOf(provider), row.model);
7453
+ await props.scope.set(baseURLFieldOf(provider), row.baseURL);
7454
+ if (row.keyInput.trim().length > 0) {
7455
+ const keyRef = cloudCredentialRef(provider);
7456
+ if (keyRef === void 0) throw new Error(t("comfyuiNoKey"));
7457
+ if (!credentialsAvailable()) throw new Error(t("credentialsUnavailable"));
7458
+ const response = await props.credentials.set(keyRef, row.keyInput.trim());
7459
+ if (!response.ok) throw new Error(response.error?.message ?? t("saveKeyFailed"));
7460
+ updateRow(provider, {
7461
+ keyInput: "",
7462
+ keyStatus: "configured"
7463
+ });
7464
+ }
6767
7465
  }
6768
- reportMessage(t("saved"));
7466
+ updateRow(provider, {
7467
+ message: t("saved"),
7468
+ messageIsError: false
7469
+ });
7470
+ } catch (cause) {
7471
+ updateRow(provider, {
7472
+ message: cause instanceof Error ? cause.message : String(cause),
7473
+ messageIsError: true
7474
+ });
7475
+ } finally {
7476
+ updateRow(provider, { saving: false });
7477
+ }
7478
+ };
7479
+ /** Probe through the host route so the browser side never touches credential values. */
7480
+ const testConnection = async (provider) => {
7481
+ updateRow(provider, {
7482
+ testing: true,
7483
+ testResult: void 0,
7484
+ message: "",
7485
+ messageIsError: false
7486
+ });
7487
+ try {
7488
+ const response = await fetch(TEST_CONNECTION_ROUTE, {
7489
+ method: "POST",
7490
+ headers: { "content-type": "application/json" },
7491
+ body: JSON.stringify({ provider })
7492
+ });
7493
+ if (!response.ok) throw new Error(`HTTP ${String(response.status)}`);
7494
+ updateRow(provider, { testResult: await response.json() });
7495
+ } catch (cause) {
7496
+ updateRow(provider, { testResult: {
7497
+ ok: false,
7498
+ reason: "error",
7499
+ message: cause instanceof Error ? cause.message : String(cause)
7500
+ } });
7501
+ } finally {
7502
+ updateRow(provider, { testing: false });
7503
+ }
7504
+ };
7505
+ /** Pull the provider's image-capable model ids through the host route (Google and the OpenAI family). */
7506
+ const fetchProviderModels = async (provider) => {
7507
+ updateRow(provider, {
7508
+ fetchingModels: true,
7509
+ modelFetchMessage: "",
7510
+ modelFetchIsError: false
7511
+ });
7512
+ try {
7513
+ const response = await fetch(TEST_CONNECTION_ROUTE, {
7514
+ method: "POST",
7515
+ headers: { "content-type": "application/json" },
7516
+ body: JSON.stringify({
7517
+ provider,
7518
+ action: "models"
7519
+ })
7520
+ });
7521
+ if (!response.ok) throw new Error(`HTTP ${String(response.status)}`);
7522
+ const payload = await response.json();
7523
+ if (payload.ok && Array.isArray(payload.models)) {
7524
+ const models = payload.models.filter((id) => typeof id === "string");
7525
+ updateRow(provider, {
7526
+ modelOptions: models,
7527
+ modelFetchMessage: models.length > 0 ? t("modelsFound", { n: String(models.length) }) : t("modelsNone"),
7528
+ modelFetchIsError: models.length === 0
7529
+ });
7530
+ } else if (payload.reason === "missing-key") updateRow(provider, {
7531
+ modelFetchMessage: t("badgeMissing"),
7532
+ modelFetchIsError: true
7533
+ });
7534
+ else if (payload.reason === "unauthorized") updateRow(provider, {
7535
+ modelFetchMessage: t("testUnauthorized"),
7536
+ modelFetchIsError: true
7537
+ });
7538
+ else updateRow(provider, {
7539
+ modelFetchMessage: payload.message ?? t("testFailed"),
7540
+ modelFetchIsError: true
7541
+ });
7542
+ } catch (cause) {
7543
+ updateRow(provider, {
7544
+ modelFetchMessage: cause instanceof Error ? cause.message : String(cause),
7545
+ modelFetchIsError: true
7546
+ });
7547
+ } finally {
7548
+ updateRow(provider, { fetchingModels: false });
7549
+ }
7550
+ };
7551
+ const clearProviderKey = async (provider) => {
7552
+ const keyRef = cloudCredentialRef(provider);
7553
+ if (keyRef === void 0) return;
7554
+ if (typeof props.credentials.unset !== "function") {
7555
+ updateRow(provider, {
7556
+ message: t("clearKeyUnsupported"),
7557
+ messageIsError: true
7558
+ });
7559
+ return;
7560
+ }
7561
+ updateRow(provider, {
7562
+ saving: true,
7563
+ message: "",
7564
+ messageIsError: false
7565
+ });
7566
+ try {
7567
+ const response = await props.credentials.unset(keyRef);
7568
+ if (!response.ok) throw new Error(response.error?.message ?? t("clearKeyFailed"));
7569
+ updateRow(provider, {
7570
+ keyStatus: "missing",
7571
+ keyInput: "",
7572
+ testResult: void 0,
7573
+ message: t("keyCleared"),
7574
+ messageIsError: false
7575
+ });
6769
7576
  } catch (cause) {
6770
- reportError(cause instanceof Error ? cause.message : String(cause));
7577
+ updateRow(provider, {
7578
+ message: cause instanceof Error ? cause.message : String(cause),
7579
+ messageIsError: true
7580
+ });
6771
7581
  } finally {
6772
- setSaving(false);
7582
+ updateRow(provider, { saving: false });
6773
7583
  }
6774
7584
  };
6775
- const keyStatus = configured === void 0 ? t("checkingKey") : configured ? t("keyConfigured") : t("keyNotConfigured");
6776
- const workflowStatus = activeWorkflow.length > 0 ? t("workflowImported", { name: activeWorkflow }) : t("workflowMissing");
7585
+ const testResultText = (result) => {
7586
+ if (result === void 0) return "";
7587
+ if (result.ok) return t("testOk");
7588
+ if (result.reason === "missing-key") return t("badgeMissing");
7589
+ if (result.reason === "unauthorized") return t("testUnauthorized");
7590
+ return `${t("testFailed")}${result.message !== void 0 && result.message.length > 0 ? `: ${result.message}` : ""}`;
7591
+ };
7592
+ const badgeOf = (provider) => {
7593
+ if (provider === "comfyui") return {
7594
+ text: t("comfyuiNoKey"),
7595
+ className: "dsh-ig-badge dsh-ig-badge-neutral"
7596
+ };
7597
+ const status = rows[provider].keyStatus;
7598
+ if (status === "checking") return {
7599
+ text: t("badgeChecking"),
7600
+ className: "dsh-ig-badge dsh-ig-badge-neutral dsh-ig-badge-checking"
7601
+ };
7602
+ if (status === "configured") return {
7603
+ text: t("badgeConfigured"),
7604
+ className: "dsh-ig-badge dsh-ig-badge-ok"
7605
+ };
7606
+ if (status === "missing") return {
7607
+ text: t("badgeMissing"),
7608
+ className: "dsh-ig-badge dsh-ig-badge-missing"
7609
+ };
7610
+ if (status === "unavailable") return {
7611
+ text: t("badgeUnavailable"),
7612
+ className: "dsh-ig-badge dsh-ig-badge-neutral"
7613
+ };
7614
+ return {
7615
+ text: t("badgeUnknown"),
7616
+ className: "dsh-ig-badge dsh-ig-badge-neutral"
7617
+ };
7618
+ };
6777
7619
  const importWorkflow = async (event) => {
6778
7620
  const file = event.target.files?.[0];
6779
7621
  event.target.value = "";
6780
7622
  if (file === void 0) return;
6781
- reportMessage("");
7623
+ updateRow("comfyui", {
7624
+ message: "",
7625
+ messageIsError: false
7626
+ });
6782
7627
  try {
6783
7628
  if (file.size > 5242880) throw new Error(t("workflowTooLarge"));
6784
7629
  const json = await file.text();
6785
7630
  validateComfyUIWorkflowJson(json);
6786
- const name = uniqueComfyUIWorkflowName(file.name, workflows.map((entry) => entry.name));
6787
- setWorkflows((current) => [...current, {
6788
- name,
6789
- json
6790
- }]);
6791
- setActiveWorkflow((current) => current.length > 0 ? current : name);
6792
- reportMessage(t("workflowImported", { name }));
7631
+ const name = uniqueComfyUIWorkflowName(file.name, rows.comfyui.workflows.map((entry) => entry.name));
7632
+ setRows((current) => ({
7633
+ ...current,
7634
+ comfyui: {
7635
+ ...current.comfyui,
7636
+ workflows: [...current.comfyui.workflows, {
7637
+ name,
7638
+ json
7639
+ }],
7640
+ activeWorkflow: current.comfyui.activeWorkflow.length > 0 ? current.comfyui.activeWorkflow : name
7641
+ }
7642
+ }));
7643
+ updateRow("comfyui", {
7644
+ message: t("workflowImported", { name }),
7645
+ messageIsError: false
7646
+ });
6793
7647
  } catch (cause) {
6794
- reportError(cause instanceof Error ? cause.message : String(cause));
7648
+ updateRow("comfyui", {
7649
+ message: cause instanceof Error ? cause.message : String(cause),
7650
+ messageIsError: true
7651
+ });
6795
7652
  }
6796
7653
  };
6797
7654
  /** Renaming the active entry keeps the active selection following its new name. */
6798
7655
  const renameWorkflow = (index, name) => {
6799
- const previous = workflows[index];
6800
- setWorkflows((current) => current.map((entry, position) => position === index ? {
6801
- ...entry,
6802
- name
6803
- } : entry));
6804
- if (previous !== void 0 && previous.name === activeWorkflow) setActiveWorkflow(name);
7656
+ setRows((current) => {
7657
+ const previous = current.comfyui.workflows[index];
7658
+ const workflows = current.comfyui.workflows.map((entry, position) => position === index ? {
7659
+ ...entry,
7660
+ name
7661
+ } : entry);
7662
+ const activeWorkflow = previous !== void 0 && previous.name === current.comfyui.activeWorkflow ? name : current.comfyui.activeWorkflow;
7663
+ return {
7664
+ ...current,
7665
+ comfyui: {
7666
+ ...current.comfyui,
7667
+ workflows,
7668
+ activeWorkflow
7669
+ }
7670
+ };
7671
+ });
6805
7672
  };
6806
7673
  /** Removing the active entry moves the selection to the first remaining workflow. */
6807
7674
  const removeWorkflow = (index) => {
6808
- const previous = workflows[index];
6809
- const next = workflows.filter((_entry, position) => position !== index);
6810
- setWorkflows(next);
6811
- if (previous !== void 0 && previous.name === activeWorkflow) setActiveWorkflow(next[0]?.name ?? "");
7675
+ setRows((current) => {
7676
+ const previous = current.comfyui.workflows[index];
7677
+ const workflows = current.comfyui.workflows.filter((_entry, position) => position !== index);
7678
+ const activeWorkflow = previous !== void 0 && previous.name === current.comfyui.activeWorkflow ? workflows[0]?.name ?? "" : current.comfyui.activeWorkflow;
7679
+ return {
7680
+ ...current,
7681
+ comfyui: {
7682
+ ...current.comfyui,
7683
+ workflows,
7684
+ activeWorkflow
7685
+ }
7686
+ };
7687
+ });
6812
7688
  };
6813
7689
  /** Editing one entry's preset leaves the rest of the entry untouched. */
6814
7690
  const setWorkflowPreset = (index, presetPrompt) => {
6815
- setWorkflows((current) => current.map((entry, position) => position === index ? {
6816
- ...entry,
6817
- presetPrompt
6818
- } : entry));
7691
+ setRows((current) => {
7692
+ const workflows = current.comfyui.workflows.map((entry, position) => position === index ? {
7693
+ ...entry,
7694
+ presetPrompt
7695
+ } : entry);
7696
+ return {
7697
+ ...current,
7698
+ comfyui: {
7699
+ ...current.comfyui,
7700
+ workflows
7701
+ }
7702
+ };
7703
+ });
6819
7704
  };
6820
- return (0, react_jsx_runtime.jsxs)("li", {
6821
- className: `dsh-ig-card ${open ? "dsh-ig-card-open" : ""}`,
6822
- children: [(0, react_jsx_runtime.jsxs)("button", {
6823
- type: "button",
7705
+ /** Workspace checkbox persists immediately, like every other toggle on this card. */
7706
+ const toggleSaveToWorkspace = (next) => {
7707
+ setSaveToWorkspace(next);
7708
+ setWorkspaceMessage("");
7709
+ setWorkspaceMessageIsError(false);
7710
+ props.scope.set("saveToWorkspace", next).catch((cause) => {
7711
+ setSaveToWorkspace(snapshot.value?.saveToWorkspace ?? true);
7712
+ setWorkspaceMessage(cause instanceof Error ? cause.message : String(cause));
7713
+ setWorkspaceMessageIsError(true);
7714
+ });
7715
+ };
7716
+ /** Folder input persists on Enter or blur; no save button needed. */
7717
+ const commitWorkspaceFolder = () => {
7718
+ const next = workspaceFolder.trim();
7719
+ if (next === (snapshot.value?.workspaceFolder ?? "dsh-image-gen")) return;
7720
+ setWorkspaceMessage("");
7721
+ setWorkspaceMessageIsError(false);
7722
+ props.scope.set("workspaceFolder", next).then(() => {
7723
+ setWorkspaceMessage(t("saved"));
7724
+ }).catch((cause) => {
7725
+ setWorkspaceFolder(snapshot.value?.workspaceFolder ?? "dsh-image-gen");
7726
+ setWorkspaceMessage(cause instanceof Error ? cause.message : String(cause));
7727
+ setWorkspaceMessageIsError(true);
7728
+ });
7729
+ };
7730
+ /** The pill toggle saves immediately, like the default-provider radio. */
7731
+ const toggleShowPill = (next) => {
7732
+ setShowPill(next);
7733
+ setUiMessage("");
7734
+ setUiMessageIsError(false);
7735
+ props.scope.set("showProviderPill", next).catch((cause) => {
7736
+ setShowPill(snapshot.value?.showProviderPill === true);
7737
+ setUiMessage(cause instanceof Error ? cause.message : String(cause));
7738
+ setUiMessageIsError(true);
7739
+ });
7740
+ };
7741
+ const renderCloudBody = (provider) => {
7742
+ const row = rows[provider];
7743
+ const keyRef = cloudCredentialRef(provider) ?? "";
7744
+ const keyReadOnly = row.keyInfo?.writable === false;
7745
+ const keyUnavailable = !credentialsAvailable();
7746
+ return (0, react_jsx_runtime.jsx)("div", {
7747
+ className: "dsh-ig-provider-body",
7748
+ children: (0, react_jsx_runtime.jsxs)("form", {
7749
+ onSubmit: (event) => {
7750
+ event.preventDefault();
7751
+ saveProviderRow(provider);
7752
+ },
7753
+ children: [
7754
+ (0, react_jsx_runtime.jsxs)("label", {
7755
+ className: "dsh-ig-field",
7756
+ children: [
7757
+ (0, react_jsx_runtime.jsx)("span", {
7758
+ className: "dsh-ig-label",
7759
+ children: t("apiKeyLabel", { provider: providerLabels[provider] })
7760
+ }),
7761
+ (0, react_jsx_runtime.jsx)("input", {
7762
+ className: "dsh-ig-input",
7763
+ type: "password",
7764
+ autoComplete: "off",
7765
+ value: row.keyInput,
7766
+ onChange: (event) => {
7767
+ updateRow(provider, { keyInput: event.target.value });
7768
+ },
7769
+ placeholder: row.keyStatus === "configured" ? t("apiKeyPlaceholder") : "",
7770
+ disabled: !snapshot.writable || keyReadOnly || keyUnavailable
7771
+ }),
7772
+ (0, react_jsx_runtime.jsx)("span", {
7773
+ className: "dsh-ig-hint",
7774
+ children: keyUnavailable ? t("credentialsUnavailable") : keyReadOnly ? t("keyReadOnly", { source: row.keyInfo?.source ?? "" }) : t("apiKeyHint", { key: keyRef })
7775
+ })
7776
+ ]
7777
+ }),
7778
+ (0, react_jsx_runtime.jsxs)("label", {
7779
+ className: "dsh-ig-field",
7780
+ children: [
7781
+ (0, react_jsx_runtime.jsx)("span", {
7782
+ className: "dsh-ig-label",
7783
+ children: t("endpoint")
7784
+ }),
7785
+ (0, react_jsx_runtime.jsxs)("div", {
7786
+ className: "dsh-ig-input-group",
7787
+ children: [(0, react_jsx_runtime.jsx)("input", {
7788
+ className: "dsh-ig-input",
7789
+ type: "url",
7790
+ value: row.baseURL,
7791
+ onChange: (event) => {
7792
+ updateRow(provider, { baseURL: event.target.value });
7793
+ },
7794
+ required: true,
7795
+ disabled: !snapshot.writable
7796
+ }), provider !== "openai-compat" ? (0, react_jsx_runtime.jsx)("button", {
7797
+ type: "button",
7798
+ className: "dsh-ig-btn-reset",
7799
+ title: t("resetTitle"),
7800
+ onClick: () => {
7801
+ updateRow(provider, { baseURL: DEFAULT_BASE_URLS[provider] });
7802
+ },
7803
+ disabled: !snapshot.writable,
7804
+ children: t("reset")
7805
+ }) : null]
7806
+ }),
7807
+ (0, react_jsx_runtime.jsx)("span", {
7808
+ className: "dsh-ig-hint",
7809
+ children: t(CLOUD_HINT_KEYS[provider])
7810
+ })
7811
+ ]
7812
+ }),
7813
+ (0, react_jsx_runtime.jsxs)("label", {
7814
+ className: "dsh-ig-field",
7815
+ children: [
7816
+ (0, react_jsx_runtime.jsx)("span", {
7817
+ className: "dsh-ig-label",
7818
+ children: t("model")
7819
+ }),
7820
+ modelPullSupported(provider) ? (0, react_jsx_runtime.jsxs)("div", {
7821
+ className: "dsh-ig-input-group",
7822
+ children: [
7823
+ (0, react_jsx_runtime.jsx)("input", {
7824
+ className: "dsh-ig-input",
7825
+ value: row.model,
7826
+ onChange: (event) => {
7827
+ updateRow(provider, { model: event.target.value });
7828
+ },
7829
+ list: `dsh-ig-${provider}-model-options`,
7830
+ required: provider !== "openai-compat",
7831
+ disabled: !snapshot.writable
7832
+ }),
7833
+ (0, react_jsx_runtime.jsx)("datalist", {
7834
+ id: `dsh-ig-${provider}-model-options`,
7835
+ children: row.modelOptions.map((id) => (0, react_jsx_runtime.jsx)("option", { value: id }, id))
7836
+ }),
7837
+ (0, react_jsx_runtime.jsx)("button", {
7838
+ type: "button",
7839
+ className: "dsh-ig-btn-secondary",
7840
+ disabled: row.fetchingModels || !snapshot.writable,
7841
+ onClick: () => {
7842
+ fetchProviderModels(provider);
7843
+ },
7844
+ children: row.fetchingModels ? t("fetchingModels") : t("fetchModels")
7845
+ })
7846
+ ]
7847
+ }) : (0, react_jsx_runtime.jsx)("input", {
7848
+ className: "dsh-ig-input",
7849
+ value: row.model,
7850
+ onChange: (event) => {
7851
+ updateRow(provider, { model: event.target.value });
7852
+ },
7853
+ required: true,
7854
+ disabled: !snapshot.writable
7855
+ }),
7856
+ modelPullSupported(provider) ? row.modelFetchMessage.length > 0 ? (0, react_jsx_runtime.jsx)("span", {
7857
+ className: `dsh-ig-hint${row.modelFetchIsError ? " dsh-ig-hint-error" : ""}`,
7858
+ role: "status",
7859
+ children: row.modelFetchMessage
7860
+ }) : (0, react_jsx_runtime.jsx)("span", {
7861
+ className: "dsh-ig-hint",
7862
+ children: t("fetchModelsHint")
7863
+ }) : null
7864
+ ]
7865
+ }),
7866
+ (0, react_jsx_runtime.jsxs)("div", {
7867
+ className: "dsh-ig-row-actions",
7868
+ children: [(0, react_jsx_runtime.jsx)("p", {
7869
+ className: `dsh-ig-status${row.messageIsError ? " dsh-ig-status-error" : ""}`,
7870
+ role: "status",
7871
+ children: row.message || testResultText(row.testResult)
7872
+ }), (0, react_jsx_runtime.jsxs)("span", {
7873
+ className: "dsh-ig-row-buttons",
7874
+ children: [
7875
+ (0, react_jsx_runtime.jsx)("button", {
7876
+ type: "button",
7877
+ className: "dsh-ig-btn-secondary",
7878
+ disabled: row.testing,
7879
+ onClick: () => {
7880
+ testConnection(provider);
7881
+ },
7882
+ children: row.testing ? t("testing") : t("testConnection")
7883
+ }),
7884
+ row.keyStatus === "configured" && !keyReadOnly ? (0, react_jsx_runtime.jsx)("button", {
7885
+ type: "button",
7886
+ className: "dsh-ig-btn-secondary dsh-ig-btn-danger",
7887
+ disabled: row.saving,
7888
+ onClick: () => {
7889
+ clearProviderKey(provider);
7890
+ },
7891
+ children: t("clearKey")
7892
+ }) : null,
7893
+ (0, react_jsx_runtime.jsx)("button", {
7894
+ className: "dsh-ig-save",
7895
+ type: "submit",
7896
+ disabled: row.saving || !snapshot.writable,
7897
+ children: row.saving ? t("saving") : t("save")
7898
+ })
7899
+ ]
7900
+ })]
7901
+ })
7902
+ ]
7903
+ })
7904
+ });
7905
+ };
7906
+ const renderComfyUIBody = () => {
7907
+ const row = rows.comfyui;
7908
+ return (0, react_jsx_runtime.jsx)("div", {
7909
+ className: "dsh-ig-provider-body",
7910
+ children: (0, react_jsx_runtime.jsxs)("form", {
7911
+ onSubmit: (event) => {
7912
+ event.preventDefault();
7913
+ saveProviderRow("comfyui");
7914
+ },
7915
+ children: [
7916
+ (0, react_jsx_runtime.jsxs)("label", {
7917
+ className: "dsh-ig-field",
7918
+ children: [
7919
+ (0, react_jsx_runtime.jsx)("span", {
7920
+ className: "dsh-ig-label",
7921
+ children: t("endpoint")
7922
+ }),
7923
+ (0, react_jsx_runtime.jsxs)("div", {
7924
+ className: "dsh-ig-input-group",
7925
+ children: [(0, react_jsx_runtime.jsx)("input", {
7926
+ className: "dsh-ig-input",
7927
+ type: "url",
7928
+ value: row.baseURL,
7929
+ onChange: (event) => {
7930
+ updateRow("comfyui", { baseURL: event.target.value });
7931
+ },
7932
+ required: true,
7933
+ disabled: !snapshot.writable
7934
+ }), (0, react_jsx_runtime.jsx)("button", {
7935
+ type: "button",
7936
+ className: "dsh-ig-btn-reset",
7937
+ title: t("resetTitle"),
7938
+ onClick: () => {
7939
+ updateRow("comfyui", { baseURL: DEFAULT_BASE_URLS.comfyui });
7940
+ },
7941
+ disabled: !snapshot.writable,
7942
+ children: t("reset")
7943
+ })]
7944
+ }),
7945
+ (0, react_jsx_runtime.jsx)("span", {
7946
+ className: "dsh-ig-hint",
7947
+ children: t("endpointHintComfyUI")
7948
+ })
7949
+ ]
7950
+ }),
7951
+ (0, react_jsx_runtime.jsxs)("div", {
7952
+ className: "dsh-ig-field",
7953
+ children: [
7954
+ (0, react_jsx_runtime.jsx)("span", {
7955
+ className: "dsh-ig-label",
7956
+ children: t("workflow")
7957
+ }),
7958
+ (0, react_jsx_runtime.jsxs)("div", {
7959
+ className: "dsh-ig-file-row",
7960
+ children: [(0, react_jsx_runtime.jsxs)("label", {
7961
+ className: "dsh-ig-file-button",
7962
+ children: [(0, react_jsx_runtime.jsx)("input", {
7963
+ className: "dsh-ig-file-input",
7964
+ type: "file",
7965
+ accept: ".json,application/json",
7966
+ onChange: (event) => {
7967
+ importWorkflow(event);
7968
+ }
7969
+ }), t("workflowImport")]
7970
+ }), row.workflows.length === 0 ? (0, react_jsx_runtime.jsx)("span", {
7971
+ className: "dsh-ig-file-name",
7972
+ children: t("workflowMissing")
7973
+ }) : null]
7974
+ }),
7975
+ row.workflows.length > 0 ? (0, react_jsx_runtime.jsx)("ul", {
7976
+ className: "dsh-ig-workflow-list",
7977
+ children: row.workflows.map((entry, index) => (0, react_jsx_runtime.jsxs)("li", {
7978
+ className: "dsh-ig-workflow-row",
7979
+ children: [(0, react_jsx_runtime.jsxs)("div", {
7980
+ className: "dsh-ig-workflow-main",
7981
+ children: [
7982
+ (0, react_jsx_runtime.jsx)("label", {
7983
+ className: "dsh-ig-workflow-active",
7984
+ title: t("workflowActiveTitle"),
7985
+ children: (0, react_jsx_runtime.jsx)("input", {
7986
+ type: "radio",
7987
+ name: "dsh-ig-active-workflow",
7988
+ "aria-label": t("workflowActiveTitle"),
7989
+ checked: entry.name === row.activeWorkflow,
7990
+ onChange: () => {
7991
+ updateRow("comfyui", { activeWorkflow: entry.name });
7992
+ }
7993
+ })
7994
+ }),
7995
+ (0, react_jsx_runtime.jsx)("input", {
7996
+ className: "dsh-ig-input dsh-ig-workflow-name",
7997
+ value: entry.name,
7998
+ title: entry.name,
7999
+ onChange: (event) => {
8000
+ renameWorkflow(index, event.target.value);
8001
+ }
8002
+ }),
8003
+ (0, react_jsx_runtime.jsx)("button", {
8004
+ type: "button",
8005
+ className: "dsh-ig-btn-reset",
8006
+ onClick: () => {
8007
+ removeWorkflow(index);
8008
+ },
8009
+ children: t("workflowRemove")
8010
+ })
8011
+ ]
8012
+ }), (0, react_jsx_runtime.jsx)("input", {
8013
+ className: "dsh-ig-input dsh-ig-workflow-preset",
8014
+ value: entry.presetPrompt ?? "",
8015
+ placeholder: t("workflowPresetPlaceholder"),
8016
+ title: t("workflowPresetTitle"),
8017
+ onChange: (event) => {
8018
+ setWorkflowPreset(index, event.target.value);
8019
+ }
8020
+ })]
8021
+ }, String(index)))
8022
+ }) : null,
8023
+ (0, react_jsx_runtime.jsx)("span", {
8024
+ className: "dsh-ig-hint",
8025
+ children: t("workflowHint")
8026
+ })
8027
+ ]
8028
+ }),
8029
+ (0, react_jsx_runtime.jsxs)("label", {
8030
+ className: "dsh-ig-field",
8031
+ children: [
8032
+ (0, react_jsx_runtime.jsx)("span", {
8033
+ className: "dsh-ig-label",
8034
+ children: t("timeout")
8035
+ }),
8036
+ (0, react_jsx_runtime.jsx)("input", {
8037
+ className: "dsh-ig-input",
8038
+ type: "number",
8039
+ min: "1",
8040
+ max: "3600",
8041
+ step: "1",
8042
+ value: row.timeoutSeconds,
8043
+ onChange: (event) => {
8044
+ updateRow("comfyui", { timeoutSeconds: Number(event.target.value) });
8045
+ },
8046
+ required: true,
8047
+ disabled: !snapshot.writable
8048
+ }),
8049
+ (0, react_jsx_runtime.jsx)("span", {
8050
+ className: "dsh-ig-hint",
8051
+ children: t("timeoutHint")
8052
+ })
8053
+ ]
8054
+ }),
8055
+ (0, react_jsx_runtime.jsxs)("div", {
8056
+ className: "dsh-ig-row-actions",
8057
+ children: [(0, react_jsx_runtime.jsx)("p", {
8058
+ className: `dsh-ig-status${row.messageIsError ? " dsh-ig-status-error" : ""}`,
8059
+ role: "status",
8060
+ children: row.message || testResultText(row.testResult)
8061
+ }), (0, react_jsx_runtime.jsxs)("span", {
8062
+ className: "dsh-ig-row-buttons",
8063
+ children: [(0, react_jsx_runtime.jsx)("button", {
8064
+ type: "button",
8065
+ className: "dsh-ig-btn-secondary",
8066
+ disabled: row.testing,
8067
+ onClick: () => {
8068
+ testConnection("comfyui");
8069
+ },
8070
+ children: row.testing ? t("testing") : t("testConnection")
8071
+ }), (0, react_jsx_runtime.jsx)("button", {
8072
+ className: "dsh-ig-save",
8073
+ type: "submit",
8074
+ disabled: row.saving || !snapshot.writable || row.workflows.length === 0,
8075
+ children: row.saving ? t("saving") : t("save")
8076
+ })]
8077
+ })]
8078
+ })
8079
+ ]
8080
+ })
8081
+ });
8082
+ };
8083
+ const renderProviderRow = (provider) => {
8084
+ const row = rows[provider];
8085
+ const badge = badgeOf(provider);
8086
+ return (0, react_jsx_runtime.jsxs)("div", {
8087
+ className: `dsh-ig-provider-row ${row.expanded ? "dsh-ig-provider-row-open" : ""}`,
8088
+ children: [(0, react_jsx_runtime.jsxs)("button", {
8089
+ type: "button",
8090
+ className: "dsh-ig-provider-head",
8091
+ "aria-expanded": row.expanded,
8092
+ onClick: () => {
8093
+ updateRow(provider, { expanded: !row.expanded });
8094
+ },
8095
+ children: [
8096
+ (0, react_jsx_runtime.jsx)("span", {
8097
+ className: "dsh-ig-provider-name",
8098
+ children: providerLabels[provider]
8099
+ }),
8100
+ (0, react_jsx_runtime.jsxs)("span", {
8101
+ className: badge.className,
8102
+ title: badge.text,
8103
+ children: [(0, react_jsx_runtime.jsx)("span", {
8104
+ className: "dsh-ig-badge-dot",
8105
+ "aria-hidden": "true"
8106
+ }), badge.text]
8107
+ }),
8108
+ (0, react_jsx_runtime.jsx)("span", {
8109
+ className: `dsh-ig-provider-chevron ${row.expanded ? "dsh-ig-provider-chevron-open" : ""}`,
8110
+ "aria-hidden": "true",
8111
+ children: (0, react_jsx_runtime.jsx)("svg", {
8112
+ width: "12",
8113
+ height: "12",
8114
+ viewBox: "0 0 16 16",
8115
+ fill: "none",
8116
+ stroke: "currentColor",
8117
+ strokeWidth: "2",
8118
+ strokeLinecap: "round",
8119
+ strokeLinejoin: "round",
8120
+ children: (0, react_jsx_runtime.jsx)("path", { d: "M4 6l4 4 4-4" })
8121
+ })
8122
+ })
8123
+ ]
8124
+ }), row.expanded ? provider === "comfyui" ? renderComfyUIBody() : renderCloudBody(provider) : null]
8125
+ }, provider);
8126
+ };
8127
+ return (0, react_jsx_runtime.jsxs)("li", {
8128
+ className: `dsh-ig-card ${open ? "dsh-ig-card-open" : ""}`,
8129
+ children: [(0, react_jsx_runtime.jsxs)("button", {
8130
+ type: "button",
6824
8131
  className: "dsh-ig-head",
6825
8132
  "aria-expanded": open,
6826
8133
  onClick: () => {
@@ -6850,280 +8157,147 @@ window.__ModuleLoader__.load({
6850
8157
  children: (0, react_jsx_runtime.jsx)("path", { d: "M4 6l4 4 4-4" })
6851
8158
  })
6852
8159
  })]
6853
- }), open ? (0, react_jsx_runtime.jsxs)("form", {
8160
+ }), open ? (0, react_jsx_runtime.jsxs)("div", {
6854
8161
  className: "dsh-ig-body",
6855
- onSubmit: (event) => {
6856
- save(event);
6857
- },
6858
8162
  children: [
6859
- (0, react_jsx_runtime.jsxs)("label", {
6860
- className: "dsh-ig-field",
6861
- children: [
6862
- (0, react_jsx_runtime.jsx)("span", {
6863
- className: "dsh-ig-label",
6864
- children: t("provider")
6865
- }),
6866
- (0, react_jsx_runtime.jsxs)("select", {
6867
- className: "dsh-ig-input",
6868
- value: provider,
6869
- onChange: (event) => {
6870
- const next = event.target.value;
6871
- setProvider(next);
6872
- setModel(modelOf(next, snapshot.value));
6873
- setBaseURL(baseURLOf(next, snapshot.value));
6874
- setKey("");
6875
- },
6876
- children: [
6877
- (0, react_jsx_runtime.jsx)("option", {
6878
- value: "google",
6879
- children: t("providerGoogle")
6880
- }),
6881
- (0, react_jsx_runtime.jsx)("option", {
6882
- value: "openai",
6883
- children: t("providerOpenAI")
6884
- }),
6885
- (0, react_jsx_runtime.jsx)("option", {
6886
- value: "seedream",
6887
- children: t("providerSeedream")
6888
- }),
6889
- (0, react_jsx_runtime.jsx)("option", {
6890
- value: "dashscope",
6891
- children: t("providerDashScope")
6892
- }),
6893
- (0, react_jsx_runtime.jsx)("option", {
6894
- value: "comfyui",
6895
- children: t("providerComfyUI")
6896
- })
6897
- ]
6898
- }),
6899
- (0, react_jsx_runtime.jsx)("span", {
6900
- className: "dsh-ig-hint",
6901
- children: providerLabels[provider]
6902
- })
6903
- ]
6904
- }),
6905
- provider !== "comfyui" ? (0, react_jsx_runtime.jsxs)("label", {
6906
- className: "dsh-ig-field",
6907
- children: [
6908
- (0, react_jsx_runtime.jsx)("span", {
6909
- className: "dsh-ig-label",
6910
- children: t("apiKeyLabel", { provider: providerLabels[provider] })
6911
- }),
6912
- (0, react_jsx_runtime.jsx)("input", {
6913
- className: "dsh-ig-input",
6914
- type: "password",
6915
- autoComplete: "off",
6916
- value: key,
6917
- onChange: (event) => {
6918
- setKey(event.target.value);
6919
- },
6920
- placeholder: configured ? t("apiKeyPlaceholder") : ""
6921
- }),
6922
- (0, react_jsx_runtime.jsx)("span", {
6923
- className: "dsh-ig-hint",
6924
- children: t("apiKeyHint", { key: KEY_REF[provider] ?? "" })
6925
- })
6926
- ]
8163
+ !snapshot.writable ? (0, react_jsx_runtime.jsx)("p", {
8164
+ className: "dsh-ig-status dsh-ig-status-readonly",
8165
+ role: "note",
8166
+ children: t("settingsReadOnly")
6927
8167
  }) : null,
6928
- (0, react_jsx_runtime.jsxs)("label", {
8168
+ (0, react_jsx_runtime.jsxs)("div", {
6929
8169
  className: "dsh-ig-field",
6930
8170
  children: [
6931
8171
  (0, react_jsx_runtime.jsx)("span", {
6932
8172
  className: "dsh-ig-label",
6933
- children: t("endpoint")
8173
+ children: t("defaultProvider")
6934
8174
  }),
6935
- (0, react_jsx_runtime.jsxs)("div", {
6936
- className: "dsh-ig-input-group",
6937
- children: [(0, react_jsx_runtime.jsx)("input", {
6938
- className: "dsh-ig-input",
6939
- type: "url",
6940
- value: baseURL,
6941
- onChange: (event) => {
6942
- setBaseURL(event.target.value);
6943
- },
6944
- required: true
6945
- }), (0, react_jsx_runtime.jsx)("button", {
6946
- type: "button",
6947
- className: "dsh-ig-btn-reset",
6948
- title: t("resetTitle"),
6949
- onClick: () => {
6950
- setBaseURL(DEFAULT_BASE_URLS[provider]);
6951
- },
6952
- children: t("reset")
6953
- })]
8175
+ (0, react_jsx_runtime.jsx)("div", {
8176
+ className: "dsh-ig-radios",
8177
+ role: "radiogroup",
8178
+ "aria-label": t("defaultProvider"),
8179
+ children: IMAGE_PROVIDERS.map((provider) => (0, react_jsx_runtime.jsxs)("label", {
8180
+ className: `dsh-ig-radio${defaultProvider === provider ? " dsh-ig-radio-checked" : ""}`,
8181
+ children: [(0, react_jsx_runtime.jsx)("input", {
8182
+ type: "radio",
8183
+ name: "dsh-ig-default-provider",
8184
+ checked: defaultProvider === provider,
8185
+ onChange: () => {
8186
+ selectDefaultProvider(provider);
8187
+ },
8188
+ disabled: !snapshot.writable
8189
+ }), providerLabels[provider]]
8190
+ }, provider))
6954
8191
  }),
6955
8192
  (0, react_jsx_runtime.jsx)("span", {
6956
8193
  className: "dsh-ig-hint",
6957
- children: provider === "google" ? t("endpointHintGoogle") : provider === "openai" ? t("endpointHintOpenAI") : provider === "seedream" ? t("endpointHintSeedream") : provider === "dashscope" ? t("endpointHintDashScope") : t("endpointHintComfyUI")
6958
- })
8194
+ children: t("defaultProviderHint")
8195
+ }),
8196
+ providerMessage.length > 0 ? (0, react_jsx_runtime.jsx)("span", {
8197
+ className: `dsh-ig-status${providerMessageIsError ? " dsh-ig-status-error" : ""}`,
8198
+ role: "status",
8199
+ children: providerMessage
8200
+ }) : null
6959
8201
  ]
6960
8202
  }),
6961
- provider !== "comfyui" ? (0, react_jsx_runtime.jsxs)("label", {
6962
- className: "dsh-ig-field",
6963
- children: [(0, react_jsx_runtime.jsx)("span", {
6964
- className: "dsh-ig-label",
6965
- children: t("model")
6966
- }), (0, react_jsx_runtime.jsx)("input", {
6967
- className: "dsh-ig-input",
6968
- value: model,
6969
- onChange: (event) => {
6970
- setModel(event.target.value);
6971
- },
6972
- required: true
6973
- })]
6974
- }) : (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsxs)("div", {
6975
- className: "dsh-ig-field",
8203
+ (0, react_jsx_runtime.jsx)("div", {
8204
+ className: "dsh-ig-providers",
8205
+ children: IMAGE_PROVIDERS.map((provider) => renderProviderRow(provider))
8206
+ }),
8207
+ (0, react_jsx_runtime.jsxs)("div", {
8208
+ className: "dsh-ig-section",
6976
8209
  children: [
6977
8210
  (0, react_jsx_runtime.jsx)("span", {
6978
- className: "dsh-ig-label",
6979
- children: t("workflow")
8211
+ className: "dsh-ig-section-title",
8212
+ children: t("workspaceSection")
6980
8213
  }),
6981
8214
  (0, react_jsx_runtime.jsxs)("div", {
6982
- className: "dsh-ig-file-row",
8215
+ className: "dsh-ig-field",
6983
8216
  children: [(0, react_jsx_runtime.jsxs)("label", {
6984
- className: "dsh-ig-file-button",
8217
+ className: "dsh-ig-check-row",
6985
8218
  children: [(0, react_jsx_runtime.jsx)("input", {
6986
- className: "dsh-ig-file-input",
6987
- type: "file",
6988
- accept: ".json,application/json",
8219
+ type: "checkbox",
8220
+ checked: saveToWorkspace,
6989
8221
  onChange: (event) => {
6990
- importWorkflow(event);
6991
- }
6992
- }), t("workflowImport")]
6993
- }), workflows.length === 0 ? (0, react_jsx_runtime.jsx)("span", {
6994
- className: "dsh-ig-file-name",
6995
- children: t("workflowMissing")
6996
- }) : null]
8222
+ toggleSaveToWorkspace(event.target.checked);
8223
+ },
8224
+ disabled: !snapshot.writable
8225
+ }), (0, react_jsx_runtime.jsx)("span", {
8226
+ className: "dsh-ig-label",
8227
+ children: t("saveToWorkspace")
8228
+ })]
8229
+ }), (0, react_jsx_runtime.jsx)("span", {
8230
+ className: "dsh-ig-hint",
8231
+ children: t("saveToWorkspaceHint")
8232
+ })]
6997
8233
  }),
6998
- workflows.length > 0 ? (0, react_jsx_runtime.jsx)("ul", {
6999
- className: "dsh-ig-workflow-list",
7000
- children: workflows.map((entry, index) => (0, react_jsx_runtime.jsxs)("li", {
7001
- className: "dsh-ig-workflow-row",
7002
- children: [(0, react_jsx_runtime.jsxs)("div", {
7003
- className: "dsh-ig-workflow-main",
7004
- children: [
7005
- (0, react_jsx_runtime.jsx)("label", {
7006
- className: "dsh-ig-workflow-active",
7007
- title: t("workflowActiveTitle"),
7008
- children: (0, react_jsx_runtime.jsx)("input", {
7009
- type: "radio",
7010
- name: "dsh-ig-active-workflow",
7011
- "aria-label": t("workflowActiveTitle"),
7012
- checked: entry.name === activeWorkflow,
7013
- onChange: () => {
7014
- setActiveWorkflow(entry.name);
7015
- }
7016
- })
7017
- }),
7018
- (0, react_jsx_runtime.jsx)("input", {
7019
- className: "dsh-ig-input dsh-ig-workflow-name",
7020
- value: entry.name,
7021
- title: entry.name,
7022
- onChange: (event) => {
7023
- renameWorkflow(index, event.target.value);
7024
- }
7025
- }),
7026
- (0, react_jsx_runtime.jsx)("button", {
7027
- type: "button",
7028
- className: "dsh-ig-btn-reset",
7029
- onClick: () => {
7030
- removeWorkflow(index);
7031
- },
7032
- children: t("workflowRemove")
7033
- })
7034
- ]
7035
- }), (0, react_jsx_runtime.jsx)("input", {
7036
- className: "dsh-ig-input dsh-ig-workflow-preset",
7037
- value: entry.presetPrompt ?? "",
7038
- placeholder: t("workflowPresetPlaceholder"),
7039
- title: t("workflowPresetTitle"),
8234
+ saveToWorkspace ? (0, react_jsx_runtime.jsxs)("label", {
8235
+ className: "dsh-ig-field",
8236
+ children: [
8237
+ (0, react_jsx_runtime.jsx)("span", {
8238
+ className: "dsh-ig-label",
8239
+ children: t("folder")
8240
+ }),
8241
+ (0, react_jsx_runtime.jsx)("input", {
8242
+ className: "dsh-ig-input",
8243
+ value: workspaceFolder,
7040
8244
  onChange: (event) => {
7041
- setWorkflowPreset(index, event.target.value);
7042
- }
7043
- })]
7044
- }, String(index)))
8245
+ setWorkspaceFolder(event.target.value);
8246
+ },
8247
+ onBlur: () => {
8248
+ commitWorkspaceFolder();
8249
+ },
8250
+ onKeyDown: (event) => {
8251
+ if (event.key === "Enter") commitWorkspaceFolder();
8252
+ },
8253
+ placeholder: "dsh-image-gen",
8254
+ disabled: !snapshot.writable
8255
+ }),
8256
+ (0, react_jsx_runtime.jsx)("span", {
8257
+ className: "dsh-ig-hint",
8258
+ children: t("folderHint")
8259
+ })
8260
+ ]
7045
8261
  }) : null,
7046
- (0, react_jsx_runtime.jsx)("span", {
7047
- className: "dsh-ig-hint",
7048
- children: t("workflowHint")
7049
- })
7050
- ]
7051
- }), (0, react_jsx_runtime.jsxs)("label", {
7052
- className: "dsh-ig-field",
7053
- children: [
7054
- (0, react_jsx_runtime.jsx)("span", {
7055
- className: "dsh-ig-label",
7056
- children: t("timeout")
7057
- }),
7058
- (0, react_jsx_runtime.jsx)("input", {
7059
- className: "dsh-ig-input",
7060
- type: "number",
7061
- min: "1",
7062
- max: "3600",
7063
- step: "1",
7064
- value: timeoutSeconds,
7065
- onChange: (event) => {
7066
- setTimeoutSeconds(Number(event.target.value));
7067
- },
7068
- required: true
7069
- }),
7070
- (0, react_jsx_runtime.jsx)("span", {
7071
- className: "dsh-ig-hint",
7072
- children: t("timeoutHint")
7073
- })
8262
+ workspaceMessage.length > 0 ? (0, react_jsx_runtime.jsx)("p", {
8263
+ className: `dsh-ig-status${workspaceMessageIsError ? " dsh-ig-status-error" : ""}`,
8264
+ role: "status",
8265
+ children: workspaceMessage
8266
+ }) : null
7074
8267
  ]
7075
- })] }),
7076
- (0, react_jsx_runtime.jsxs)("div", {
7077
- className: "dsh-ig-field",
7078
- children: [(0, react_jsx_runtime.jsxs)("label", {
7079
- className: "dsh-ig-check-row",
7080
- children: [(0, react_jsx_runtime.jsx)("input", {
7081
- type: "checkbox",
7082
- checked: saveToWorkspace,
7083
- onChange: (event) => {
7084
- setSaveToWorkspace(event.target.checked);
7085
- }
7086
- }), (0, react_jsx_runtime.jsx)("span", {
7087
- className: "dsh-ig-label",
7088
- children: t("saveToWorkspace")
7089
- })]
7090
- }), (0, react_jsx_runtime.jsx)("span", {
7091
- className: "dsh-ig-hint",
7092
- children: t("saveToWorkspaceHint")
7093
- })]
7094
8268
  }),
7095
- saveToWorkspace ? (0, react_jsx_runtime.jsxs)("label", {
7096
- className: "dsh-ig-field",
7097
- children: [
7098
- (0, react_jsx_runtime.jsx)("span", {
7099
- className: "dsh-ig-label",
7100
- children: t("folder")
7101
- }),
7102
- (0, react_jsx_runtime.jsx)("input", {
7103
- className: "dsh-ig-input",
7104
- value: workspaceFolder,
7105
- onChange: (event) => {
7106
- setWorkspaceFolder(event.target.value);
7107
- },
7108
- placeholder: "dsh-image-gen"
7109
- }),
7110
- (0, react_jsx_runtime.jsx)("span", {
7111
- className: "dsh-ig-hint",
7112
- children: t("folderHint")
7113
- })
7114
- ]
7115
- }) : null,
7116
8269
  (0, react_jsx_runtime.jsxs)("div", {
7117
- className: "dsh-ig-actions",
7118
- children: [(0, react_jsx_runtime.jsx)("p", {
7119
- className: `dsh-ig-status${messageIsError ? " dsh-ig-status-error" : ""}`,
7120
- role: "status",
7121
- children: message || (provider === "comfyui" ? workflowStatus : keyStatus)
7122
- }), (0, react_jsx_runtime.jsx)("button", {
7123
- className: "dsh-ig-save",
7124
- type: "submit",
7125
- disabled: saving || !snapshot.writable || provider === "comfyui" && workflows.length === 0,
7126
- children: saving ? t("saving") : t("save")
8270
+ className: "dsh-ig-section",
8271
+ children: [(0, react_jsx_runtime.jsx)("span", {
8272
+ className: "dsh-ig-section-title",
8273
+ children: t("uiSection")
8274
+ }), (0, react_jsx_runtime.jsxs)("div", {
8275
+ className: "dsh-ig-field",
8276
+ children: [
8277
+ (0, react_jsx_runtime.jsxs)("label", {
8278
+ className: "dsh-ig-check-row",
8279
+ children: [(0, react_jsx_runtime.jsx)("input", {
8280
+ type: "checkbox",
8281
+ checked: showPill,
8282
+ onChange: (event) => {
8283
+ toggleShowPill(event.target.checked);
8284
+ },
8285
+ disabled: !snapshot.writable
8286
+ }), (0, react_jsx_runtime.jsx)("span", {
8287
+ className: "dsh-ig-label",
8288
+ children: t("showPill")
8289
+ })]
8290
+ }),
8291
+ (0, react_jsx_runtime.jsx)("span", {
8292
+ className: "dsh-ig-hint",
8293
+ children: t("showPillHint")
8294
+ }),
8295
+ uiMessage.length > 0 ? (0, react_jsx_runtime.jsx)("p", {
8296
+ className: `dsh-ig-status${uiMessageIsError ? " dsh-ig-status-error" : ""}`,
8297
+ role: "status",
8298
+ children: uiMessage
8299
+ }) : null
8300
+ ]
7127
8301
  })]
7128
8302
  })
7129
8303
  ]
@@ -7674,11 +8848,11 @@ window.__ModuleLoader__.load({
7674
8848
  };
7675
8849
  }
7676
8850
  function modelOf(provider, value) {
7677
- const stored = provider === "google" ? value?.googleModel : provider === "openai" ? value?.openaiModel : provider === "seedream" ? value?.seedreamModel : provider === "dashscope" ? value?.dashscopeModel : activeComfyUIWorkflow(value ?? {})?.name;
8851
+ const stored = provider === "comfyui" ? activeComfyUIWorkflow(value ?? {})?.name : value?.[CLOUD_MODEL_FIELDS[provider]];
7678
8852
  return typeof stored === "string" && stored.length > 0 ? stored : DEFAULT_MODELS[provider];
7679
8853
  }
7680
8854
  function baseURLOf(provider, value) {
7681
- const stored = provider === "google" ? value?.googleEndpoint : provider === "openai" ? value?.openaiBaseURL : provider === "seedream" ? value?.seedreamBaseURL : provider === "dashscope" ? value?.dashscopeEndpoint : value?.comfyuiBaseURL;
8855
+ const stored = provider === "comfyui" ? value?.comfyuiBaseURL : value?.[CLOUD_URL_FIELDS[provider]];
7682
8856
  return typeof stored === "string" && stored.length > 0 ? stored : DEFAULT_BASE_URLS[provider];
7683
8857
  }
7684
8858
  //#endregion