@dickpy/dsh-imagegen 1.1.0 → 1.2.0

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.
@@ -9,18 +9,19 @@
9
9
  import { useState } from 'react'
10
10
  import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
11
11
  import { createSnapshotStore, type SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
12
- import { CardForm, booleanField, secretField, textField, type CardActions, type CardShell, type FieldState as CardFieldState } from './settings-form.ts'
12
+ import { CardForm, booleanField, secretField, stringListField, textField, type CardActions, type CardShell, type FieldState as CardFieldState } from './settings-form.ts'
13
13
  import type { ImageGenScope } from './settings-scope.ts'
14
- import { PLUGIN_VERSION } from '../protocol.ts'
15
- import { PROMPT_ENHANCE_API } from '../protocol.ts'
14
+ import { IMAGE_MODEL_API, PROMPT_ENHANCE_API } from '../protocol.ts'
16
15
  import css from './settings-card.module.css'
17
16
 
18
17
  /** The fields this card edits (the namespace's full schema). */
19
18
  export interface ImageGenSettings {
20
19
  enabled?: boolean
21
20
  announceToAgent?: boolean
21
+ allowAgentImageGeneration?: boolean
22
22
  apiUrl?: string
23
23
  apiKey?: string
24
+ imageModels?: string[]
24
25
  promptApiUrl?: string
25
26
  promptApiKey?: string
26
27
  promptModel?: string
@@ -32,10 +33,12 @@ export interface ImageGenSettingsCardState extends CardShell {
32
33
  enabled: CardFieldState
33
34
  /** System-prompt announcement flag. */
34
35
  announceToAgent: CardFieldState
36
+ allowAgentImageGeneration: CardFieldState
35
37
  /** API base URL. */
36
38
  apiUrl: CardFieldState
37
39
  /** API key draft (the stored value is never rendered). */
38
40
  apiKey: CardFieldState
41
+ imageModels: CardFieldState
39
42
  promptApiUrl: CardFieldState
40
43
  promptApiKey: CardFieldState
41
44
  promptModel: CardFieldState
@@ -60,8 +63,10 @@ export class ImageGenSettingsCardController {
60
63
  this.form = new CardForm(scope, [
61
64
  booleanField('enabled'),
62
65
  booleanField('announceToAgent'),
66
+ booleanField('allowAgentImageGeneration'),
63
67
  textField('apiUrl'),
64
68
  secretField('apiKey'),
69
+ stringListField('imageModels'),
65
70
  textField('promptApiUrl'),
66
71
  secretField('promptApiKey'),
67
72
  textField('promptModel'),
@@ -77,8 +82,10 @@ export class ImageGenSettingsCardController {
77
82
  ...this.form.shell(),
78
83
  enabled: this.form.field('enabled'),
79
84
  announceToAgent: this.form.field('announceToAgent'),
85
+ allowAgentImageGeneration: this.form.field('allowAgentImageGeneration'),
80
86
  apiUrl: this.form.field('apiUrl'),
81
87
  apiKey: this.form.field('apiKey'),
88
+ imageModels: this.form.field('imageModels'),
82
89
  promptApiUrl: this.form.field('promptApiUrl'),
83
90
  promptApiKey: this.form.field('promptApiKey'),
84
91
  promptModel: this.form.field('promptModel'),
@@ -119,9 +126,19 @@ export function ImageGenSettingsCard(props: ImageGenSettingsCardProps) {
119
126
  const state = props.useImageGenSettingsCard(snapshot => snapshot)
120
127
  const keySet = props.useImageGenKeySet(snapshot => snapshot)
121
128
  const [open, setOpen] = useState(false)
122
- const [models, setModels] = useState<string[]>([])
123
- const [loadingModels, setLoadingModels] = useState(false)
124
- const [modelsError, setModelsError] = useState<string | null>(null)
129
+ const [promptModels, setPromptModels] = useState<string[]>([])
130
+ const [loadingPromptModels, setLoadingPromptModels] = useState(false)
131
+ const [promptModelsError, setPromptModelsError] = useState<string | null>(null)
132
+ const [imageModelCandidates, setImageModelCandidates] = useState<string[]>([])
133
+ const [loadingImageModels, setLoadingImageModels] = useState(false)
134
+ const [imageModelsError, setImageModelsError] = useState<string | null>(null)
135
+ const [manualModelsOpen, setManualModelsOpen] = useState(false)
136
+ const [manualModel, setManualModel] = useState('')
137
+ const [enhancementOpen, setEnhancementOpen] = useState(false)
138
+ const [manualPromptModelOpen, setManualPromptModelOpen] = useState(false)
139
+ const [manualPromptModel, setManualPromptModel] = useState('')
140
+ const [promptApiOpen, setPromptApiOpen] = useState(false)
141
+ const [moreOpen, setMoreOpen] = useState(false)
125
142
  if (!state.available) return null
126
143
  const title = t('settings.title')
127
144
  const blocked = !state.dirty || state.invalid || state.saving
@@ -178,10 +195,16 @@ export function ImageGenSettingsCard(props: ImageGenSettingsCardProps) {
178
195
  ? (
179
196
  <div className={css.body}>
180
197
  {!state.writable ? <p className={css.readOnly} role="status">{t('settings.readOnly')}</p> : null}
181
- <div className={css.versionRow}>
182
- <span className={css.versionLabel}>{t('settings.currentVersion')}</span>
183
- <code className={css.versionValue}>v{PLUGIN_VERSION}</code>
184
- </div>
198
+ <ValueField
199
+ id="dsh-imagegen-settings-apiurl"
200
+ label={t('settings.apiUrl')}
201
+ hint={t('settings.apiUrlHint')}
202
+ placeholder="https://api.openai.com/v1"
203
+ {...fieldProps}
204
+ {...state.apiUrl}
205
+ onEdit={(text) => { props.edit('apiUrl', text) }}
206
+ onReset={() => { props.resetField('apiUrl') }}
207
+ />
185
208
  <ValueField
186
209
  id="dsh-imagegen-settings-apikey"
187
210
  label={t('settings.apiKey')}
@@ -197,19 +220,187 @@ export function ImageGenSettingsCard(props: ImageGenSettingsCardProps) {
197
220
  onClear={() => { props.resetField('apiKey') }}
198
221
  canClear={keySet}
199
222
  />
200
- <ValueField
201
- id="dsh-imagegen-settings-apiurl"
202
- label={t('settings.apiUrl')}
203
- hint={t('settings.apiUrlHint')}
204
- placeholder="https://api.openai.com/v1"
205
- {...fieldProps}
206
- {...state.apiUrl}
207
- onEdit={(text) => { props.edit('apiUrl', text) }}
208
- onReset={() => { props.resetField('apiUrl') }}
209
- />
210
223
  <div className={css.sectionDivider} />
211
- <h3 className={css.sectionTitle}>{t('settings.promptEnhanceTitle')}</h3>
224
+ <section className={css.modelSection} aria-label={t('settings.imageModelsTitle')}>
225
+ <div className={css.sectionHeader}>
226
+ <div>
227
+ <h3 className={css.sectionTitle}>{t('settings.imageModelsTitle')}</h3>
228
+ <p className={css.sectionHint}>{t('settings.imageModelsHint')}</p>
229
+ </div>
230
+ <button
231
+ type="button"
232
+ className={css.modelFetch}
233
+ disabled={disabled || loadingImageModels}
234
+ onClick={() => {
235
+ setLoadingImageModels(true)
236
+ setImageModelsError(null)
237
+ void fetch(IMAGE_MODEL_API.models, { method: 'POST' })
238
+ .then(async response => {
239
+ const body = await response.json() as { ok?: boolean; models?: string[]; message?: string }
240
+ if (!response.ok || body.ok !== true) throw new Error(body.message ?? `HTTP ${response.status}`)
241
+ setImageModelCandidates(body.models ?? [])
242
+ })
243
+ .catch(error => { setImageModelsError(error instanceof Error ? error.message : String(error)) })
244
+ .finally(() => { setLoadingImageModels(false) })
245
+ }}
246
+ >
247
+ {loadingImageModels ? t('settings.imageModelsLoading') : t('settings.imageModelsFetch')}
248
+ </button>
249
+ </div>
250
+ <div className={css.modelSummary}>
251
+ {splitModels(state.imageModels.text).map(model => (
252
+ <span key={model} className={css.modelChip}>
253
+ <span>{model}</span>
254
+ <button
255
+ type="button"
256
+ disabled={disabled}
257
+ aria-label={`${t('settings.removeModel')}: ${model}`}
258
+ onClick={() => { props.edit('imageModels', splitModels(state.imageModels.text).filter(value => value !== model).join('\n')) }}
259
+ >
260
+ ×
261
+ </button>
262
+ </span>
263
+ ))}
264
+ <button type="button" className={css.addModel} disabled={disabled} onClick={() => { setManualModelsOpen(open => !open) }}>
265
+ {manualModelsOpen ? t('settings.cancelAddModel') : t('settings.addModel')}
266
+ </button>
267
+ </div>
268
+ {manualModelsOpen ? (
269
+ <div className={css.manualModelRow}>
270
+ <input
271
+ className={css.input}
272
+ value={manualModel}
273
+ placeholder={t('settings.addModelPlaceholder')}
274
+ disabled={disabled}
275
+ onChange={event => { setManualModel(event.target.value) }}
276
+ onKeyDown={event => {
277
+ if (event.key !== 'Enter') return
278
+ event.preventDefault()
279
+ const next = manualModel.trim()
280
+ if (next === '') return
281
+ props.edit('imageModels', [...splitModels(state.imageModels.text), next].join('\n'))
282
+ setManualModel('')
283
+ }}
284
+ />
285
+ <button
286
+ type="button"
287
+ className={css.addModel}
288
+ disabled={disabled || manualModel.trim() === ''}
289
+ onClick={() => {
290
+ props.edit('imageModels', [...splitModels(state.imageModels.text), manualModel.trim()].join('\n'))
291
+ setManualModel('')
292
+ }}
293
+ >
294
+ {t('settings.addModelConfirm')}
295
+ </button>
296
+ </div>
297
+ ) : null}
298
+ {imageModelCandidates.length > 0 ? (
299
+ <div className={css.modelCandidateList} role="group" aria-label={t('settings.imageModelsCandidates')}>
300
+ <span className={css.modelCandidateLabel}>{t('settings.imageModelsCandidates')}</span>
301
+ {imageModelCandidates.map(candidate => {
302
+ const selected = splitModels(state.imageModels.text).includes(candidate)
303
+ return (
304
+ <label key={candidate} className={css.modelCandidate} data-selected={selected ? '' : undefined}>
305
+ <input
306
+ type="checkbox"
307
+ checked={selected}
308
+ disabled={disabled}
309
+ onChange={() => {
310
+ const selectedModels = splitModels(state.imageModels.text)
311
+ const next = selected ? selectedModels.filter(model => model !== candidate) : [...selectedModels, candidate]
312
+ props.edit('imageModels', next.join('\n'))
313
+ }}
314
+ />
315
+ <span>{candidate}</span>
316
+ </label>
317
+ )
318
+ })}
319
+ </div>
320
+ ) : null}
321
+ {imageModelsError !== null ? <p className={css.failed} role="status">{imageModelsError}</p> : null}
322
+ </section>
323
+ <button
324
+ type="button"
325
+ className={css.disclosure}
326
+ aria-expanded={enhancementOpen}
327
+ onClick={() => { setEnhancementOpen(open => !open) }}
328
+ >
329
+ <span>{t('settings.promptEnhanceTitle')}</span>
330
+ <span>{t('settings.optional')}</span>
331
+ <span aria-hidden="true">{enhancementOpen ? '⌃' : '⌄'}</span>
332
+ </button>
333
+ {enhancementOpen ? <section className={css.optionalContent} aria-label={t('settings.promptEnhanceTitle')}>
212
334
  <p className={css.sectionHint}>{t('settings.promptEnhanceHint')}</p>
335
+ <div className={css.sectionHeader}>
336
+ <div>
337
+ <h3 className={css.sectionTitle}>{t('settings.promptModel')}</h3>
338
+ <p className={css.sectionHint}>{t('settings.promptModelDetectionHint')}</p>
339
+ </div>
340
+ <button
341
+ type="button"
342
+ className={css.modelFetch}
343
+ disabled={disabled || loadingPromptModels}
344
+ onClick={() => {
345
+ setLoadingPromptModels(true)
346
+ setPromptModelsError(null)
347
+ void fetch(PROMPT_ENHANCE_API.models, { method: 'POST' })
348
+ .then(async response => {
349
+ const body = await response.json() as { ok?: boolean; models?: string[]; message?: string }
350
+ if (!response.ok || body.ok !== true) throw new Error(body.message ?? `HTTP ${response.status}`)
351
+ setPromptModels(body.models ?? [])
352
+ })
353
+ .catch(error => { setPromptModelsError(error instanceof Error ? error.message : String(error)) })
354
+ .finally(() => { setLoadingPromptModels(false) })
355
+ }}
356
+ >
357
+ {loadingPromptModels ? t('settings.promptModelsLoading') : t('settings.promptModelsFetch')}
358
+ </button>
359
+ </div>
360
+ <div className={css.modelSummary}>
361
+ {state.promptModel.text.trim() !== '' ? (
362
+ <span className={css.modelChip}>
363
+ <span>{state.promptModel.text}</span>
364
+ <button type="button" disabled={disabled} aria-label={`${t('settings.removeModel')}: ${state.promptModel.text}`} onClick={() => { props.edit('promptModel', '') }}>×</button>
365
+ </span>
366
+ ) : null}
367
+ <button type="button" className={css.addModel} disabled={disabled} onClick={() => { setManualPromptModelOpen(open => !open) }}>
368
+ {manualPromptModelOpen ? t('settings.cancelAddModel') : t('settings.addModel')}
369
+ </button>
370
+ </div>
371
+ {manualPromptModelOpen ? (
372
+ <div className={css.manualModelRow}>
373
+ <input className={css.input} value={manualPromptModel} placeholder={t('settings.addPromptModelPlaceholder')} disabled={disabled} onChange={event => { setManualPromptModel(event.target.value) }} />
374
+ <button type="button" className={css.addModel} disabled={disabled || manualPromptModel.trim() === ''} onClick={() => { props.edit('promptModel', manualPromptModel); setManualPromptModel('') }}>
375
+ {t('settings.addModelConfirm')}
376
+ </button>
377
+ </div>
378
+ ) : null}
379
+ {promptModels.length > 0 ? (
380
+ <div className={css.modelCandidateList} role="radiogroup" aria-label={t('settings.promptModelsCandidates')}>
381
+ <span className={css.modelCandidateLabel}>{t('settings.promptModelsCandidates')}</span>
382
+ {promptModels.map(candidate => (
383
+ <button
384
+ key={candidate}
385
+ type="button"
386
+ role="radio"
387
+ className={css.modelCandidate}
388
+ aria-checked={state.promptModel.text === candidate}
389
+ data-selected={state.promptModel.text === candidate ? '' : undefined}
390
+ disabled={disabled}
391
+ onClick={() => { props.edit('promptModel', candidate) }}
392
+ >
393
+ {candidate}
394
+ </button>
395
+ ))}
396
+ </div>
397
+ ) : null}
398
+ {promptModelsError !== null ? <p className={css.failed} role="status">{promptModelsError}</p> : null}
399
+ <button type="button" className={css.inlineDisclosure} aria-expanded={promptApiOpen} onClick={() => { setPromptApiOpen(open => !open) }}>
400
+ <span>{t('settings.promptApiAdvanced')}</span>
401
+ <span aria-hidden="true">{promptApiOpen ? '⌃' : '⌄'}</span>
402
+ </button>
403
+ {promptApiOpen ? <div className={css.optionalContent}>
213
404
  <ValueField
214
405
  id="dsh-imagegen-settings-prompt-apiurl"
215
406
  label={t('settings.promptApiUrl')}
@@ -232,44 +423,13 @@ export function ImageGenSettingsCard(props: ImageGenSettingsCardProps) {
232
423
  onEdit={(text) => { props.edit('promptApiKey', text) }}
233
424
  onReset={() => { props.resetField('promptApiKey') }}
234
425
  />
235
- <ValueField
236
- id="dsh-imagegen-settings-prompt-model"
237
- label={t('settings.promptModel')}
238
- hint={t('settings.promptModelHint')}
239
- placeholder="gpt-4.1-mini"
240
- {...fieldProps}
241
- {...state.promptModel}
242
- onEdit={(text) => { props.edit('promptModel', text) }}
243
- onReset={() => { props.resetField('promptModel') }}
244
- />
245
- <div className={css.modelFetchRow}>
246
- <button
247
- type="button"
248
- className={css.modelFetch}
249
- disabled={disabled || loadingModels}
250
- onClick={() => {
251
- setLoadingModels(true)
252
- setModelsError(null)
253
- void fetch(PROMPT_ENHANCE_API.models, { method: 'POST' })
254
- .then(async response => {
255
- const body = await response.json() as { ok?: boolean; models?: string[]; message?: string }
256
- if (!response.ok || body.ok !== true) throw new Error(body.message ?? `HTTP ${response.status}`)
257
- setModels(body.models ?? [])
258
- })
259
- .catch(error => { setModelsError(error instanceof Error ? error.message : String(error)) })
260
- .finally(() => { setLoadingModels(false) })
261
- }}
262
- >
263
- {loadingModels ? t('settings.promptModelsLoading') : t('settings.promptModelsFetch')}
264
- </button>
265
- {models.length > 0 ? (
266
- <select className={css.modelChoices} value="" onChange={event => { if (event.target.value !== '') props.edit('promptModel', event.target.value) }}>
267
- <option value="">{t('settings.promptModelsSelect')}</option>
268
- {models.map(model => <option key={model} value={model}>{model}</option>)}
269
- </select>
270
- ) : null}
271
- </div>
272
- {modelsError !== null ? <p className={css.failed} role="status">{modelsError}</p> : null}
426
+ </div> : null}
427
+ </section> : null}
428
+ <button type="button" className={css.disclosure} aria-expanded={moreOpen} onClick={() => { setMoreOpen(open => !open) }}>
429
+ <span>{t('settings.moreOptions')}</span>
430
+ <span aria-hidden="true">{moreOpen ? '⌃' : '⌄'}</span>
431
+ </button>
432
+ {moreOpen ? <div className={css.optionalContent}>
273
433
  <BooleanField
274
434
  id="dsh-imagegen-settings-enabled"
275
435
  label={t('settings.enabled')}
@@ -294,6 +454,19 @@ export function ImageGenSettingsCard(props: ImageGenSettingsCardProps) {
294
454
  onEdit={(text) => { props.edit('announceToAgent', text) }}
295
455
  onReset={() => { props.resetField('announceToAgent') }}
296
456
  />
457
+ <BooleanField
458
+ id="dsh-imagegen-settings-agent-generation"
459
+ label={t('settings.allowAgentImageGeneration')}
460
+ hint={t('settings.allowAgentImageGenerationHint')}
461
+ inheritLabel={t('settings.inherit')}
462
+ onLabel={t('settings.on')}
463
+ offLabel={t('settings.off')}
464
+ {...fieldProps}
465
+ {...state.allowAgentImageGeneration}
466
+ onEdit={(text) => { props.edit('allowAgentImageGeneration', text) }}
467
+ onReset={() => { props.resetField('allowAgentImageGeneration') }}
468
+ />
469
+ </div> : null}
297
470
  <div className={css.footer}>
298
471
  {state.failed ? <p className={css.failed} role="status">{t('settings.saveFailed')}</p> : null}
299
472
  <button
@@ -360,6 +533,8 @@ function ValueField(props: FieldProps & {
360
533
  onClear?: () => void
361
534
  /** Whether a stored secret exists (enables the clear control). */
362
535
  canClear?: boolean
536
+ /** Render a multiline input for newline-separated configuration values. */
537
+ multiline?: boolean
363
538
  }) {
364
539
  return (
365
540
  <div className={css.field}>
@@ -393,17 +568,29 @@ function ValueField(props: FieldProps & {
393
568
  )
394
569
  : null}
395
570
  </div>
396
- <input
397
- id={props.id}
398
- className={props.invalid ? css.inputInvalid : css.input}
399
- type={props.secret === true ? 'password' : 'text'}
400
- autoComplete={props.secret === true ? 'off' : undefined}
401
- {...props.invalid ? { 'aria-invalid': true } : {}}
402
- value={props.text}
403
- placeholder={props.placeholder ?? ''}
404
- disabled={props.disabled}
405
- onChange={(event) => { props.onEdit(event.target.value) }}
406
- />
571
+ {props.multiline === true ? (
572
+ <textarea
573
+ id={props.id}
574
+ className={props.invalid ? css.textareaInvalid : css.textarea}
575
+ {...props.invalid ? { 'aria-invalid': true } : {}}
576
+ value={props.text}
577
+ placeholder={props.placeholder ?? ''}
578
+ disabled={props.disabled}
579
+ onChange={(event) => { props.onEdit(event.target.value) }}
580
+ />
581
+ ) : (
582
+ <input
583
+ id={props.id}
584
+ className={props.invalid ? css.inputInvalid : css.input}
585
+ type={props.secret === true ? 'password' : 'text'}
586
+ autoComplete={props.secret === true ? 'off' : undefined}
587
+ {...props.invalid ? { 'aria-invalid': true } : {}}
588
+ value={props.text}
589
+ placeholder={props.placeholder ?? ''}
590
+ disabled={props.disabled}
591
+ onChange={(event) => { props.onEdit(event.target.value) }}
592
+ />
593
+ )}
407
594
  <p className={props.invalid ? css.invalid : css.hint}>
408
595
  {props.invalid ? props.invalidLabel : props.hint}
409
596
  </p>
@@ -411,6 +598,10 @@ function ValueField(props: FieldProps & {
411
598
  )
412
599
  }
413
600
 
601
+ function splitModels(value: string): string[] {
602
+ return [...new Set(value.split(/[\n,]/).map(model => model.trim()).filter(Boolean))]
603
+ }
604
+
414
605
  /** A staged boolean field: 继承 / 开 / 关. */
415
606
  function BooleanField(props: FieldProps & {
416
607
  /** Copy for the inherit option. */
@@ -171,6 +171,20 @@ export const zh = {
171
171
  'settings.apiKeyHint': 'Bearer 密钥,明文存于本机设置文档;界面只显示是否已设置',
172
172
  'settings.apiKeySet': '已保存密钥;输入新值可更换,点击「清除」可删除',
173
173
  'settings.apiKeyClear': '清除',
174
+ 'settings.imageModelsTitle': '生图模型',
175
+ 'settings.imageModelsHint': '保存 API 地址和密钥后检测候选模型;请只选择实际支持生图的项。',
176
+ 'settings.imageModels': '允许使用的生图模型',
177
+ 'settings.imageModelsManualHint': '一行一个模型;API 未提供 /models 时可手动填写。面板和 Agent 只能使用此列表。',
178
+ 'settings.imageModelsFetch': '检测可用模型',
179
+ 'settings.imageModelsLoading': '检测中…',
180
+ 'settings.imageModelsCandidates': '检测到的候选模型(勾选后保存)',
181
+ 'settings.addModel': '+ 手动添加',
182
+ 'settings.cancelAddModel': '收起添加',
183
+ 'settings.addModelPlaceholder': '输入模型名称,例如 qwen-image',
184
+ 'settings.addModelConfirm': '添加',
185
+ 'settings.removeModel': '移除模型',
186
+ 'settings.optional': '可选',
187
+ 'settings.moreOptions': '更多设置',
174
188
  'settings.promptEnhanceTitle': '提示词增强模型',
175
189
  'settings.promptEnhanceHint': '用于将简短描述扩写为更完整的生图提示词;留空的地址和密钥会复用生图配置。',
176
190
  'settings.promptApiUrl': '对话 API 地址(可选)',
@@ -179,11 +193,17 @@ export const zh = {
179
193
  'settings.promptApiKeyHint': '留空则复用图像 API 密钥。',
180
194
  'settings.promptModel': '对话模型',
181
195
  'settings.promptModelHint': '选择或填写支持 /chat/completions 的模型。',
196
+ 'settings.promptModelDetectionHint': '默认复用生图 API;检测后点选一个模型即可。',
182
197
  'settings.promptModelsFetch': '获取可用模型',
183
198
  'settings.promptModelsLoading': '正在获取…',
184
199
  'settings.promptModelsSelect': '选择一个模型',
200
+ 'settings.promptModelsCandidates': '检测到的候选对话模型',
201
+ 'settings.addPromptModelPlaceholder': '输入对话模型名称,例如 gpt-4.1-mini',
202
+ 'settings.promptApiAdvanced': '使用独立对话 API(可选)',
185
203
  'settings.announceToAgent': '向 Agent 播报本插件',
186
204
  'settings.announceToAgentHint': '开启后,本插件的存在与能力会写入每个 Agent 的系统提示词',
205
+ 'settings.allowAgentImageGeneration': '允许 Agent 调用生图',
206
+ 'settings.allowAgentImageGenerationHint': '默认开启。关闭后,Agent 无法提交、查询或取消生图任务;侧边栏工作台不受影响。',
187
207
  'settings.enabled': '启用插件',
188
208
  'settings.enabledHint': '关闭后生图面板不可用(设置卡片始终可用)',
189
209
  'settings.save': '保存',
@@ -388,6 +408,20 @@ export const en: Record<keyof typeof zh, string> = {
388
408
  'settings.apiKeyHint': 'Bearer key, stored in plaintext in the local settings document; the UI only shows whether it is set',
389
409
  'settings.apiKeySet': 'A key is stored; type a new value to replace it, or click "Clear" to remove it',
390
410
  'settings.apiKeyClear': 'Clear',
411
+ 'settings.imageModelsTitle': 'Image generation models',
412
+ 'settings.imageModelsHint': 'Save the API URL and key, then detect candidate models. Select only models that actually support image generation.',
413
+ 'settings.imageModels': 'Allowed image models',
414
+ 'settings.imageModelsManualHint': 'One model per line. Add models manually when the API does not provide /models. The panel and Agent can use only this list.',
415
+ 'settings.imageModelsFetch': 'Detect available models',
416
+ 'settings.imageModelsLoading': 'Detecting…',
417
+ 'settings.imageModelsCandidates': 'Detected candidate models (select, then save)',
418
+ 'settings.addModel': '+ Add manually',
419
+ 'settings.cancelAddModel': 'Hide add',
420
+ 'settings.addModelPlaceholder': 'Enter a model name, e.g. qwen-image',
421
+ 'settings.addModelConfirm': 'Add',
422
+ 'settings.removeModel': 'Remove model',
423
+ 'settings.optional': 'Optional',
424
+ 'settings.moreOptions': 'More settings',
391
425
  'settings.promptEnhanceTitle': 'Prompt enhancement model',
392
426
  'settings.promptEnhanceHint': 'Expands short requests into complete image prompts. Blank URL and key reuse the image API configuration.',
393
427
  'settings.promptApiUrl': 'Chat API URL (optional)',
@@ -396,11 +430,17 @@ export const en: Record<keyof typeof zh, string> = {
396
430
  'settings.promptApiKeyHint': 'Leave blank to reuse the image API key.',
397
431
  'settings.promptModel': 'Chat model',
398
432
  'settings.promptModelHint': 'Select or enter a model supporting /chat/completions.',
433
+ 'settings.promptModelDetectionHint': 'Uses the image API by default. Detect and select one model.',
399
434
  'settings.promptModelsFetch': 'Fetch available models',
400
435
  'settings.promptModelsLoading': 'Fetching…',
401
436
  'settings.promptModelsSelect': 'Select a model',
437
+ 'settings.promptModelsCandidates': 'Detected chat model candidates',
438
+ 'settings.addPromptModelPlaceholder': 'Enter a chat model name, e.g. gpt-4.1-mini',
439
+ 'settings.promptApiAdvanced': 'Use a separate chat API (optional)',
402
440
  'settings.announceToAgent': 'Announce this plugin to agents',
403
441
  'settings.announceToAgentHint': 'When on, the plugin presence and capabilities are written into every agent system prompt',
442
+ 'settings.allowAgentImageGeneration': 'Allow agents to generate images',
443
+ 'settings.allowAgentImageGenerationHint': 'On by default. When off, agents cannot submit, query, or cancel image tasks; the sidebar studio remains available.',
404
444
  'settings.enabled': 'Enable plugin',
405
445
  'settings.enabledHint': 'When off, the generation studio is unavailable (this card stays available)',
406
446
  'settings.save': 'Save',