@dickpy/dsh-imagegen 1.3.0 → 1.4.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 (45) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +203 -182
  3. package/cordis.patch.yml +8 -8
  4. package/docs/images/multi-model-comparison.png +0 -0
  5. package/lib/client.js +1103 -837
  6. package/lib/client.js.map +1 -1
  7. package/lib/index.js +265 -135
  8. package/package.json +70 -68
  9. package/src/agent-image-tools.ts +418 -418
  10. package/src/client/ImageGenPanel.tsx +1699 -1508
  11. package/src/client/SettingsCard.tsx +936 -957
  12. package/src/client/TemplateLibrary.tsx +336 -336
  13. package/src/client/api.ts +193 -193
  14. package/src/client/channels-form.ts +263 -263
  15. package/src/client/controller.ts +46 -46
  16. package/src/client/conversation-sync.ts +14 -0
  17. package/src/client/css-modules.d.ts +5 -5
  18. package/src/client/helpers.ts +33 -33
  19. package/src/client/image-toolview.module.css +73 -73
  20. package/src/client/image-toolview.tsx +169 -158
  21. package/src/client/index.ts +32 -22
  22. package/src/client/locales.ts +610 -594
  23. package/src/client/mount.tsx +185 -96
  24. package/src/client/panel.module.css +1713 -1445
  25. package/src/client/settings-card.module.css +1023 -1023
  26. package/src/client/settings-form.ts +336 -336
  27. package/src/client/settings-scope.ts +298 -298
  28. package/src/client/sidebar-entry.ts +148 -102
  29. package/src/client/templates.module.css +453 -453
  30. package/src/engine.ts +520 -478
  31. package/src/gallery-store.ts +286 -286
  32. package/src/generation-runtime.ts +79 -75
  33. package/src/history-store.ts +250 -244
  34. package/src/image-format.ts +11 -11
  35. package/src/image-models.ts +19 -19
  36. package/src/index.ts +318 -318
  37. package/src/model-catalog.ts +115 -98
  38. package/src/presets.ts +71 -63
  39. package/src/prompt-enhancer.ts +137 -79
  40. package/src/protocol.ts +338 -326
  41. package/src/routes.ts +916 -906
  42. package/src/task-queue.ts +113 -103
  43. package/src/templates/cases.json +10196 -10196
  44. package/src/templates-store.ts +278 -278
  45. package/src/updater.ts +117 -117
