@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
@@ -249,10 +249,11 @@ export function ImageGenSettingsCard(props: ImageGenSettingsCardProps) {
249
249
  <button type="button" className={css.channelMain} disabled={disabled} onClick={() => { setEditingId(channel.id) }}>
250
250
  <span className={css.channelName}>{isDefault ? `★ ${channel.name || t('channels.untitled')}` : (channel.name || t('channels.untitled'))}</span>
251
251
  <span className={css.channelMeta}>
252
- {channel.apiUrl !== '' ? <span className={css.channelHost}>{hostOf(channel.apiUrl)}</span> : null}
253
- <span className={css.channelBadge} data-warn={keyHeld ? undefined : ''}>{keyHeld ? t('channels.keySet') : t('channels.keyMissing')}</span>
254
- <span className={css.channelBadge} data-warn={channel.models.length > 0 ? undefined : ''}>{channel.models.length > 0 ? t('channels.modelCount', { n: channel.models.length }) : t('channels.noModels')}</span>
255
- {isDefault ? <span className={css.channelBadge} data-default>{t('channels.defaultLabel')}</span> : null}
252
+ <span className={css.channelBadge} data-warn={!keyHeld || channel.models.length === 0 ? '' : undefined}>
253
+ {keyHeld ? t('channels.keySet') : t('channels.keyMissing')}
254
+ {' · '}
255
+ {channel.models.length > 0 ? t('channels.modelCount', { n: channel.models.length }) : t('channels.noModels')}
256
+ </span>
256
257
  </span>
257
258
  </button>
258
259
  <button type="button" className={css.channelAction} onClick={() => { setEditingId(channel.id) }}>{t('channels.edit')}</button>
@@ -517,14 +518,6 @@ function replaceChannel(channels: ChannelDraft[], id: string, patch: Partial<Cha
517
518
  form.setChannels(channels.map(channel => channel.id === id ? { ...channel, ...patch } : channel))
518
519
  }
519
520
 
520
- function hostOf(apiUrl: string): string {
521
- try {
522
- return new URL(apiUrl).hostname
523
- } catch {
524
- return apiUrl
525
- }
526
- }
527
-
528
521
  function clientId(): string {
529
522
  const random = typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function' ? crypto.randomUUID() : undefined
530
523
  return random ?? `ch-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`
@@ -561,8 +554,7 @@ function PresetPicker(props: {
561
554
  {props.presets.map(preset => (
562
555
  <button key={preset.id} type="button" className={css.presetRow} disabled={props.disabled} onClick={() => { props.onPick(preset) }}>
563
556
  <span className={css.presetName}>{preset.name}</span>
564
- <span className={css.presetMeta}>{preset.apiUrl} · {t('channels.presetModelsHint', { n: preset.models.length })}</span>
565
- <span className={css.presetHint}>{preset.hint}</span>
557
+ <span className={css.presetMeta}>{preset.models.map(model => model.alias).join(' · ')}</span>
566
558
  </button>
567
559
  ))}
568
560
  <button type="button" className={css.presetRow} data-custom disabled={props.disabled} onClick={props.onCustom}>
@@ -656,18 +648,6 @@ function ChannelEditor(props: {
656
648
  setCopyFrom('')
657
649
  }
658
650
 
659
- const onlyKnown = (): void => {
660
- const known = (candidates ?? []).filter(id => describeModel(id).known)
661
- const merged = [...channel.models]
662
- for (const id of known) {
663
- const alias = id
664
- if (!merged.some(model => model.alias === alias)) merged.push({ alias, id })
665
- }
666
- props.onSetModels(merged)
667
- }
668
-
669
- const knownCount = (candidates ?? []).filter(id => describeModel(id).known).length
670
-
671
651
  return (
672
652
  <div className={css.editorBackdrop} role="dialog" aria-modal="true" aria-label={`${t('channels.editorTitle')} · ${channel.name || t('channels.untitled')}`} onClick={props.onClose}>
673
653
  <div className={css.editorPanel} onClick={event => { event.stopPropagation() }}>
@@ -776,7 +756,6 @@ function ChannelEditor(props: {
776
756
  <div className={css.modelCandidateList}>
777
757
  <span className={css.modelCandidateLabel}>
778
758
  {t('channels.candidatesTitle')}
779
- <button type="button" className={css.inlineDisclosure} disabled={!props.writable || knownCount === 0} onClick={onlyKnown}>{t('channels.candidatesQuick')}</button>
780
759
  </span>
781
760
  {candidates.map(candidate => {
782
761
  const selected = channel.models.some(model => model.alias === candidate)
package/src/client/api.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * data access path the panel uses — plain fetch, same origin.
4
4
  */
5
5
 
6
- import { GALLERY_API, GENERATE_API, HISTORY_API, PROMPT_ENHANCE_API, TASK_API, TEMPLATES_API, UPDATE_API, type GenerateRequest, type GenerateResult, type GenerationTask, type HistoryEntry, type HistoryEntryInput, type TemplateListResult, type TemplateRefreshResult, type UpdateInfo } from '../protocol.ts'
6
+ import { CONVERSATION_IMAGE_API, GALLERY_API, GENERATE_API, HISTORY_API, PROMPT_ENHANCE_API, TASK_API, TEMPLATES_API, UPDATE_API, type GenerateRequest, type GenerateResult, type GenerationTask, type HistoryEntry, type HistoryEntryInput, type TemplateListResult, type TemplateRefreshResult, type UpdateInfo } from '../protocol.ts'
7
7
 
8
8
  /** Error carrying the route's JSON error message. */
9
9
  export class ImageGenApiError extends Error {
@@ -84,6 +84,16 @@ export class ImageGenApi {
84
84
  return body.prompt
85
85
  }
86
86
 
87
+ /** Stage a generated image as a durable reference for `/edit_image`. */
88
+ async attachConversationImage(sessionId: string, dataUrl: string, name: string): Promise<void> {
89
+ const response = await fetch(CONVERSATION_IMAGE_API, {
90
+ method: 'POST',
91
+ headers: { 'content-type': 'application/json' },
92
+ body: JSON.stringify({ sessionId, dataUrl, name }),
93
+ })
94
+ await readEnvelope<{ ok: true }>(response)
95
+ }
96
+
87
97
  async taskSubmit(request: GenerateRequest): Promise<GenerationTask> {
88
98
  const response = await fetch(TASK_API.submit, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(request) })
89
99
  return (await readEnvelope<{ ok: true; task: GenerationTask }>(response)).task
@@ -0,0 +1,14 @@
1
+ import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
2
+ import type { ConversationController, IConversation } from '@deepseek-ai/dsh-client-ui-conversation/client'
3
+ import type { SessionId } from '@deepseek-ai/dsh-client-runtime/client'
4
+
5
+ /** Document event used to bridge chat tool results into the image workspace. */
6
+ export const CHAT_IMAGE_EVENT = 'dsh-imagegen:chat-images'
7
+
8
+ export interface ChatImageEventDetail {
9
+ sessionId: SessionId
10
+ refs: readonly ImageAttachmentRef[]
11
+ }
12
+
13
+ /** Composer-facing extension of the narrower public conversation interface. */
14
+ export type ConversationService = IConversation & Pick<ConversationController, 'createDraftImages' | 'releaseDraftImages'>
@@ -1,165 +1,176 @@
1
- /** Inline renderer for image-generation tool-result attachments. */
2
-
3
- import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
4
- import type { ClientContext, SessionId, ToolCallBlock } from '@deepseek-ai/dsh-client-runtime/client'
5
- import { useEffect, useMemo, useState } from 'react'
6
- import { AGENT_IMAGE_API } from '../protocol.ts'
7
- import css from './image-toolview.module.css'
8
-
9
- /** Owner props supplied by the host's keyed tool-call slot. */
10
- export interface ImageToolViewOwnerProps {
11
- callId: string
12
- toolName: string
13
- block: ToolCallBlock
14
- cwd?: string
15
- home?: string
16
- openFile: (path: string) => void
17
- inspect?: () => void
18
- }
19
-
20
- interface LoadedImage {
21
- ref: ImageAttachmentRef
22
- src: string
23
- }
24
-
25
- interface ImageToolViewProps extends ImageToolViewOwnerProps {
26
- sessionId: SessionId
27
- }
28
-
29
- function isSettled(block: ToolCallBlock): block is Extract<ToolCallBlock, { kind: 'tool-result' }> {
30
- return 'kind' in block
31
- }
32
-
33
- function imageRefsOf(block: ToolCallBlock): ImageAttachmentRef[] {
34
- if (!isSettled(block)) return []
35
- const resultContent = block.resultView?.card === 'generic' ? block.resultView.content ?? [] : []
36
- return [...block.content, ...resultContent]
37
- .flatMap(content => content.type === 'image' ? [content.attachment] : [])
38
- }
39
-
40
- function textOf(block: ToolCallBlock): string {
41
- if (!isSettled(block)) return ''
42
- return block.content
43
- .filter(content => content.type === 'text')
44
- .map(content => content.text)
45
- .join('\n')
46
- }
47
-
48
- function resultInfo(block: ToolCallBlock): { status: string; message: string } {
49
- if (!isSettled(block)) return { status: 'running', message: '正在生成图片…' }
50
- const text = textOf(block)
51
- try {
52
- const parsed = JSON.parse(text) as { status?: unknown; message?: unknown }
53
- return {
54
- status: typeof parsed.status === 'string' ? parsed.status : block.isError ? 'failed' : 'completed',
55
- message: typeof parsed.message === 'string' ? parsed.message : '',
56
- }
57
- } catch {
58
- return { status: block.isError ? 'failed' : 'completed', message: text }
59
- }
60
- }
61
-
62
- function statusLabel(status: string): string {
63
- if (status === 'running' || status === 'queued') return '生成中'
64
- if (status === 'failed') return '生成失败'
65
- if (status === 'cancelled') return '已取消'
66
- return '图片结果'
67
- }
68
-
69
- function useAttachmentImages(
70
- sessionId: SessionId,
71
- refs: ImageAttachmentRef[],
72
- load: (sessionId: SessionId, ref: ImageAttachmentRef) => Promise<string>,
73
- ): { images: LoadedImage[]; error: string | null } {
74
- const key = useMemo(() => refs.map(ref => String(ref.attachmentId)).join('|'), [refs])
75
- const [images, setImages] = useState<LoadedImage[]>([])
76
- const [error, setError] = useState<string | null>(null)
77
-
78
- useEffect(() => {
79
- let disposed = false
80
- const urls: string[] = []
81
- const revoke = (): void => {
82
- for (const url of urls) URL.revokeObjectURL(url)
83
- urls.length = 0
84
- }
85
-
86
- setImages([])
87
- setError(null)
88
- if (refs.length === 0) return () => { /* no attachments to clean up */ }
89
-
90
- void Promise.all(refs.map(async ref => {
91
- const src = await load(sessionId, ref)
92
- urls.push(src)
93
- return { ref, src }
94
- }))
95
- .then(next => {
96
- if (!disposed) setImages(next)
97
- })
98
- .catch(errorValue => {
99
- revoke()
100
- if (!disposed) setError(errorValue instanceof Error ? errorValue.message : String(errorValue))
101
- })
102
-
103
- return () => {
104
- disposed = true
105
- revoke()
106
- }
107
- }, [key, load, refs, sessionId])
108
-
109
- return { images, error }
110
- }
111
-
112
- /** Register the inline image result view for all image-generation result tools. */
113
- export function registerImageToolviews(ctx: ClientContext): void {
114
- const load = async (_sessionId: SessionId, ref: ImageAttachmentRef): Promise<string> => {
115
- // Tool-result images intentionally do not occur in model-visible session
116
- // content, so session.readAttachment() rejects them. The plugin route
117
- // reads the same durable attachment after validating the complete ref.
118
- const query = new URLSearchParams({
119
- attachment_id: String(ref.attachmentId),
120
- media_type: ref.mediaType,
121
- bytes: String(ref.bytes),
122
- width: String(ref.width),
123
- height: String(ref.height),
124
- })
125
- const response = await fetch(`${AGENT_IMAGE_API}?${query.toString()}`)
126
- if (!response.ok) throw new Error(`无法读取图片附件(HTTP ${response.status})。`)
127
- const blob = await response.blob()
128
- return URL.createObjectURL(blob)
129
- }
130
-
131
- const ImageToolView = (props: ImageToolViewProps): React.JSX.Element => {
132
- const refs = useMemo(() => imageRefsOf(props.block), [props.block])
133
- const { status, message } = resultInfo(props.block)
134
- const { images, error } = useAttachmentImages(props.sessionId, refs, load)
135
-
136
- return <section className={css.root} data-state={status} data-tool={props.toolName}>
137
- <header className={css.header}>
138
- <span className={css.icon} aria-hidden="true">▧</span>
139
- <strong>{props.toolName}</strong>
140
- <span className={css.status}>{statusLabel(status)}</span>
141
- </header>
142
- {message !== '' && <p className={css.message}>{message}</p>}
143
- {images.length > 0 && <div className={css.images}>
144
- {images.map(image => <a
145
- className={css.imageLink}
146
- href={image.src}
147
- key={String(image.ref.attachmentId)}
148
- rel="noreferrer"
149
- target="_blank"
150
- title="打开原图"
151
- >
152
- <img className={css.image} src={image.src} alt={image.ref.name ?? '生成图片'} />
153
- </a>)}
154
- </div>}
155
- {refs.length > 0 && images.length === 0 && error === null && <p className={css.loading}>正在加载图片…</p>}
156
- {error !== null && <p className={css.error}>{error}</p>}
157
- </section>
158
- }
159
-
160
- ctx.slots.inject('tool.call.toolview', function* () {
161
- for (const key of ['generate_image', 'edit_image', 'get_image_generation_task']) {
162
- yield ctx.slots.register({ name: 'tool.call.toolview', key }, ImageToolView)
163
- }
164
- })
165
- }
1
+ /** Inline renderer for image-generation tool-result attachments. */
2
+
3
+ import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
4
+ import type { ClientContext, SessionId, ToolCallBlock } from '@deepseek-ai/dsh-client-runtime/client'
5
+ import { useEffect, useMemo, useState } from 'react'
6
+ import { AGENT_IMAGE_API } from '../protocol.ts'
7
+ import { CHAT_IMAGE_EVENT } from './conversation-sync.ts'
8
+ import css from './image-toolview.module.css'
9
+
10
+ /** Owner props supplied by the host's keyed tool-call slot. */
11
+ export interface ImageToolViewOwnerProps {
12
+ callId: string
13
+ toolName: string
14
+ block: ToolCallBlock
15
+ cwd?: string
16
+ home?: string
17
+ openFile: (path: string) => void
18
+ inspect?: () => void
19
+ }
20
+
21
+ interface LoadedImage {
22
+ ref: ImageAttachmentRef
23
+ src: string
24
+ }
25
+
26
+ interface ImageToolViewProps extends ImageToolViewOwnerProps {
27
+ sessionId: SessionId
28
+ }
29
+
30
+ function isSettled(block: ToolCallBlock): block is Extract<ToolCallBlock, { kind: 'tool-result' }> {
31
+ return 'kind' in block
32
+ }
33
+
34
+ function imageRefsOf(block: ToolCallBlock): ImageAttachmentRef[] {
35
+ if (!isSettled(block)) return []
36
+ const resultContent = block.resultView?.card === 'generic' ? block.resultView.content ?? [] : []
37
+ return [...block.content, ...resultContent]
38
+ .flatMap(content => content.type === 'image' ? [content.attachment] : [])
39
+ }
40
+
41
+ function textOf(block: ToolCallBlock): string {
42
+ if (!isSettled(block)) return ''
43
+ return block.content
44
+ .filter(content => content.type === 'text')
45
+ .map(content => content.text)
46
+ .join('\n')
47
+ }
48
+
49
+ function resultInfo(block: ToolCallBlock): { status: string; message: string } {
50
+ if (!isSettled(block)) return { status: 'running', message: '正在生成图片…' }
51
+ const text = textOf(block)
52
+ try {
53
+ const parsed = JSON.parse(text) as { status?: unknown; message?: unknown }
54
+ return {
55
+ status: typeof parsed.status === 'string' ? parsed.status : block.isError ? 'failed' : 'completed',
56
+ message: typeof parsed.message === 'string' ? parsed.message : '',
57
+ }
58
+ } catch {
59
+ return { status: block.isError ? 'failed' : 'completed', message: text }
60
+ }
61
+ }
62
+
63
+ function statusLabel(status: string): string {
64
+ if (status === 'running' || status === 'queued') return '生成中'
65
+ if (status === 'failed') return '生成失败'
66
+ if (status === 'cancelled') return '已取消'
67
+ return '图片结果'
68
+ }
69
+
70
+ function useAttachmentImages(
71
+ sessionId: SessionId,
72
+ refs: ImageAttachmentRef[],
73
+ load: (sessionId: SessionId, ref: ImageAttachmentRef) => Promise<string>,
74
+ ): { images: LoadedImage[]; error: string | null } {
75
+ const key = useMemo(() => refs.map(ref => String(ref.attachmentId)).join('|'), [refs])
76
+ const [images, setImages] = useState<LoadedImage[]>([])
77
+ const [error, setError] = useState<string | null>(null)
78
+
79
+ useEffect(() => {
80
+ let disposed = false
81
+ const urls: string[] = []
82
+ const revoke = (): void => {
83
+ for (const url of urls) URL.revokeObjectURL(url)
84
+ urls.length = 0
85
+ }
86
+
87
+ setImages([])
88
+ setError(null)
89
+ if (refs.length === 0) return () => { /* no attachments to clean up */ }
90
+
91
+ void Promise.all(refs.map(async ref => {
92
+ const src = await load(sessionId, ref)
93
+ urls.push(src)
94
+ return { ref, src }
95
+ }))
96
+ .then(next => {
97
+ if (!disposed) setImages(next)
98
+ })
99
+ .catch(errorValue => {
100
+ revoke()
101
+ if (!disposed) setError(errorValue instanceof Error ? errorValue.message : String(errorValue))
102
+ })
103
+
104
+ return () => {
105
+ disposed = true
106
+ revoke()
107
+ }
108
+ }, [key, load, refs, sessionId])
109
+
110
+ return { images, error }
111
+ }
112
+
113
+ /** Register the inline image result view for all image-generation result tools. */
114
+ export function registerImageToolviews(ctx: ClientContext): void {
115
+ const load = async (_sessionId: SessionId, ref: ImageAttachmentRef): Promise<string> => {
116
+ // Tool-result images intentionally do not occur in model-visible session
117
+ // content, so session.readAttachment() rejects them. The plugin route
118
+ // reads the same durable attachment after validating the complete ref.
119
+ const query = new URLSearchParams({
120
+ attachment_id: String(ref.attachmentId),
121
+ media_type: ref.mediaType,
122
+ bytes: String(ref.bytes),
123
+ width: String(ref.width),
124
+ height: String(ref.height),
125
+ })
126
+ const response = await fetch(`${AGENT_IMAGE_API}?${query.toString()}`)
127
+ if (!response.ok) throw new Error(`无法读取图片附件(HTTP ${response.status})。`)
128
+ const blob = await response.blob()
129
+ return URL.createObjectURL(blob)
130
+ }
131
+
132
+ const ImageToolView = (props: ImageToolViewProps): React.JSX.Element => {
133
+ const refs = useMemo(() => imageRefsOf(props.block), [props.block])
134
+ const { status, message } = resultInfo(props.block)
135
+ const { images, error } = useAttachmentImages(props.sessionId, refs, load)
136
+
137
+ useEffect(() => {
138
+ if (images.length === 0) return
139
+ document.dispatchEvent(new CustomEvent(CHAT_IMAGE_EVENT, {
140
+ detail: {
141
+ sessionId: props.sessionId,
142
+ refs: images.map(image => image.ref),
143
+ },
144
+ }))
145
+ }, [images, props.sessionId])
146
+
147
+ return <section className={css.root} data-state={status} data-tool={props.toolName}>
148
+ <header className={css.header}>
149
+ <span className={css.icon} aria-hidden="true">▧</span>
150
+ <strong>{props.toolName}</strong>
151
+ <span className={css.status}>{statusLabel(status)}</span>
152
+ </header>
153
+ {message !== '' && <p className={css.message}>{message}</p>}
154
+ {images.length > 0 && <div className={css.images}>
155
+ {images.map(image => <a
156
+ className={css.imageLink}
157
+ href={image.src}
158
+ key={String(image.ref.attachmentId)}
159
+ rel="noreferrer"
160
+ target="_blank"
161
+ title="打开原图"
162
+ >
163
+ <img className={css.image} src={image.src} alt={image.ref.name ?? '生成图片'} />
164
+ </a>)}
165
+ </div>}
166
+ {refs.length > 0 && images.length === 0 && error === null && <p className={css.loading}>正在加载图片…</p>}
167
+ {error !== null && <p className={css.error}>{error}</p>}
168
+ </section>
169
+ }
170
+
171
+ ctx.slots.inject('tool.call.toolview', function* () {
172
+ for (const key of ['generate_image', 'edit_image', 'get_image_generation_task']) {
173
+ yield ctx.slots.register({ name: 'tool.call.toolview', key }, ImageToolView)
174
+ }
175
+ })
176
+ }
@@ -12,6 +12,7 @@
12
12
  * the GUI down.
13
13
  */
14
14
  import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
15
+ import type { ISessions } from '@deepseek-ai/dsh-client-runtime/client'
15
16
  import type { ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client'
16
17
  // Type-only: pulls the locale plugin's Context merge (ctx.locale).
17
18
  import type {} from '@deepseek-ai/dsh-client-locale/client'
@@ -23,9 +24,10 @@ import { tt } from './helpers.ts'
23
24
  import { en, zh, type ImageGenKey } from './locales.ts'
24
25
  import { mountPanel } from './mount.tsx'
25
26
  import { mountSidebarEntry } from './sidebar-entry.ts'
26
- import { ImageGenSettingsCard, ImageGenSettingsCardController } from './SettingsCard.tsx'
27
- import { bindImageGenScope, type ImageGenScope } from './settings-scope.ts'
28
- import { registerImageToolviews, type ImageToolViewOwnerProps } from './image-toolview.tsx'
27
+ import { ImageGenSettingsCard, ImageGenSettingsCardController } from './SettingsCard.tsx'
28
+ import { bindImageGenScope, type ImageGenScope } from './settings-scope.ts'
29
+ import { registerImageToolviews, type ImageToolViewOwnerProps } from './image-toolview.tsx'
30
+ import type { ConversationService } from './conversation-sync.ts'
29
31
 
30
32
  /** Locale namespace this plugin owns. */
31
33
  const NS = 'dsh-imagegen'
@@ -36,7 +38,7 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
36
38
  'dsh-imagegen': ImageGenKey
37
39
  }
38
40
 
39
- interface SlotMap {
41
+ interface SlotMap {
40
42
  /**
41
43
  * The official plugin-configuration slot the Settings → Plugins →
42
44
  * Configurable tab declares and renders. This card registers there as its
@@ -45,11 +47,11 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
45
47
  * same shape so this package can register without depending on the
46
48
  * sibling UI package.
47
49
  */
48
- 'settings.plugin.item': { kind: 'keyed'; scope: 'root'; owner: ImageGenPluginItemOwnerProps }
49
- /** Image-generation results render their durable image blocks inline. */
50
- 'tool.call.toolview': { kind: 'keyed'; scope: 'session'; owner: ImageToolViewOwnerProps }
51
- }
52
- }
50
+ 'settings.plugin.item': { kind: 'keyed'; scope: 'root'; owner: ImageGenPluginItemOwnerProps }
51
+ /** Image-generation results render their durable image blocks inline. */
52
+ 'tool.call.toolview': { kind: 'keyed'; scope: 'session'; owner: ImageToolViewOwnerProps }
53
+ }
54
+ }
53
55
 
54
56
  /** Owner share of a plugin card (the section supplies nothing). */
55
57
  export interface ImageGenPluginItemOwnerProps {
@@ -58,15 +60,15 @@ export interface ImageGenPluginItemOwnerProps {
58
60
  }
59
61
 
60
62
  /** Required services (fiber inject waiting — the runtime must be up first). */
61
- export const inject = ['slots', 'locale', 'connection', 'sessions']
63
+ export const inject = ['slots', 'locale', 'connection', 'sessions', 'conversation']
62
64
 
63
65
  /**
64
66
  * Mount the studio, its sidebar entry, and the settings card.
65
67
  * @param ctx - client root context (services: slots, locale, connection).
66
68
  */
67
- export function apply(ctx: ClientContext): void {
68
- ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-imagegen: dictionaries')
69
- registerImageToolviews(ctx)
69
+ export function apply(ctx: ClientContext): void {
70
+ ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-imagegen: dictionaries')
71
+ registerImageToolviews(ctx)
70
72
 
71
73
  const connection = ctx.get('connection') as ConnectionHandle | undefined
72
74
  const loopback = connection?.isLoopback === true
@@ -104,10 +106,18 @@ export function apply(ctx: ClientContext): void {
104
106
  if (uiDisposer !== undefined) return
105
107
  const controller = new ImageGenController()
106
108
  const api = new ImageGenApi()
109
+ const sessions = ctx.get('sessions') as ISessions | undefined
110
+ const conversation = ctx.get('conversation') as ConversationService | undefined
107
111
  const disposers: Array<() => void> = []
108
112
  try {
109
- disposers.push(mountSidebarEntry(controller, tt('entry.label'), tt('entry.tooltip')))
110
- disposers.push(mountPanel(controller, api, scope))
113
+ disposers.push(mountSidebarEntry(
114
+ controller,
115
+ tt('entry.newSession'),
116
+ tt('entry.newSessionTooltip'),
117
+ tt('entry.image'),
118
+ tt('entry.tooltip'),
119
+ ))
120
+ disposers.push(mountPanel(controller, api, scope, { sessions, conversation }))
111
121
  } catch (error) {
112
122
  // DOM failures degrade the studio, never the GUI.
113
123
  console.warn('[dsh-imagegen] mount failed:', error)