@dickpy/dsh-imagegen 1.3.0 → 1.5.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.
Files changed (38) hide show
  1. package/README.md +363 -196
  2. package/docs/images/ecommerce-mode.png +0 -0
  3. package/docs/images/image-generation-studio-three-column.png +0 -0
  4. package/docs/images/imagegen-overview.png +0 -0
  5. package/docs/images/multi-model-comparison.png +0 -0
  6. package/docs/videos/agent-chat-edit.gif +0 -0
  7. package/docs/videos/agent-chat-edit.mp4 +0 -0
  8. package/lib/client.js +2589 -884
  9. package/lib/client.js.map +1 -1
  10. package/lib/index.js +585 -240
  11. package/package.json +5 -2
  12. package/src/agent-image-tools.ts +131 -102
  13. package/src/client/ImageGenPanel.tsx +2679 -1594
  14. package/src/client/SettingsCard.tsx +6 -27
  15. package/src/client/api.ts +11 -1
  16. package/src/client/conversation-sync.ts +14 -0
  17. package/src/client/image-toolview.tsx +176 -165
  18. package/src/client/index.ts +25 -15
  19. package/src/client/locales.ts +746 -602
  20. package/src/client/mount.tsx +213 -124
  21. package/src/client/panel.module.css +2619 -1563
  22. package/src/client/sidebar-entry.ts +190 -144
  23. package/src/edit-image-command.ts +110 -0
  24. package/src/engine.ts +47 -5
  25. package/src/gallery-store.ts +20 -0
  26. package/src/generation-runtime.ts +11 -2
  27. package/src/history-store.ts +26 -0
  28. package/src/image-models.ts +1 -1
  29. package/src/index.ts +31 -12
  30. package/src/model-catalog.ts +19 -2
  31. package/src/presets.ts +11 -3
  32. package/src/prompt-enhancer.ts +63 -5
  33. package/src/protocol.ts +59 -5
  34. package/src/routes.ts +62 -4
  35. package/src/task-queue.ts +42 -32
  36. package/docs/images/agent-chat-edit.png +0 -0
  37. package/docs/images/agent-chat-generate.png +0 -0
  38. package/docs/images/agent-chat-poster-workflow.png +0 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dickpy/dsh-imagegen",