@@ -1,33 +1,33 @@
1
- /**
2
- * Shared panel helpers: the active-dictionary pick (document-language based,
3
- * dsh-ssh precedent) bound to the dsh-imagegen interpolator, plus a small
4
- * error-message extractor. All copy stays in the locale dictionaries.
5
- */
6
-
7
- import { en, zh, type ImageGenKey } from './locales.ts'
8
-
9
- /** Template values accepted by the interpolator. */
10
- export type TranslateValues = Record<string, string | number>
11
-
12
- /** Active dictionary, picked by the document language at call time. */
13
- export function dictionary(): Record<string, string> {
14
- const lang = typeof document !== 'undefined' ? document.documentElement.lang : 'zh'
15
- return lang.toLowerCase().startsWith('en') ? { ...en } : { ...zh }
16
- }
17
-
18
- /** Translate a key with optional {name} template params (current language). */
19
- export function tt(key: ImageGenKey, values?: TranslateValues): string {
20
- const text = dictionary()[key] ?? key
21
- if (values === undefined) return text
22
- let rendered = text
23
- for (const [name, value] of Object.entries(values)) {
24
- rendered = rendered.replaceAll(`{${name}}`, String(value))
25
- }
26
- return rendered
27
- }
28
-
29
- /** Human-readable error text from an unknown thrown value. */
30
- export function errorMessage(error: unknown): string {
31
- if (error instanceof Error) return error.message
32
- return String(error)
33
- }
1
+ /**
2
+ * Shared panel helpers: the active-dictionary pick (document-language based,
3
+ * dsh-ssh precedent) bound to the dsh-imagegen interpolator, plus a small
4
+ * error-message extractor. All copy stays in the locale dictionaries.
5
+ */
6
+
7
+ import { en, zh, type ImageGenKey } from './locales.ts'
8
+
9
+ /** Template values accepted by the interpolator. */
10
+ export type TranslateValues = Record<string, string | number>
11
+
12
+ /** Active dictionary, picked by the document language at call time. */
13
+ export function dictionary(): Record<string, string> {
14
+ const lang = typeof document !== 'undefined' ? document.documentElement.lang : 'zh'
15
+ return lang.toLowerCase().startsWith('en') ? { ...en } : { ...zh }
16
+ }
17
+
18
+ /** Translate a key with optional {name} template params (current language). */
19
+ export function tt(key: ImageGenKey, values?: TranslateValues): string {
20
+ const text = dictionary()[key] ?? key
21
+ if (values === undefined) return text
22
+ let rendered = text
23
+ for (const [name, value] of Object.entries(values)) {
24
+ rendered = rendered.replaceAll(`{${name}}`, String(value))
25
+ }
26
+ return rendered
27
+ }
28
+
29
+ /** Human-readable error text from an unknown thrown value. */
30
+ export function errorMessage(error: unknown): string {
31
+ if (error instanceof Error) return error.message
32
+ return String(error)
33
+ }
@@ -1,73 +1,73 @@
1
- .root {
2
- display: flex;
3
- flex-direction: column;
4
- gap: 7px;
5
- margin: 4px 0;
6
- padding: 8px 10px 10px;
7
- border: 1px solid var(--dsw-alias-border-l1);
8
- border-radius: 8px;
9
- background: var(--dsw-alias-bg-layer-1);
10
- }
11
-
12
- .header {
13
- display: flex;
14
- align-items: center;
15
- gap: 7px;
16
- min-height: 20px;
17
- color: var(--dsw-alias-label-secondary);
18
- font-size: 12px;
19
- }
20
-
21
- .icon {
22
- color: var(--dsw-alias-brand-primary);
23
- font-size: 15px;
24
- line-height: 1;
25
- }
26
-
27
- .status {
28
- margin-left: auto;
29
- color: var(--dsw-alias-label-tertiary);
30
- font-size: 11px;
31
- }
32
-
33
- .message,
34
- .loading,
35
- .error {
36
- margin: 0;
37
- color: var(--dsw-alias-label-tertiary);
38
- font-size: 12px;
39
- line-height: 1.5;
40
- overflow-wrap: anywhere;
41
- }
42
-
43
- .images {
44
- display: flex;
45
- flex-wrap: wrap;
46
- gap: 8px;
47
- }
48
-
49
- .imageLink {
50
- display: block;
51
- max-width: min(280px, 100%);
52
- overflow: hidden;
53
- border: 1px solid var(--dsw-alias-border-l1);
54
- border-radius: 6px;
55
- background: var(--dsw-alias-bg-base);
56
- }
57
-
58
- .imageLink:hover {
59
- border-color: var(--dsw-alias-brand-primary);
60
- }
61
-
62
- .image {
63
- display: block;
64
- width: auto;
65
- max-width: 280px;
66
- height: auto;
67
- max-height: 220px;
68
- object-fit: contain;
69
- }
70
-
71
- .error {
72
- color: var(--dsw-alias-state-error-primary);
73
- }
1
+ .root {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: 7px;
5
+ margin: 4px 0;
6
+ padding: 8px 10px 10px;
7
+ border: 1px solid var(--dsw-alias-border-l1);
8
+ border-radius: 8px;
9
+ background: var(--dsw-alias-bg-layer-1);
10
+ }
11
+
12
+ .header {
13
+ display: flex;
14
+ align-items: center;
15
+ gap: 7px;
16
+ min-height: 20px;
17
+ color: var(--dsw-alias-label-secondary);
18
+ font-size: 12px;
19
+ }
20
+
21
+ .icon {
22
+ color: var(--dsw-alias-brand-primary);
23
+ font-size: 15px;
24
+ line-height: 1;
25
+ }
26
+
27
+ .status {
28
+ margin-left: auto;
29
+ color: var(--dsw-alias-label-tertiary);
30
+ font-size: 11px;
31
+ }
32
+
33
+ .message,
34
+ .loading,
35
+ .error {
36
+ margin: 0;
37
+ color: var(--dsw-alias-label-tertiary);
38
+ font-size: 12px;
39
+ line-height: 1.5;
40
+ overflow-wrap: anywhere;
41
+ }
42
+
43
+ .images {
44
+ display: flex;
45
+ flex-wrap: wrap;
46
+ gap: 8px;
47
+ }
48
+
49
+ .imageLink {
50
+ display: block;
51
+ max-width: min(280px, 100%);
52
+ overflow: hidden;
53
+ border: 1px solid var(--dsw-alias-border-l1);
54
+ border-radius: 6px;
55
+ background: var(--dsw-alias-bg-base);
56
+ }
57
+
58
+ .imageLink:hover {
59
+ border-color: var(--dsw-alias-brand-primary);
60
+ }
61
+
62
+ .image {
63
+ display: block;
64
+ width: auto;
65
+ max-width: 280px;
66
+ height: auto;
67
+ max-height: 220px;
68
+ object-fit: contain;
69
+ }
70
+
71
+ .error {
72
+ color: var(--dsw-alias-state-error-primary);
73
+ }
@@ -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'
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
5
  import { useEffect, useMemo, useState } from 'react'
6
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 => {
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 => {
132
133
  const refs = useMemo(() => imageRefsOf(props.block), [props.block])
133
134
  const { status, message } = resultInfo(props.block)
134
135
  const { images, error } = useAttachmentImages(props.sessionId, refs, load)
135
136
 
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
- }
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])
159
146
 
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
- }
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
+ }
@@ -11,8 +11,9 @@
11
11
  * whole boot when a plugin apply throws, and an external plugin must not take
12
12
  * the GUI down.
13
13
  */
14
- import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
15
- import type { ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client'
14
+ import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
15
+ import type { ISessions } from '@deepseek-ai/dsh-client-runtime/client'
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'
18
19
  // Type-only: pulls the LocaleNamespaceMap merge table.
@@ -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'
27
+ import { ImageGenSettingsCard, ImageGenSettingsCardController } from './SettingsCard.tsx'
28
+ import { bindImageGenScope, type ImageGenScope } from './settings-scope.ts'
28
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
@@ -100,14 +102,22 @@ export function apply(ctx: ClientContext): void {
100
102
  // the scope is still loading, the composition default is unknown, so nothing
101
103
  // mounts yet. Only an unavailable scope falls back to the default (enabled).
102
104
  let uiDisposer: (() => void) | undefined
103
- const mountUi = (): void => {
104
- if (uiDisposer !== undefined) return
105
- const controller = new ImageGenController()
106
- const api = new ImageGenApi()
107
- const disposers: Array<() => void> = []
108
- try {
109
- disposers.push(mountSidebarEntry(controller, tt('entry.label'), tt('entry.tooltip')))
110
- disposers.push(mountPanel(controller, api, scope))
105
+ const mountUi = (): void => {
106
+ if (uiDisposer !== undefined) return
107
+ const controller = new ImageGenController()
108
+ const api = new ImageGenApi()
109
+ const sessions = ctx.get('sessions') as ISessions | undefined
110
+ const conversation = ctx.get('conversation') as ConversationService | undefined
111
+ const disposers: Array<() => void> = []
112
+ try {
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)