3
- "description": "AI image generation plugin for the dsh web GUI: text-to-image and image-to-image through configurable provider channels (gpt-image-2 / grok-imagine-image / nanobanana series / seedream-5.0-pro / dall-e-3, with native xAI Grok Imagine, Google Nano Banana and ByteDance Seedream request shaping), with per-channel model catalogs and a sidebar entry opening a split-pane generation studio.",
4
- "version": "1.3.0",
3
+ "description": "AI image generation plugin for the dsh web GUI: text-to-image and image-to-image through configurable provider channels (gpt-image-2 / grok-imagine-image / nanobanana series / seedream-5.0-pro / dall-e-3, with native xAI Grok Imagine, Google Nano Banana and ByteDance Seedream request shaping), with per-channel model catalogs and a New Session / Image Generation tab entry opening a three-column studio beside the native conversation.",
4
+ "version": "1.5.0",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {
@@ -17,6 +17,7 @@
17
17
  "inject": [
18
18
  "@deepseek-ai/dsh-client-runtime",
19
19
  "@deepseek-ai/dsh-client-connection",
20
+ "@deepseek-ai/dsh-client-ui-conversation",
20
21
  "@deepseek-ai/dsh-client-ui-settings",
21
22
  "@deepseek-ai/dsh-client-ui-tool"
22
23
  ],
@@ -32,9 +33,11 @@
32
33
  "@deepseek-ai/dsh-client-connection": "0.1.0-rc.7",
33
34
  "@deepseek-ai/dsh-client-locale": "0.1.0-rc.7",
34
35
  "@deepseek-ai/dsh-client-runtime": "0.1.0-rc.7",
36
+ "@deepseek-ai/dsh-client-ui-conversation": "0.1.0-rc.7",
35
37
  "@deepseek-ai/dsh-client-ui-primitives": "0.1.0-rc.7",
36
38
  "@deepseek-ai/dsh-client-ui-settings": "0.1.0-rc.7",
37
39
  "@deepseek-ai/dsh-client-ui-slots": "0.1.0-rc.7",
40
+ "@deepseek-ai/dsh-commands": "0.1.0-rc.7",
38
41
  "@deepseek-ai/dsh-host-webserver": "0.1.0-rc.7",
39
42
  "@deepseek-ai/dsh-llm": "0.1.0-rc.7",
40
43
  "@deepseek-ai/dsh-settings": "0.1.0-rc.7",
@@ -3,7 +3,7 @@
3
3
  import type { Context } from '@deepseek-ai/cordis'
4
4
  import { defineTool } from '@deepseek-ai/dsh-tools'
5
5
  import type { ToolResult, ToolResultView } from '@deepseek-ai/dsh-tools'
6
- import type { ImageAttachmentRef, ImageMediaType } from '@deepseek-ai/dsh-attachment'
6
+ import type { AttachmentStore, ImageAttachmentRef, ImageMediaType } from '@deepseek-ai/dsh-attachment'
7
7
  import type {} from '@deepseek-ai/dsh-attachment'
8
8
  import type {} from '@deepseek-ai/dsh-tools'
9
9
  import { ImageGenError } from './engine.ts'
@@ -63,6 +63,132 @@ const taskResultSchema = {
63
63
  /** Agent calls stay pending until the provider and history write settle. */
64
64
  const AGENT_GENERATION_TIMEOUT_MS = 300_000
65
65
 
66
+ export function ensureAgentImageConfigured(config: AgentImageToolConfig): void {
67
+ if (!config.enabled) throw new ImageGenError('AI image generation is disabled. Open Settings > Plugins > AI Image and enable it.', 'plugin-disabled')
68
+ if (!config.allowAgentImageGeneration) throw new ImageGenError('Agent image generation is disabled in Settings > Plugins > AI Image.', 'agent-generation-disabled')
69
+ const usable = config.channels.some(channel => channel.apiUrl.trim() !== '' && channel.apiKey.trim() !== '')
70
+ if (!usable) throw new ImageGenError('Image API credentials are not configured. Open Settings > Plugins > AI Image, add a channel and fill in its API URL and API key.', 'image-api-not-configured')
71
+ }
72
+
73
+ /** Resolve a configured image alias and its owning channel. */
74
+ export function resolveAgentImageModel(config: AgentImageToolConfig, requested: unknown): { channel: RuntimeChannel; alias: string; upstream: string } {
75
+ const entries = config.channels.flatMap(channel => channel.models.map(model => ({ channel, alias: model.alias, upstream: model.id })))
76
+ if (entries.length === 0) {
77
+ throw new ImageGenError('No image models are configured. Open Settings > Plugins > AI Image and add a channel with at least one model.', 'no-models-configured')
78
+ }
79
+ const wanted = typeof requested === 'string' && requested.trim() !== '' ? requested.trim() : ''
80
+ if (wanted === '') {
81
+ if (entries.length === 1) return entries[0]!
82
+ const options = config.channels.flatMap(channel => channel.models.map(model => `"${channel.name} · ${model.alias}"`)).join(', ')
83
+ throw new ImageGenError(`Multiple image models are available — ask the user which channel and model to use, then call this tool again with that exact model name. Options: ${options}.`, 'model-choice-required')
84
+ }
85
+ const hosting = entries.filter(entry => entry.alias === wanted)
86
+ if (hosting.length === 0) {
87
+ const available = [...new Set(entries.map(entry => entry.alias))].join(', ')
88
+ throw new ImageGenError(`Image model "${wanted}" is not configured in any channel. Choose one of: ${available}.`, 'image-model-not-configured')
89
+ }
90
+ const preferred = hosting.find(entry => entry.channel.id === config.defaultChannelId)
91
+ return preferred ?? hosting[0]!
92
+ }
93
+
94
+ function findAgentImageTask(runtime: ImageGenerationRuntime, id: string): GenerationTask {
95
+ const task = runtime.queue.list().find(candidate => candidate.id === id)
96
+ if (task === undefined) throw new ImageGenError(`Image generation task ${id} was not found.`, 'task-not-found')
97
+ return task
98
+ }
99
+
100
+ /** Wait for a queue task without sending anything through the chat model. */
101
+ export function waitForAgentImageTask(runtime: ImageGenerationRuntime, id: string, signal: AbortSignal | undefined): Promise<GenerationTask> {
102
+ return new Promise<GenerationTask>((resolveTask, rejectTask) => {
103
+ let settled = false
104
+ let dispose = (): void => {}
105
+ let timer: ReturnType<typeof setTimeout> | undefined
106
+ let abort = (): void => {}
107
+
108
+ const cleanup = (): void => {
109
+ dispose()
110
+ if (timer !== undefined) clearTimeout(timer)
111
+ signal?.removeEventListener('abort', abort)
112
+ }
113
+ const resolve = (task: GenerationTask): void => {
114
+ if (settled) return
115
+ settled = true
116
+ cleanup()
117
+ resolveTask(task)
118
+ }
119
+ const reject = (error: unknown): void => {
120
+ if (settled) return
121
+ settled = true
122
+ cleanup()
123
+ rejectTask(error)
124
+ }
125
+ abort = (): void => {
126
+ if (settled) return
127
+ const reason = signal?.reason instanceof Error ? signal.reason : new Error('Image generation was cancelled.')
128
+ settled = true
129
+ cleanup()
130
+ runtime.queue.cancel(id)
131
+ rejectTask(reason)
132
+ }
133
+ const onChange = (updated: GenerationTask): void => {
134
+ if (updated.id === id && isFinalTask(updated)) resolve(updated)
135
+ }
136
+
137
+ if (signal?.aborted === true) {
138
+ abort()
139
+ return
140
+ }
141
+ dispose = runtime.queue.subscribe(onChange)
142
+ signal?.addEventListener('abort', abort, { once: true })
143
+ timer = setTimeout(() => {
144
+ if (settled) return
145
+ const timeout = new ImageGenError(`Image generation task ${id} timed out after ${AGENT_GENERATION_TIMEOUT_MS / 1000} seconds.`, 'generation-timeout')
146
+ settled = true
147
+ cleanup()
148
+ runtime.queue.cancel(id)
149
+ rejectTask(timeout)
150
+ }, AGENT_GENERATION_TIMEOUT_MS)
151
+ let current: GenerationTask
152
+ try {
153
+ current = findAgentImageTask(runtime, id)
154
+ } catch (error) {
155
+ reject(error)
156
+ return
157
+ }
158
+ if (isFinalTask(current)) resolve(current)
159
+ })
160
+ }
161
+
162
+ /** Submit the same image-edit request used by the Agent tool. */
163
+ export async function submitAgentImageEdit(
164
+ attachments: Pick<AttachmentStore, 'readImage'>,
165
+ runtime: ImageGenerationRuntime,
166
+ resolve: () => AgentImageToolConfig,
167
+ input: { prompt: string; sourceImage: ImageAttachmentRef; signal?: AbortSignal },
168
+ ): Promise<GenerationTask> {
169
+ const config = resolve()
170
+ ensureAgentImageConfigured(config)
171
+ const reference = await attachments.readImage(input.sourceImage, input.signal)
172
+ const defaultChannel = config.channels.find(channel => channel.id === config.defaultChannelId) ?? config.channels[0]
173
+ const defaultModel = defaultChannel?.models[0]?.alias ?? config.channels.flatMap(channel => channel.models)[0]?.alias
174
+ const picked = resolveAgentImageModel(config, defaultModel)
175
+ const task = runtime.queue.submit({
176
+ mode: 'edit',
177
+ model: picked.alias,
178
+ upstream: picked.upstream,
179
+ channelId: picked.channel.id,
180
+ channel: picked.channel.name,
181
+ prompt: input.prompt.trim(),
182
+ size: 'auto',
183
+ quality: 'auto',
184
+ n: 1,
185
+ detail: '',
186
+ image: imageDataUrl(reference),
187
+ ...reference.ref.name === undefined ? {} : { refName: reference.ref.name },
188
+ })
189
+ return waitForAgentImageTask(runtime, task.id, input.signal)
190
+ }
191
+
66
192
  function acceptedMediaType(value: string): value is ImageMediaType {
67
193
  return value === 'image/png' || value === 'image/jpeg' || value === 'image/webp' || value === 'image/gif'
68
194
  }
@@ -158,42 +284,8 @@ function presentImageResult(_args: unknown, result: ToolResult): ToolResultView
158
284
  /** Register the global Agent tools and unregister them with the plugin lifecycle. */
159
285
  export function registerAgentImageTools(ctx: Context, runtime: ImageGenerationRuntime, resolve: () => AgentImageToolConfig): () => void {
160
286
  const attachmentRefs = new Map<string, Promise<AgentImageRef[]>>()
161
- const ensureConfigured = (): void => {
162
- const config = resolve()
163
- if (!config.enabled) throw new ImageGenError('AI image generation is disabled. Open Settings > Plugins > AI Image and enable it.', 'plugin-disabled')
164
- if (!config.allowAgentImageGeneration) throw new ImageGenError('Agent image generation is disabled in Settings > Plugins > AI Image.', 'agent-generation-disabled')
165
- const usable = config.channels.some(channel => channel.apiUrl.trim() !== '' && channel.apiKey.trim() !== '')
166
- if (!usable) throw new ImageGenError('Image API credentials are not configured. Open Settings > Plugins > AI Image, add a channel and fill in its API URL and API key.', 'image-api-not-configured')
167
- }
168
-
169
- /**
170
- * Resolve the requested model alias onto a channel. Rules:
171
- * - a named alias must exist in some channel's catalog (several channels
172
- * may host it; the default channel wins);
173
- * - with no alias, a single configured model is used directly, while
174
- * multiple models require the Agent to ask the user first.
175
- * @returns the channel plus the alias and its upstream id.
176
- */
177
- const resolveModel = (requested: unknown): { channel: RuntimeChannel; alias: string; upstream: string } => {
178
- const config = resolve()
179
- const entries = config.channels.flatMap(channel => channel.models.map(model => ({ channel, alias: model.alias, upstream: model.id })))
180
- if (entries.length === 0) {
181
- throw new ImageGenError('No image models are configured. Open Settings > Plugins > AI Image and add a channel with at least one model.', 'no-models-configured')
182
- }
183
- const wanted = typeof requested === 'string' && requested.trim() !== '' ? requested.trim() : ''
184
- if (wanted === '') {
185
- if (entries.length === 1) return entries[0]!
186
- const options = config.channels.flatMap(channel => channel.models.map(model => `"${channel.name} · ${model.alias}"`)).join(', ')
187
- throw new ImageGenError(`Multiple image models are available — ask the user which channel and model to use, then call this tool again with that exact model name. Options: ${options}.`, 'model-choice-required')
188
- }
189
- const hosting = entries.filter(entry => entry.alias === wanted)
190
- if (hosting.length === 0) {
191
- const available = [...new Set(entries.map(entry => entry.alias))].join(', ')
192
- throw new ImageGenError(`Image model "${wanted}" is not configured in any channel. Choose one of: ${available}.`, 'image-model-not-configured')
193
- }
194
- const preferred = hosting.find(entry => entry.channel.id === config.defaultChannelId)
195
- return preferred ?? hosting[0]!
196
- }
287
+ const ensureConfigured = (): void => { ensureAgentImageConfigured(resolve()) }
288
+ const resolveModel = (requested: unknown): { channel: RuntimeChannel; alias: string; upstream: string } => resolveAgentImageModel(resolve(), requested)
197
289
  const materializeTaskImages = (task: GenerationTask): Promise<AgentImageRef[]> => {
198
290
  if (task.status !== 'completed') return Promise.resolve([])
199
291
  const existing = attachmentRefs.get(task.id)
@@ -222,71 +314,8 @@ export function registerAgentImageTools(ctx: Context, runtime: ImageGenerationRu
222
314
  images,
223
315
  }
224
316
  }
225
- const findTask = (id: string): GenerationTask => {
226
- const task = runtime.queue.list().find(candidate => candidate.id === id)
227
- if (task === undefined) throw new ImageGenError(`Image generation task ${id} was not found.`, 'task-not-found')
228
- return task
229
- }
230
- const waitForTask = (id: string, signal: AbortSignal | undefined): Promise<GenerationTask> => new Promise((resolveTask, rejectTask) => {
231
- let settled = false
232
- let dispose = (): void => {}
233
- let timer: ReturnType<typeof setTimeout> | undefined
234
- let abort = (): void => {}
235
-
236
- const cleanup = (): void => {
237
- dispose()
238
- if (timer !== undefined) clearTimeout(timer)
239
- signal?.removeEventListener('abort', abort)
240
- }
241
- const resolve = (task: GenerationTask): void => {
242
- if (settled) return
243
- settled = true
244
- cleanup()
245
- resolveTask(task)
246
- }
247
- const reject = (error: unknown): void => {
248
- if (settled) return
249
- settled = true
250
- cleanup()
251
- rejectTask(error)
252
- }
253
- abort = (): void => {
254
- if (settled) return
255
- const reason = signal?.reason instanceof Error ? signal.reason : new Error('Image generation was cancelled.')
256
- // Remove the listener before publishing cancellation so the queue event
257
- // cannot turn an execution abort into a successful cancelled result.
258
- settled = true
259
- cleanup()
260
- runtime.queue.cancel(id)
261
- rejectTask(reason)
262
- }
263
- const onChange = (updated: GenerationTask): void => {
264
- if (updated.id === id && isFinalTask(updated)) resolve(updated)
265
- }
266
-
267
- if (signal?.aborted === true) {
268
- abort()
269
- return
270
- }
271
- dispose = runtime.queue.subscribe(onChange)
272
- signal?.addEventListener('abort', abort, { once: true })
273
- timer = setTimeout(() => {
274
- if (settled) return
275
- const timeout = new ImageGenError(`Image generation task ${id} timed out after ${AGENT_GENERATION_TIMEOUT_MS / 1000} seconds.`, 'generation-timeout')
276
- settled = true
277
- cleanup()
278
- runtime.queue.cancel(id)
279
- rejectTask(timeout)
280
- }, AGENT_GENERATION_TIMEOUT_MS)
281
- let current: GenerationTask
282
- try {
283
- current = findTask(id)
284
- } catch (error) {
285
- reject(error)
286
- return
287
- }
288
- if (isFinalTask(current)) resolve(current)
289
- })
317
+ const findTask = (id: string): GenerationTask => findAgentImageTask(runtime, id)
318
+ const waitForTask = (id: string, signal: AbortSignal | undefined): Promise<GenerationTask> => waitForAgentImageTask(runtime, id, signal)
290
319
  const disposers = [
291
320
  ctx.tools.register(defineTool({
292
321
  name: 'generate_image',