@sparkelf/dsh-patch-document-attachments 0.1.0-rc.11 → 0.1.0-rc.12

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.
@@ -1,11 +1,58 @@
1
+ diff --git a/packages/api/session-controller/src/client/contract/session.ts b/packages/api/session-controller/src/client/contract/session.ts
2
+ index 7810249c46..db2c674ce2 100644
3
+ --- a/packages/api/session-controller/src/client/contract/session.ts
4
+ +++ b/packages/api/session-controller/src/client/contract/session.ts
5
+ @@ -7,7 +7,7 @@
6
+ * must stub); implementation-internal entry points (history staging, wire-frame
7
+ * dispatch) stay on the class, invisible out here.
8
+ */
9
+ -import type { AttachmentIdType, ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
10
+ +import type { AttachmentIdType, FileAttachmentRef, ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
11
+ import type { MessageId } from '@deepseek-ai/dsh-llm/brand'
12
+ import type { SessionId } from '@deepseek-ai/dsh-session/types'
13
+ import type { RemoteResult } from '@deepseek-ai/dsh-typert-protocol'
14
+ @@ -85,13 +85,13 @@ export interface ISession {
15
+ requestId?: SessionRequestId,
16
+ ): Promise<RemoteResult<{ accepted: true }>>
17
+ /**
18
+ - * Resolve one durable image referenced by this session.
19
+ + * Resolve one durable file referenced by this session.
20
+ * @param attachmentId - opaque id found in the folded session log.
21
+ * @returns the authenticated reference and decoded bytes.
22
+ */
23
+ readAttachment(
24
+ attachmentId: AttachmentIdType,
25
+ - ): Promise<RemoteResult<{ attachment: ImageAttachmentRef; data: Uint8Array }>>
26
+ + ): Promise<RemoteResult<{ attachment: FileAttachmentRef; data: Uint8Array }>>
27
+ /**
28
+ * Apply one edit, remove, or strict steer action to a still-pending queue occurrence.
29
+ * @param itemId - agent-owned inbox occurrence identity.
1
30
  diff --git a/packages/api/session-controller/src/client/sessions/session.ts b/packages/api/session-controller/src/client/sessions/session.ts
2
- index 815cf89c40e404a32dd1ec8c20568fc15995a73d..9e03894a8d895bc73fd5ea8ac853b0073b7c70dd 100644
31
+ index 8af47b5aad..3599956698 100644
3
32
  --- a/packages/api/session-controller/src/client/sessions/session.ts
4
33
  +++ b/packages/api/session-controller/src/client/sessions/session.ts
5
- @@ -215,27 +215,19 @@ export class Session implements SessionFace {
34
+ @@ -2,7 +2,7 @@
35
+
36
+ import type { Context } from '@deepseek-ai/cordis'
37
+ import { randomUUID } from '@deepseek-ai/dsh-util-crypto'
38
+ -import type { AttachmentIdType, ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
39
+ +import type { AttachmentIdType, FileAttachmentRef, ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
40
+ import type { SubagentAddress } from '@deepseek-ai/dsh-subagent/client'
41
+ import type { MessageId } from '@deepseek-ai/dsh-llm/brand'
42
+ import type { SessionId } from '@deepseek-ai/dsh-session/types'
43
+ @@ -28,7 +28,7 @@ import type {
44
+ } from '../contract/events.ts'
45
+ import { Notifier } from './notifier.ts'
46
+ import { isRemoteFailure } from '@deepseek-ai/dsh-api-gateway/client'
47
+ -import type { RemoteFailure, RemoteResult } from '@deepseek-ai/dsh-typert-protocol'
48
+ +import { RemoteError, type RemoteFailure, type RemoteResult } from '@deepseek-ai/dsh-typert-protocol'
49
+ import type { SessionRemotes } from './remotes.ts'
50
+ import { ProjectionValueStore } from './projection-store.ts'
51
+ import type { ProjectionsBaseline } from './projection-store.ts'
52
+ @@ -210,25 +210,17 @@ export class Session implements SessionFace {
6
53
  signal?: AbortSignal,
7
54
  requestId?: SessionRequestId,
8
- ): Promise<ClientResult<{ accepted: true }>> {
55
+ ): Promise<RemoteResult<{ accepted: true }>> {
9
56
  - this.promptError = null
10
57
  - this.lastAgentError = null
11
58
  - // Synchronous, before the first await: the blank → engaging edge must be
@@ -14,72 +61,36 @@ index 815cf89c40e404a32dd1ec8c20568fc15995a73d..9e03894a8d895bc73fd5ea8ac853b007
14
61
  - this.promptAttempted = true
15
62
  - if (this.blankBit) this.firstPromptPendingTurn = true
16
63
  - this.notifier.markDirty()
17
- - let result: ClientResult<{ accepted: true }>
18
- - try {
64
+ - let result: RemoteResult<{ accepted: true }>
65
+ - if (this.address === undefined) {
66
+ - const clientTimeZone = resolvedClientTimeZone()
67
+ - result = await this.remote.session.prompt({
68
+ - requestId: requestId ?? randomUUID() as SessionRequestId,
69
+ - sessionId: this.sessionId,
70
+ - mode,
71
+ - content,
72
+ - clientTimeZone,
73
+ - }, signal)
74
+ - } else {
19
75
  + return this.settlePrompt(requestId, async () => {
20
- if (this.address === undefined) {
21
- const clientTimeZone = resolvedClientTimeZone()
22
- - result = toSessionResult(await this.remote.session.prompt({
23
- + return toSessionResult(await this.remote.session.prompt({
24
- requestId: requestId ?? randomUUID() as SessionRequestId,
25
- sessionId: this.sessionId,
26
- mode,
27
- content,
28
- clientTimeZone,
29
- }, signal))
30
- - } else if (this.address.mode === 'one-shot') {
31
- - result = {
32
- + }
33
- + if (this.address.mode === 'one-shot') {
34
- + return {
35
- ok: false,
36
- error: {
37
- code: 'subagent-not-resumable',
38
- @@ -243,30 +235,64 @@ export class Session implements SessionFace {
39
- details: { childSessionId: this.address.childSessionId },
40
- },
41
- }
42
- - } else {
43
- - if (content.some(part => part.type === 'image')) {
44
- - result = {
45
- - ok: false,
46
- - error: {
47
- - code: 'attachment-error',
48
- - message: 'Image input is unavailable for subagent continuations.',
49
- - details: { reason: 'SUBAGENT_IMAGE_UNSUPPORTED' },
50
- - },
51
- - }
52
- - } else {
53
- - const routed = toSessionResult(await this.remote.subagents.prompt({
54
- - requestId: randomUUID() as SessionRequestId,
55
- - parentSessionId: this.address.parentSessionId,
56
- - childSessionId: this.address.childSessionId,
57
- - mode: this.address.mode,
58
- - content: content.flatMap(part => part.type === 'text'
59
- - ? [{ type: 'text' as const, text: part.text }]
60
- - : []),
61
- - clientTimeZone: resolvedClientTimeZone(),
62
- - }, signal))
63
- - result = routed.ok ? { ok: true, value: { accepted: true } } : routed
76
+ + if (this.address === undefined) {
77
+ + const clientTimeZone = resolvedClientTimeZone()
78
+ + return this.remote.session.prompt({
79
+ + requestId: requestId ?? randomUUID() as SessionRequestId,
80
+ + sessionId: this.sessionId,
81
+ + mode,
82
+ + content,
83
+ + clientTimeZone,
84
+ + }, signal)
64
85
  + }
65
- + if (content.some(part => part.type === 'image')) {
66
- + return {
67
- + ok: false,
68
- + error: {
69
- + code: 'attachment-error',
70
- + message: 'Image input is unavailable for subagent continuations.',
71
- + details: { reason: 'SUBAGENT_IMAGE_UNSUPPORTED' },
72
- + },
73
- }
74
- }
75
- + const routed = toSessionResult(await this.remote.subagents.prompt({
76
- + requestId: randomUUID() as SessionRequestId,
77
- + parentSessionId: this.address.parentSessionId,
78
- + childSessionId: this.address.childSessionId,
79
- + mode: this.address.mode,
80
- + content: content.flatMap(part => part.type === 'text' ? [{ type: 'text' as const, text: part.text }] : []),
81
- + clientTimeZone: resolvedClientTimeZone(),
82
- + }, signal))
86
+ const routed = await this.remote.subagents.prompt({
87
+ requestId: randomUUID() as SessionRequestId,
88
+ parentSessionId: this.address.parentSessionId,
89
+ @@ -237,22 +229,49 @@ export class Session implements SessionFace {
90
+ content,
91
+ clientTimeZone: resolvedClientTimeZone(),
92
+ }, signal)
93
+ - result = routed.ok ? { ok: true, value: { accepted: true } } : routed
83
94
  + return routed.ok ? { ok: true, value: { accepted: true } } : routed
84
95
  + })
85
96
  + }
@@ -92,37 +103,34 @@ index 815cf89c40e404a32dd1ec8c20568fc15995a73d..9e03894a8d895bc73fd5ea8ac853b007
92
103
  + */
93
104
  + promptPrepared(
94
105
  + requestId: SessionRequestId,
95
- + submit: () => Promise<ClientResult<{ accepted: true }>>,
96
- + ): Promise<ClientResult<{ accepted: true }>> {
106
+ + submit: () => Promise<RemoteResult<{ accepted: true }>>,
107
+ + ): Promise<RemoteResult<{ accepted: true }>> {
97
108
  + if (this.address !== undefined) {
98
109
  + return this.settlePrompt(requestId, async () => ({
99
110
  + ok: false,
100
- + error: {
101
- + code: 'attachment-error',
102
- + message: 'Document input is unavailable for subagent continuations.',
103
- + details: { reason: 'SUBAGENT_DOCUMENT_UNSUPPORTED' },
104
- + },
111
+ + error: new RemoteError(
112
+ + 'session/attachment-invalid',
113
+ + 'Document input is unavailable for subagent continuations.',
114
+ + { reason: 'SUBAGENT_DOCUMENT_UNSUPPORTED' },
115
+ + ),
105
116
  + }))
106
- + }
117
+ }
107
118
  + return this.settlePrompt(requestId, submit)
108
119
  + }
109
120
  +
110
121
  + private async settlePrompt(
111
122
  + requestId: SessionRequestId | undefined,
112
- + submit: () => Promise<ClientResult<{ accepted: true }>>,
113
- + ): Promise<ClientResult<{ accepted: true }>> {
123
+ + submit: () => Promise<RemoteResult<{ accepted: true }>>,
124
+ + ): Promise<RemoteResult<{ accepted: true }>> {
114
125
  + this.promptError = null
115
126
  + this.lastAgentError = null
116
127
  + this.promptAttempted = true
117
128
  + if (this.blankBit) this.firstPromptPendingTurn = true
118
129
  + this.notifier.markDirty()
119
- + let result: ClientResult<{ accepted: true }>
120
- + try {
121
- + result = await submit()
122
- } catch (error) {
123
- result = transportResult(error)
124
- }
125
- @@ -276,14 +302,6 @@ export class Session implements SessionFace {
130
+ + const result = await submit()
131
+ if (!result.ok) {
132
+ if (requestId !== undefined) this.retireFailedSubmission(requestId)
133
+ this.promptError = { op: 'send', error: result.error }
126
134
  this.notifier.markDirty()
127
135
  return result
128
136
  }
@@ -137,28 +145,45 @@ index 815cf89c40e404a32dd1ec8c20568fc15995a73d..9e03894a8d895bc73fd5ea8ac853b007
137
145
  if (this.blankBit) {
138
146
  this.blankBit = false
139
147
  this.options.onEngaged?.(this)
148
+ @@ -262,13 +281,13 @@ export class Session implements SessionFace {
149
+ }
150
+
151
+ /**
152
+ - * Resolve one image referenced by this session into browser-consumable bytes.
153
+ + * Resolve one file referenced by this session into browser-consumable bytes.
154
+ * @param attachmentId - opaque id found in the folded session log.
155
+ * @returns the authenticated reference and decoded bytes.
156
+ */
157
+ async readAttachment(
158
+ attachmentId: AttachmentIdType,
159
+ - ): Promise<RemoteResult<{ attachment: ImageAttachmentRef; data: Uint8Array }>> {
160
+ + ): Promise<RemoteResult<{ attachment: FileAttachmentRef; data: Uint8Array }>> {
161
+ const result = await this.remote.session.attachment({
162
+ sessionId: this.sessionId,
163
+ attachmentId,
140
164
  diff --git a/packages/api/session-controller/src/commands.ts b/packages/api/session-controller/src/commands.ts
141
- index 48c41f3aa143d420890a4ad244d5b7b75b7d3eb4..cae9940f2ae4d5b6b8f57b0d88d48629f8b33c0d 100644
165
+ index 1ad9127925..4384b275a8 100644
142
166
  --- a/packages/api/session-controller/src/commands.ts
143
167
  +++ b/packages/api/session-controller/src/commands.ts
144
- @@ -281,6 +281,34 @@ export class SessionCommandController {
168
+ @@ -5,7 +5,7 @@ import type { Context } from '@deepseek-ai/cordis'
169
+ import { brandString } from '@deepseek-ai/dsh-brand'
170
+ import type { Agent, ModelSelection as AgentModelSelection } from '@deepseek-ai/dsh-agent'
171
+ import { AttachmentError, admitEncodedImages } from '@deepseek-ai/dsh-attachment'
172
+ -import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
173
+ +import type { FileAttachmentRef, ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
174
+ import {
175
+ ReasoningEffortId, createUserMessage, freezeMessage,
176
+ } from '@deepseek-ai/dsh-llm'
177
+ @@ -281,6 +281,26 @@ export class SessionCommandController {
145
178
  * @returns acknowledgement that the Agent accepted the prompt.
146
179
  */
147
180
  async prompt(request: SessionPromptRequest): Promise<SessionPromptValue> {
148
181
  + const hasImage = request.content.some(part => part.type === 'image')
149
- + try {
150
- + return await this.promptPrepared(
151
- + request,
152
- + hasImage,
153
- + () => durablePromptContent(this.ctx, request.content),
154
- + )
155
- + } catch (error) {
156
- + if (error instanceof TypertRemoteFailure) throw error
157
- + if (error instanceof AttachmentError) {
158
- + reject('attachment-error', error.message, { reason: error.code })
159
- + }
160
- + reject('agent-busy', 'prompt rejected', { reason: String(error) })
161
- + }
182
+ + return this.promptPrepared(
183
+ + request,
184
+ + hasImage,
185
+ + () => durablePromptContent(this.ctx, request.content),
186
+ + )
162
187
  + }
163
188
  +
164
189
  + /**
@@ -176,52 +201,193 @@ index 48c41f3aa143d420890a4ad244d5b7b75b7d3eb4..cae9940f2ae4d5b6b8f57b0d88d48629
176
201
  const clientTimeZone = request.clientTimeZone === undefined
177
202
  ? undefined
178
203
  : canonicalClientTimeZone(request.clientTimeZone)
179
- @@ -305,31 +333,21 @@ export class SessionCommandController {
204
+ @@ -305,7 +325,6 @@ export class SessionCommandController {
180
205
  rpcId: request.requestId,
181
206
  ...(clientTimeZone === undefined ? {} : { clientTimeZone }),
182
207
  }
183
208
  - const hasImage = request.content.some(part => part.type === 'image')
184
209
  const admit = async (): Promise<SessionPromptValue> => {
185
- - try {
186
- - if (hasImage) {
187
- - const current = this.agents.selectionFor(agent).current
188
- - const model = await this.ctx.llm.resolveModelInfo(current.provider, current.model)
189
- - if (model.inputModalities !== undefined && !model.inputModalities.includes('image')) {
190
- - reject(
191
- - 'attachment-error',
192
- - `Model "${current.model}" does not support image input.`,
193
- - { reason: 'MODEL_DOES_NOT_SUPPORT_IMAGES' },
194
- - )
195
- - }
196
- - }
210
+ try {
211
+ if (hasImage) {
212
+ @@ -319,8 +338,7 @@ export class SessionCommandController {
213
+ )
214
+ }
215
+ }
197
216
  - const content = await durablePromptContent(this.ctx, request.content)
198
217
  - const message: UserMessage = createUserMessage({ content, source })
199
- - if (request.mode === 'steer') agent.steer(message)
200
- - else agent.followup(message)
201
- - } catch (error) {
202
- - if (error instanceof TypertRemoteFailure) throw error
203
- - if (error instanceof AttachmentError) {
204
- - reject('attachment-error', error.message, { reason: error.code })
205
- + if (hasImage) {
206
- + const current = this.agents.selectionFor(agent).current
207
- + const model = await this.ctx.llm.resolveModelInfo(current.provider, current.model)
208
- + if (model.inputModalities !== undefined && !model.inputModalities.includes('image')) {
209
- + reject(
210
- + 'attachment-error',
211
- + `Model "${current.model}" does not support image input.`,
212
- + { reason: 'MODEL_DOES_NOT_SUPPORT_IMAGES' },
213
- + )
214
- }
215
- - reject('agent-busy', 'prompt rejected', { reason: String(error) })
218
+ + const message: UserMessage = createUserMessage({ content: await prepare(), source })
219
+ if (request.mode === 'steer') agent.steer(message)
220
+ else agent.followup(message)
221
+ } catch (error) {
222
+ @@ -336,7 +354,7 @@ export class SessionCommandController {
223
+ }
224
+
225
+ /**
226
+ - * Read one durable image after proving the Session log references it.
227
+ + * Read one durable attachment after proving the Session log references it.
228
+ * @param request - Session and attachment identities used for authorization.
229
+ * @returns the durable attachment reference and base64-encoded bytes.
230
+ */
231
+ @@ -348,31 +366,35 @@ export class SessionCommandController {
232
+ if (error instanceof ApiSessionNotFound) {
233
+ throw new RemoteError('session/not-found', error.message, { sessionId: request.sessionId })
234
+ }
235
+ + console.error('session-controller: attachment authorization failed', error)
236
+ throw new RemoteError(
237
+ 'gateway/internal',
238
+ `attachment authorization unavailable for session "${request.sessionId}": ${String(error)}`,
239
+ {},
240
+ )
241
+ }
242
+ - const ref = referencedImage(source.events, String(request.attachmentId))
243
+ - if (ref === undefined) {
244
+ + const referenced = referencedAttachment(source.events, String(request.attachmentId))
245
+ + if (referenced === undefined) {
246
+ throw new RemoteError(
247
+ 'session/attachment-invalid',
248
+ - 'Image is not referenced by this session.',
249
+ + 'Attachment is not referenced by this session.',
250
+ { reason: 'ATTACHMENT_NOT_REFERENCED' },
251
+ )
252
+ }
253
+ try {
254
+ - const stored = await this.ctx.attachments.readImage(ref)
255
+ + const stored = referenced.kind === 'image'
256
+ + ? await this.ctx.attachments.readImage(referenced.ref)
257
+ + : await this.ctx.attachments.readFile(referenced.ref)
258
+ return {
259
+ attachment: stored.ref,
260
+ data: Buffer.from(stored.data).toString('base64'),
261
+ }
262
+ } catch (error) {
263
+ + console.error('session-controller: attachment read failed', error)
264
+ if (error instanceof AttachmentError) {
265
+ throw new RemoteError('session/attachment-invalid', error.message, { reason: error.code })
216
266
  }
217
- + const message: UserMessage = createUserMessage({ content: await prepare(), source })
218
- + if (request.mode === 'steer') agent.steer(message)
219
- + else agent.followup(message)
220
- return { accepted: true }
267
+ - throw new RemoteError('gateway/internal', 'Unable to read image attachment.', {})
268
+ + throw new RemoteError('gateway/internal', 'Unable to read attachment.', {})
221
269
  }
222
- return hasImage ? this.agents.serializeImageAdmission(agent, admit) : admit()
270
+ }
271
+
272
+ @@ -507,55 +529,98 @@ async function durablePromptContent(
273
+ : { type: 'image', attachment: refs[next++] as ImageAttachmentRef })
274
+ }
275
+
276
+ -function imageBlockIn(
277
+ +type ReferencedAttachment =
278
+ + | { readonly kind: 'image'; readonly ref: ImageAttachmentRef }
279
+ + | { readonly kind: 'file'; readonly ref: FileAttachmentRef }
280
+ +
281
+ +function matchingAttachment(
282
+ + value: unknown,
283
+ + kind: ReferencedAttachment['kind'],
284
+ + attachmentId: string,
285
+ +): ReferencedAttachment | undefined {
286
+ + if (typeof value !== 'object' || value === null || Array.isArray(value)) return undefined
287
+ + if (String((value as { readonly attachmentId?: unknown }).attachmentId) !== attachmentId) return undefined
288
+ + return kind === 'image'
289
+ + ? { kind, ref: value as ImageAttachmentRef }
290
+ + : { kind, ref: value as FileAttachmentRef }
291
+ +}
292
+ +
293
+ +function attachmentBlockIn(
294
+ content: unknown,
295
+ - match: (ref: ImageAttachmentRef) => boolean,
296
+ -): ImageAttachmentRef | undefined {
297
+ + attachmentId: string,
298
+ +): ReferencedAttachment | undefined {
299
+ if (!Array.isArray(content)) return undefined
300
+ for (const value of content) {
301
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) continue
302
+ - const block = value as { readonly type?: unknown; readonly attachment?: unknown; readonly content?: unknown }
303
+ - if (block.type === 'image' && typeof block.attachment === 'object' && block.attachment !== null) {
304
+ - const ref = block.attachment as ImageAttachmentRef
305
+ - if (match(ref)) return ref
306
+ + const block = value as {
307
+ + readonly type?: unknown
308
+ + readonly attachment?: unknown
309
+ + readonly parsed?: unknown
310
+ + readonly content?: unknown
311
+ + }
312
+ + if (block.type === 'image') {
313
+ + const found = matchingAttachment(block.attachment, 'image', attachmentId)
314
+ + if (found !== undefined) return found
315
+ + }
316
+ + if (block.type === 'document') {
317
+ + const original = matchingAttachment(block.attachment, 'file', attachmentId)
318
+ + if (original !== undefined) return original
319
+ + if (typeof block.parsed === 'object' && block.parsed !== null && !Array.isArray(block.parsed)) {
320
+ + const parsed = block.parsed as {
321
+ + readonly markdown?: unknown
322
+ + readonly modelText?: unknown
323
+ + readonly contentList?: unknown
324
+ + readonly images?: unknown
325
+ + }
326
+ + for (const artifact of [parsed.markdown, parsed.modelText, parsed.contentList]) {
327
+ + const found = matchingAttachment(artifact, 'file', attachmentId)
328
+ + if (found !== undefined) return found
329
+ + }
330
+ + if (Array.isArray(parsed.images)) {
331
+ + for (const image of parsed.images) {
332
+ + const found = matchingAttachment(image, 'image', attachmentId)
333
+ + if (found !== undefined) return found
334
+ + }
335
+ + }
336
+ + }
337
+ }
338
+ if (block.type === 'tool-result') {
339
+ - const nested = imageBlockIn(block.content, match)
340
+ + const nested = attachmentBlockIn(block.content, attachmentId)
341
+ if (nested !== undefined) return nested
342
+ }
343
+ }
344
+ return undefined
345
+ }
346
+
347
+ -function imageInEvent(
348
+ +function attachmentInEvent(
349
+ event: SessionEvent,
350
+ - match: (ref: ImageAttachmentRef) => boolean,
351
+ -): ImageAttachmentRef | undefined {
352
+ + attachmentId: string,
353
+ +): ReferencedAttachment | undefined {
354
+ const data = event.data as {
355
+ readonly content?: unknown
356
+ readonly message?: { readonly content?: unknown }
357
+ readonly inserted?: readonly { readonly content?: unknown }[]
358
+ readonly chunk?: { readonly type?: unknown; readonly block?: unknown }
359
+ }
360
+ - const direct = imageBlockIn(data.content, match)
361
+ + const direct = attachmentBlockIn(data.content, attachmentId)
362
+ if (direct !== undefined) return direct
363
+ - const message = imageBlockIn(data.message?.content, match)
364
+ + const message = attachmentBlockIn(data.message?.content, attachmentId)
365
+ if (message !== undefined) return message
366
+ for (const inserted of data.inserted ?? []) {
367
+ - const found = imageBlockIn(inserted.content, match)
368
+ + const found = attachmentBlockIn(inserted.content, attachmentId)
369
+ if (found !== undefined) return found
370
+ }
371
+ return event.type === 'assistant/chunk' && data.chunk?.type === 'block-end'
372
+ - ? imageBlockIn([data.chunk.block], match)
373
+ + ? attachmentBlockIn([data.chunk.block], attachmentId)
374
+ : undefined
375
+ }
376
+
377
+ -function referencedImage(
378
+ +function referencedAttachment(
379
+ events: readonly SessionEvent[],
380
+ attachmentId: string,
381
+ -): ImageAttachmentRef | undefined {
382
+ +): ReferencedAttachment | undefined {
383
+ for (const event of events) {
384
+ - const found = imageInEvent(event, ref => String(ref.attachmentId) === attachmentId)
385
+ + const found = attachmentInEvent(event, attachmentId)
386
+ if (found !== undefined) return found
387
+ }
388
+ return undefined
223
389
  diff --git a/packages/api/session-controller/src/index.ts b/packages/api/session-controller/src/index.ts
224
- index 342dd977ebcb7b12e6c8e4c7f9cb93c72e23261c..f2a42d4e33a450a5874aed5a43bf9184677882a4 100644
390
+ index 572f895754..b828038982 100644
225
391
  --- a/packages/api/session-controller/src/index.ts
226
392
  +++ b/packages/api/session-controller/src/index.ts
227
393
  @@ -3,6 +3,7 @@
@@ -232,7 +398,7 @@ index 342dd977ebcb7b12e6c8e4c7f9cb93c72e23261c..f2a42d4e33a450a5874aed5a43bf9184
232
398
  import { canOpenNativePath, openNativePath } from '@deepseek-ai/dsh-native-command'
233
399
  import type { SessionEvent, SessionHeader, SessionId } from '@deepseek-ai/dsh-session'
234
400
  import type { SessionObservation } from '@deepseek-ai/dsh-session-query'
235
- @@ -328,6 +329,21 @@ export class SessionController extends TypertRemoteService {
401
+ @@ -324,6 +325,21 @@ export class SessionController extends TypertRemoteService {
236
402
  return this.commands.prompt(request)
237
403
  }
238
404
 
@@ -255,7 +421,7 @@ index 342dd977ebcb7b12e6c8e4c7f9cb93c72e23261c..f2a42d4e33a450a5874aed5a43bf9184
255
421
  * Read one image proven reachable from the addressed Session log.
256
422
  * @param request - Session and attachment identities used for authorization.
257
423
  diff --git a/packages/api/session-controller/src/list.ts b/packages/api/session-controller/src/list.ts
258
- index c9a53a6bd0209669b3e7994d0cb1aa29dfeabf2f..f15912a035586dfa324bf220378c6407e6ebcf86 100644
424
+ index ab72da1c92..be3fbe1e31 100644
259
425
  --- a/packages/api/session-controller/src/list.ts
260
426
  +++ b/packages/api/session-controller/src/list.ts
261
427
  @@ -3,7 +3,7 @@
@@ -281,18 +447,18 @@ index c9a53a6bd0209669b3e7994d0cb1aa29dfeabf2f..f15912a035586dfa324bf220378c6407
281
447
  /**
282
448
  * Advance the Session-list metadata projection by one committed event.
283
449
  * @param state - metadata before the event.
284
- @@ -109,6 +116,17 @@ export class ApiSessionList {
450
+ @@ -107,6 +114,17 @@ export class ApiSessionList {
285
451
  },
286
452
  stateVersion: 1,
287
453
  })
288
- + projectionCtx.sessionProjections.register<'documentLimits', null>({
454
+ + ctx.sessionProjections.register<'documentLimits', null>({
289
455
  + key: 'documentLimits',
290
456
  + stateSchema: z.null(),
291
457
  + init: () => null,
292
458
  + apply: state => state,
293
459
  + wire: {
294
460
  + viewSchema: documentLimitsSchema,
295
- + view: () => projectionCtx.attachments.documentLimits,
461
+ + view: () => attachmentCtx.attachments.documentLimits,
296
462
  + },
297
463
  + stateVersion: 1,
298
464
  + })
@@ -300,7 +466,7 @@ index c9a53a6bd0209669b3e7994d0cb1aa29dfeabf2f..f15912a035586dfa324bf220378c6407
300
466
  }
301
467
 
302
468
  diff --git a/packages/api/session-controller/src/types.ts b/packages/api/session-controller/src/types.ts
303
- index 167937e3d3317bdc20878662f337f88b85ea47f8..97fc8315afe139fe7c009e5295ee807ec2b542a5 100644
469
+ index c0b25fcf2c..f080be8601 100644
304
470
  --- a/packages/api/session-controller/src/types.ts
305
471
  +++ b/packages/api/session-controller/src/types.ts
306
472
  @@ -1,7 +1,7 @@
@@ -308,11 +474,11 @@ index 167937e3d3317bdc20878662f337f88b85ea47f8..97fc8315afe139fe7c009e5295ee807e
308
474
 
309
475
  import type {
310
476
  - AttachmentIdType, ImageAttachmentLimits, ImageAttachmentRef, ImageMediaType,
311
- + AttachmentIdType, DocumentAttachmentLimits, ImageAttachmentLimits, ImageAttachmentRef, ImageMediaType,
477
+ + AttachmentIdType, DocumentAttachmentLimits, FileAttachmentRef, ImageAttachmentLimits, ImageMediaType,
312
478
  } from '@deepseek-ai/dsh-attachment'
313
479
  import type { Branded } from '@deepseek-ai/dsh-brand'
314
480
  import type { MessageId } from '@deepseek-ai/dsh-llm/brand'
315
- @@ -18,6 +18,8 @@ declare module '@deepseek-ai/dsh-session-projection/types' {
481
+ @@ -19,6 +19,8 @@ declare module '@deepseek-ai/dsh-session-projection/types' {
316
482
  sessionListMetadata: SessionListMetadata
317
483
  /** Host state for the boot-constant image-limit view. */
318
484
  imageLimits: null
@@ -321,7 +487,7 @@ index 167937e3d3317bdc20878662f337f88b85ea47f8..97fc8315afe139fe7c009e5295ee807e
321
487
  /** Durable model selection already used by a request and still pending for a later request. */
322
488
  modelSelection: ModelSelectionProjectionState
323
489
  }
324
- @@ -26,6 +28,8 @@ declare module '@deepseek-ai/dsh-session-projection/types' {
490
+ @@ -27,6 +29,8 @@ declare module '@deepseek-ai/dsh-session-projection/types' {
325
491
  sessionListMetadata: SessionListMetadata
326
492
  /** Image-intake limits enforced by the Session prompt endpoint. */
327
493
  imageLimits: ImageAttachmentLimits
@@ -330,8 +496,27 @@ index 167937e3d3317bdc20878662f337f88b85ea47f8..97fc8315afe139fe7c009e5295ee807e
330
496
  /** Durable model selection already used and selected for the next request. */
331
497
  modelSelection: ModelSelectionProjection
332
498
  }
499
+ @@ -313,15 +317,15 @@ export interface SessionPromptValue {
500
+ readonly accepted: true
501
+ }
502
+
503
+ -/** Durable image read request. */
504
+ +/** Durable attachment read request. */
505
+ export interface SessionAttachmentRequest {
506
+ readonly sessionId: SessionId
507
+ readonly attachmentId: AttachmentIdType
508
+ }
509
+
510
+ -/** Durable image read response value. */
511
+ +/** Durable attachment read response value. */
512
+ export interface SessionAttachmentValue {
513
+ - readonly attachment: ImageAttachmentRef
514
+ + readonly attachment: FileAttachmentRef
515
+ readonly data: string
516
+ }
517
+
333
518
  diff --git a/packages/attachment/attachment-local/src/index.ts b/packages/attachment/attachment-local/src/index.ts
334
- index 16b963f2259d0fc7e7a0761afb9a73eb1603e7ad..c10fe04f5dd1cf9812c37d814a9ce7c41dc77dba 100644
519
+ index 16b963f225..c10fe04f5d 100644
335
520
  --- a/packages/attachment/attachment-local/src/index.ts
336
521
  +++ b/packages/attachment/attachment-local/src/index.ts
337
522
  @@ -5,22 +5,26 @@ import { Context } from '@deepseek-ai/cordis'
@@ -445,7 +630,7 @@ index 16b963f2259d0fc7e7a0761afb9a73eb1603e7ad..c10fe04f5dd1cf9812c37d814a9ce7c4
445
630
  await this.compression.run(() => validateImageFile(input, this.imageLimits, this.normalizationPolicy))
446
631
  }
447
632
  diff --git a/packages/attachment/attachment-local/src/store.ts b/packages/attachment/attachment-local/src/store.ts
448
- index e5a979aec14e91ae726b5aef414b307dc0780795..26f8e40e2a97809660f0df1dc7d03f812d2734bc 100644
633
+ index e5a979aec1..14de66088e 100644
449
634
  --- a/packages/attachment/attachment-local/src/store.ts
450
635
  +++ b/packages/attachment/attachment-local/src/store.ts
451
636
  @@ -9,9 +9,12 @@ import {
@@ -489,13 +674,14 @@ index e5a979aec14e91ae726b5aef414b307dc0780795..26f8e40e2a97809660f0df1dc7d03f81
489
674
  }
490
675
 
491
676
  async function inspectMetadata(
492
- @@ -183,73 +189,120 @@ async function ensureDurableHome(path: string): Promise<string> {
677
+ @@ -183,73 +189,121 @@ async function ensureDurableHome(path: string): Promise<string> {
493
678
  }
494
679
 
495
680
  /**
496
681
  - * Publish one already verified normalized image below a versioned attachment root.
497
682
  + * Publish one immutable object after its content digest is known.
498
- + * 中文约束:返回前必须完成file与directory durability;Session只能记录已经返回的reference。
683
+ + * The operation returns only after file and directory durability; Sessions
684
+ + * may record only references returned by this function.
499
685
  * @param root - absolute `DSH_HOME/attachments/v1` root.
500
686
  - * @param prepared - deterministic normalized bytes and reference.
501
687
  - * @returns durable content-addressed normalized image reference.
@@ -650,7 +836,7 @@ index e5a979aec14e91ae726b5aef414b307dc0780795..26f8e40e2a97809660f0df1dc7d03f81
650
836
  }
651
837
 
652
838
  diff --git a/packages/attachment/attachment/src/error.ts b/packages/attachment/attachment/src/error.ts
653
- index c19229872b976dc7e20b32220cd07bcc7d380395..67f9852511845c38e0a8359d85da24b9abeda7d4 100644
839
+ index c19229872b..67f9852511 100644
654
840
  --- a/packages/attachment/attachment/src/error.ts
655
841
  +++ b/packages/attachment/attachment/src/error.ts
656
842
  @@ -24,6 +24,7 @@ export type AttachmentErrorCode =
@@ -662,7 +848,7 @@ index c19229872b976dc7e20b32220cd07bcc7d380395..67f9852511845c38e0a8359d85da24b9
662
848
  /** Runtime membership for structurally compatible errors crossing package boundaries. */
663
849
  const IMAGE_ADMISSION_ERROR_CODE_SET: ReadonlySet<string> = new Set(IMAGE_ADMISSION_ERROR_CODES)
664
850
  diff --git a/packages/attachment/attachment/src/index.ts b/packages/attachment/attachment/src/index.ts
665
- index 4ee001b86c55aaa1da788543477ca670a24f8a5d..9f96b4bf5cdac35afb804e378013cb63eec154ec 100644
851
+ index 4ee001b86c..9f96b4bf5c 100644
666
852
  --- a/packages/attachment/attachment/src/index.ts
667
853
  +++ b/packages/attachment/attachment/src/index.ts
668
854
  @@ -3,11 +3,15 @@
@@ -746,7 +932,7 @@ index 4ee001b86c55aaa1da788543477ca670a24f8a5d..9f96b4bf5cdac35afb804e378013cb63
746
932
  * Validate one image without persisting it.
747
933
  * Batch callers validate every member before saving any member.
748
934
  diff --git a/packages/attachment/attachment/src/types.ts b/packages/attachment/attachment/src/types.ts
749
- index 046444cd76551380a8d1d924f27db9a002d8a93f..146fe3d501b038c8affc61a59c16d727354b9bb8 100644
935
+ index 046444cd76..146fe3d501 100644
750
936
  --- a/packages/attachment/attachment/src/types.ts
751
937
  +++ b/packages/attachment/attachment/src/types.ts
752
938
  @@ -7,6 +7,47 @@ export type { AttachmentId } from './brand.ts'
@@ -840,23 +1026,20 @@ index 046444cd76551380a8d1d924f27db9a002d8a93f..146fe3d501b038c8affc61a59c16d727
840
1026
  /** Stored image bytes returned after reference and digest verification. */
841
1027
  export interface StoredImageAttachment {
842
1028
  ref: ImageAttachmentRef
843
- diff --git a/packages/client/ui-attachment/src/client/ComposerAttachments.module.css b/packages/client/ui-attachment/src/client/ComposerAttachments.module.css
844
- index 770a64cef5dba2baded4adb7afe63ae88c7fcffc..1cdfd8c3f5b1138b76ce294db6e0198c2226353d 100644
845
- --- a/packages/client/ui-attachment/src/client/ComposerAttachments.module.css
846
- +++ b/packages/client/ui-attachment/src/client/ComposerAttachments.module.css
847
- @@ -1,3 +1,34 @@
848
- +.picker {
849
- + display: flex;
850
- + padding: 4px 12px 0;
851
- +}
852
- +
1029
+ diff --git a/packages/client/ui-attachment/src/client/AttachmentPicker.module.css b/packages/client/ui-attachment/src/client/AttachmentPicker.module.css
1030
+ new file mode 100644
1031
+ index 0000000000..da3ae830f9
1032
+ --- /dev/null
1033
+ +++ b/packages/client/ui-attachment/src/client/AttachmentPicker.module.css
1034
+ @@ -0,0 +1,26 @@
853
1035
  +.fileInput {
854
1036
  + display: none;
855
1037
  +}
856
1038
  +
857
- +.add {
1039
+ +.trigger {
858
1040
  + display: grid;
859
1041
  + place-items: center;
1042
+ + flex: none;
860
1043
  + width: 28px;
861
1044
  + height: 28px;
862
1045
  + padding: 0;
@@ -867,34 +1050,79 @@ index 770a64cef5dba2baded4adb7afe63ae88c7fcffc..1cdfd8c3f5b1138b76ce294db6e0198c
867
1050
  + cursor: pointer;
868
1051
  +}
869
1052
  +
870
- +.add:hover:not(:disabled) {
1053
+ +.trigger:hover:not(:disabled) {
871
1054
  + background: var(--dsw-alias-interactive-bg-hover-solid);
872
1055
  +}
873
1056
  +
874
- +.add:disabled {
1057
+ +.trigger:disabled {
875
1058
  + opacity: 0.5;
876
1059
  + cursor: default;
877
1060
  +}
1061
+ diff --git a/packages/client/ui-attachment/src/client/AttachmentPicker.tsx b/packages/client/ui-attachment/src/client/AttachmentPicker.tsx
1062
+ new file mode 100644
1063
+ index 0000000000..8edb8c5da5
1064
+ --- /dev/null
1065
+ +++ b/packages/client/ui-attachment/src/client/AttachmentPicker.tsx
1066
+ @@ -0,0 +1,46 @@
1067
+ +import { useCallback, useRef } from 'react'
1068
+ +import type { ChangeEvent } from 'react'
1069
+ +import { IconPaperclipOutline16, Tooltip } from '@deepseek-ai/dsh-client-ui-primitives'
1070
+ +import type { ComposerAttachmentPickerProps } from '@deepseek-ai/dsh-client-ui-conversation/client'
1071
+ +import css from './AttachmentPicker.module.css'
1072
+ +
1073
+ +/**
1074
+ + * Render one image-and-document chooser directly after the command button.
1075
+ + * @param props - current intake availability, file callback, and localized labels.
1076
+ + * @returns the hidden file input and its visible attachment action.
1077
+ + */
1078
+ +export function AttachmentPicker({ canAcceptFiles, onAddFiles, t }: ComposerAttachmentPickerProps) {
1079
+ + const fileInput = useRef<HTMLInputElement>(null)
1080
+ + const chooseFiles = useCallback(() => { fileInput.current?.click() }, [])
1081
+ + const onFilesChosen = useCallback((event: ChangeEvent<HTMLInputElement>) => {
1082
+ + const files = event.currentTarget.files
1083
+ + if (files !== null && files.length > 0) onAddFiles([...files])
1084
+ + event.currentTarget.value = ''
1085
+ + }, [onAddFiles])
878
1086
  +
879
- .rail {
880
- min-width: 0;
881
- padding: 4px 12px 0;
1087
+ + return (
1088
+ + <>
1089
+ + <input
1090
+ + ref={fileInput}
1091
+ + className={css.fileInput}
1092
+ + type="file"
1093
+ + multiple
1094
+ + accept="image/png,image/jpeg,image/webp,image/gif,.pdf,.docx,.pptx,.xlsx"
1095
+ + disabled={!canAcceptFiles}
1096
+ + onChange={onFilesChosen}
1097
+ + />
1098
+ + <Tooltip label={t('attachment.add')} side="top" delayMs={500}>
1099
+ + <button
1100
+ + type="button"
1101
+ + className={css.trigger}
1102
+ + aria-label={t('attachment.add')}
1103
+ + disabled={!canAcceptFiles}
1104
+ + onMouseDown={(event) => { event.preventDefault() }}
1105
+ + onClick={chooseFiles}
1106
+ + >
1107
+ + <IconPaperclipOutline16 />
1108
+ + </button>
1109
+ + </Tooltip>
1110
+ + </>
1111
+ + )
1112
+ +}
882
1113
  diff --git a/packages/client/ui-attachment/src/client/ComposerAttachments.tsx b/packages/client/ui-attachment/src/client/ComposerAttachments.tsx
883
- index 1e45dc6230ac024154822ddca86509bf2850d791..553c6bc24acfb65d11358ebf9fa9f0824469e376 100644
1114
+ index 1e45dc6230..79db6a90b3 100644
884
1115
  --- a/packages/client/ui-attachment/src/client/ComposerAttachments.tsx
885
1116
  +++ b/packages/client/ui-attachment/src/client/ComposerAttachments.tsx
886
- @@ -1,7 +1,10 @@
887
- import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
888
- +import type { ChangeEvent } from 'react'
1117
+ @@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
889
1118
  import type {
890
1119
  ComposerAttachment, ComposerAttachmentsProps,
891
1120
  } from '@deepseek-ai/dsh-client-ui-conversation/client'
892
1121
  +import type { PropsRenderSlots } from '@deepseek-ai/dsh-client-ui-slots'
893
- +import { IconPaperclipOutline16, Tooltip } from '@deepseek-ai/dsh-client-ui-primitives'
894
1122
  import { AttachmentRail } from '../AttachmentRail.tsx'
895
1123
  import type { AttachmentRailItem } from '../AttachmentRail.tsx'
896
1124
  import { DropOverlay } from '../DropOverlay.tsx'
897
- @@ -15,12 +18,16 @@ interface ComposerRailItem extends AttachmentRailItem {
1125
+ @@ -15,9 +16,12 @@ interface ComposerRailItem extends AttachmentRailItem {
898
1126
  }
899
1127
 
900
1128
  /** Draft-image rail, document drop target, and original-image preview slot entry. */
@@ -909,11 +1137,7 @@ index 1e45dc6230ac024154822ddca86509bf2850d791..553c6bc24acfb65d11358ebf9fa9f082
909
1137
  const [preview, setPreview] = useState<ComposerAttachment | null>(null)
910
1138
  const [dragActive, setDragActive] = useState(false)
911
1139
  const dragDepth = useRef(0)
912
- + const fileInput = useRef<HTMLInputElement>(null)
913
- const closePreview = useCallback(() => { setPreview(null) }, [])
914
-
915
- useEffect(() => {
916
- @@ -62,7 +69,7 @@ export function ComposerAttachments({
1140
+ @@ -62,7 +66,7 @@ export function ComposerAttachments({
917
1141
  if (dataTransfer === null) return
918
1142
  event.preventDefault()
919
1143
  reset()
@@ -922,23 +1146,16 @@ index 1e45dc6230ac024154822ddca86509bf2850d791..553c6bc24acfb65d11358ebf9fa9f082
922
1146
  }
923
1147
  document.addEventListener('dragenter', onDragEnter)
924
1148
  document.addEventListener('dragover', onDragOver)
925
- @@ -76,7 +83,14 @@ export function ComposerAttachments({
1149
+ @@ -76,7 +80,7 @@ export function ComposerAttachments({
926
1150
  document.removeEventListener('drop', onDrop)
927
1151
  window.removeEventListener('dragend', reset)
928
1152
  }
929
1153
  - }, [canAcceptDrop, onAddImages])
930
1154
  + }, [canAcceptDrop, onAddFiles])
931
- +
932
- + const chooseFiles = useCallback(() => { fileInput.current?.click() }, [])
933
- + const onFilesChosen = useCallback((event: ChangeEvent<HTMLInputElement>) => {
934
- + const files = event.currentTarget.files
935
- + if (files !== null && files.length > 0) onAddFiles([...files])
936
- + event.currentTarget.value = ''
937
- + }, [onAddFiles])
938
1155
 
939
1156
  const railItems = useMemo<ComposerRailItem[]>(() => attachments.map(attachment => ({
940
1157
  id: attachment.id,
941
- @@ -91,9 +105,28 @@ export function ComposerAttachments({
1158
+ @@ -91,9 +95,12 @@ export function ComposerAttachments({
942
1159
  {dragActive && (
943
1160
  <DropOverlay
944
1161
  disabled={!canAcceptDrop}
@@ -948,28 +1165,12 @@ index 1e45dc6230ac024154822ddca86509bf2850d791..553c6bc24acfb65d11358ebf9fa9f082
948
1165
  + : dropOverlayLabels(t, canAcceptDrop, dropLimits)}
949
1166
  />
950
1167
  )}
951
- + <div className={css.picker}>
952
- + <input
953
- + ref={fileInput}
954
- + className={css.fileInput}
955
- + type="file"
956
- + multiple
957
- + accept="image/png,image/jpeg,image/webp,image/gif,.pdf,.docx,.pptx,.xlsx"
958
- + disabled={!canAcceptDrop}
959
- + onChange={onFilesChosen}
960
- + />
961
- + <Tooltip label={t('attachment.add')} side="top">
962
- + <button type="button" className={css.add} aria-label={t('attachment.add')} disabled={!canAcceptDrop} onClick={chooseFiles}>
963
- + <IconPaperclipOutline16 />
964
- + </button>
965
- + </Tooltip>
966
- + </div>
967
1168
  + {renderSlot('conversation.input.attachments.documents', { documents, onRemoveDocument })}
968
1169
  {railItems.length > 0 && (
969
1170
  <div className={css.rail}>
970
1171
  <AttachmentRail
971
1172
  diff --git a/packages/client/ui-attachment/src/client/MessageImages.tsx b/packages/client/ui-attachment/src/client/MessageImages.tsx
972
- index c84914db543de23165732809bbe9c723b34dfa90..70b28eb66089203432df9efed6368923c4409754 100644
1173
+ index c84914db54..46426f4519 100644
973
1174
  --- a/packages/client/ui-attachment/src/client/MessageImages.tsx
974
1175
  +++ b/packages/client/ui-attachment/src/client/MessageImages.tsx
975
1176
  @@ -1,8 +1,30 @@
@@ -994,7 +1195,7 @@ index c84914db543de23165732809bbe9c723b34dfa90..70b28eb66089203432df9efed6368923
994
1195
  +}
995
1196
  +
996
1197
  +/** Chat attachment slot entry. */
997
- +export function MessageImages({ images, documents = [], loadImage, align, t, renderSlot }: MessageImagesProps) {
1198
+ +export function MessageImages({ images, documents, loadImage, align, t, renderSlot }: MessageImagesProps) {
998
1199
  + return <AttachmentGallery images={images} loadImage={loadImage} align={align} t={t} documentCards={renderSlot('conversation.message.images.documents', { documents })} />
999
1200
  +}
1000
1201
  +
@@ -1003,23 +1204,31 @@ index c84914db543de23165732809bbe9c723b34dfa90..70b28eb66089203432df9efed6368923
1003
1204
  + & PropsLocale<'conversation'>
1004
1205
  +
1005
1206
  +/** Trajectory attachment slot entry. */
1006
- +export function TrajectoryImages({ images, documents = [], loadImage, align, t, renderSlot }: TrajectoryProps) {
1207
+ +export function TrajectoryImages({ images, documents, loadImage, align, t, renderSlot }: TrajectoryProps) {
1007
1208
  + return <AttachmentGallery images={images} loadImage={loadImage} align={align} t={t} documentCards={renderSlot('conversation.trajectory.images.documents', { documents })} />
1008
1209
  }
1009
1210
  diff --git a/packages/client/ui-attachment/src/client/index.ts b/packages/client/ui-attachment/src/client/index.ts
1010
- index 8fe94f64ee171798431716e3765e446ca95c935f..a045a3433ef7984df767ce00000ef52deaa9a5d6 100644
1211
+ index 8fe94f64ee..0654b882f4 100644
1011
1212
  --- a/packages/client/ui-attachment/src/client/index.ts
1012
1213
  +++ b/packages/client/ui-attachment/src/client/index.ts
1013
- @@ -5,7 +5,7 @@ import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
1214
+ @@ -4,24 +4,38 @@ import type {} from '@deepseek-ai/dsh-client-ui-chat/client'
1215
+ import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
1014
1216
  import type {} from '@deepseek-ai/dsh-client-ui-renderer/client'
1015
1217
  import type {} from '@deepseek-ai/dsh-client-ui-trajectory/client'
1218
+ +import { AttachmentPicker } from './AttachmentPicker.tsx'
1016
1219
  import { ComposerAttachments } from './ComposerAttachments.tsx'
1017
1220
  -import { MessageImages } from './MessageImages.tsx'
1018
1221
  +import { MessageImages, TrajectoryImages } from './MessageImages.tsx'
1019
1222
 
1020
1223
  /** Slot registry required by this presentation plugin. */
1021
1224
  export const inject = ['slots']
1022
- @@ -15,13 +15,22 @@ export function apply(ctx: ClientContext): void {
1225
+
1226
+ /** Register attachment presentation without exporting React components as package values. */
1227
+ export function apply(ctx: ClientContext): void {
1228
+ + ctx.slots.inject('conversation.input.attachment-picker', () => ctx.slots.register({
1229
+ + name: 'conversation.input.attachment-picker',
1230
+ + locale: 'conversation',
1231
+ + }, AttachmentPicker))
1023
1232
  ctx.slots.inject('conversation.input.attachments', () => ctx.slots.register({
1024
1233
  name: 'conversation.input.attachments',
1025
1234
  locale: 'conversation',
@@ -1044,10 +1253,10 @@ index 8fe94f64ee171798431716e3765e446ca95c935f..a045a3433ef7984df767ce00000ef52d
1044
1253
  + }, TrajectoryImages))
1045
1254
  }
1046
1255
  diff --git a/packages/client/ui-attachment/tests/composer-attachments.client.spec.tsx b/packages/client/ui-attachment/tests/composer-attachments.client.spec.tsx
1047
- index ef77068f7e017845bd400c96bb63340aa0b3cc16..b5f9c55df4a2d75f99df8ab17755b3b20f345a2a 100644
1256
+ index ef77068f7e..ba4fc855d5 100644
1048
1257
  --- a/packages/client/ui-attachment/tests/composer-attachments.client.spec.tsx
1049
1258
  +++ b/packages/client/ui-attachment/tests/composer-attachments.client.spec.tsx
1050
- @@ -53,15 +53,16 @@ function attachment(id: string, name = `${id}.png`): ComposerAttachment {
1259
+ @@ -53,22 +53,26 @@ function attachment(id: string, name = `${id}.png`): ComposerAttachment {
1051
1260
  }
1052
1261
  }
1053
1262
 
@@ -1055,9 +1264,12 @@ index ef77068f7e017845bd400c96bb63340aa0b3cc16..b5f9c55df4a2d75f99df8ab17755b3b2
1055
1264
  +function props(overrides: Partial<ComposerAttachmentsOwnerProps> = {}): Parameters<typeof ComposerAttachments>[0] {
1056
1265
  return {
1057
1266
  attachments: [],
1267
+ + documents: [],
1058
1268
  canAcceptDrop: true,
1059
1269
  onAddImages: () => {},
1270
+ + onAddFiles: () => {},
1060
1271
  onRemoveImage: () => {},
1272
+ + onRemoveDocument: () => {},
1061
1273
  t,
1062
1274
  + renderSlot: () => null,
1063
1275
  ...overrides,
@@ -1066,8 +1278,26 @@ index ef77068f7e017845bd400c96bb63340aa0b3cc16..b5f9c55df4a2d75f99df8ab17755b3b2
1066
1278
  }
1067
1279
 
1068
1280
  describe('ComposerAttachments', () => {
1281
+ it('accepts file drops anywhere on the document and keeps non-file drags native', () => {
1282
+ - const onAddImages = vi.fn()
1283
+ + const onAddFiles = vi.fn()
1284
+ const view = render(<ComposerAttachments {...props({
1285
+ - onAddImages,
1286
+ + onAddFiles,
1287
+ dropLimits: { count: 20, size: '5MB' },
1288
+ })} />)
1289
+
1290
+ @@ -87,7 +91,7 @@ describe('ComposerAttachments', () => {
1291
+ expect(fireEvent.dragOver(document.body, { dataTransfer })).toBe(false)
1292
+ expect(dataTransfer.dropEffect).toBe('copy')
1293
+ expect(fireEvent.drop(document.body, { dataTransfer })).toBe(false)
1294
+ - expect(onAddImages).toHaveBeenCalledWith([image])
1295
+ + expect(onAddFiles).toHaveBeenCalledWith([image])
1296
+ expect(view.queryByRole('status')).toBeNull()
1297
+ })
1298
+
1069
1299
  diff --git a/packages/client/ui-attachment/tests/message-image.client.spec.tsx b/packages/client/ui-attachment/tests/message-image.client.spec.tsx
1070
- index 76a720b983be3c54d4af91435f5b82ecf746685c..62475e6924e1373611d7035b4319329d458d07e7 100644
1300
+ index 76a720b983..0ed7b07d03 100644
1071
1301
  --- a/packages/client/ui-attachment/tests/message-image.client.spec.tsx
1072
1302
  +++ b/packages/client/ui-attachment/tests/message-image.client.spec.tsx
1073
1303
  @@ -261,6 +261,7 @@ describe('ImageGallery', () => {
@@ -1078,7 +1308,11 @@ index 76a720b983be3c54d4af91435f5b82ecf746685c..62475e6924e1373611d7035b4319329d
1078
1308
  useSession,
1079
1309
  useSessions,
1080
1310
  useSessionPendingInteraction,
1081
- @@ -281,6 +282,7 @@ describe('ImageGallery', () => {
1311
+ @@ -278,9 +279,11 @@ describe('ImageGallery', () => {
1312
+ submit: vi.fn(),
1313
+ },
1314
+ images: [{ attachment }],
1315
+ + documents: [],
1082
1316
  loadImage,
1083
1317
  align: 'end',
1084
1318
  t,
@@ -1086,10 +1320,31 @@ index 76a720b983be3c54d4af91435f5b82ecf746685c..62475e6924e1373611d7035b4319329d
1086
1320
  }
1087
1321
  const view = render(<MessageImages {...props} />)
1088
1322
  await waitFor(() => { expect(view.getByAltText('history.png')).toBeTruthy() })
1323
+ diff --git a/packages/client/ui-chat/src/client/chat/AssistantMarkdown.tsx b/packages/client/ui-chat/src/client/chat/AssistantMarkdown.tsx
1324
+ index c517f9d0c2..f24f94ea1d 100644
1325
+ --- a/packages/client/ui-chat/src/client/chat/AssistantMarkdown.tsx
1326
+ +++ b/packages/client/ui-chat/src/client/chat/AssistantMarkdown.tsx
1327
+ @@ -87,6 +87,7 @@ export const AssistantMarkdown = memo(function AssistantMarkdown({
1328
+ <Fragment key={start}>
1329
+ {renderMessageImages({
1330
+ images: group.map(({ attachment }) => ({ attachment })),
1331
+ + documents: [],
1332
+ align: 'start',
1333
+ })}
1334
+ </Fragment>,
1089
1335
  diff --git a/packages/client/ui-chat/src/client/chat/MessageItem.tsx b/packages/client/ui-chat/src/client/chat/MessageItem.tsx
1090
- index 467e486ecb3b28ddf7aa99281ad231032a98a76c..d89c77009fb52bdf7992f8f35c14553bb5e798ed 100644
1336
+ index cb8084fe41..5dd3aa8512 100644
1091
1337
  --- a/packages/client/ui-chat/src/client/chat/MessageItem.tsx
1092
1338
  +++ b/packages/client/ui-chat/src/client/chat/MessageItem.tsx
1339
+ @@ -1,7 +1,7 @@
1340
+ import { memo, useEffect, useMemo, useState } from 'react'
1341
+ import type { ReactNode } from 'react'
1342
+ import type { PendingSubmission } from '@deepseek-ai/dsh-api-session-controller/client'
1343
+ -import type { MessageImageSource } from '@deepseek-ai/dsh-client-ui-conversation/client'
1344
+ +import type { MessageDocumentSource, MessageImageSource } from '@deepseek-ai/dsh-client-ui-conversation/client'
1345
+ import { JsonBlock, projectUserText, StateDot } from '@deepseek-ai/dsh-client-ui-primitives'
1346
+ import type { ChatNodeOwnerProps, ChatNodeViewProps, ChatViewSlotProps } from '../contract/slots.ts'
1347
+ import type { ModelRetryNode, TurnErrorNode, UserMessageNode } from '../contract/snapshot.ts'
1093
1348
  @@ -11,14 +11,17 @@ import { MessageIconActions } from './MessageIconActions.tsx'
1094
1349
  import css from './MessageItem.module.css'
1095
1350
 
@@ -1099,22 +1354,28 @@ index 467e486ecb3b28ddf7aa99281ad231032a98a76c..d89c77009fb52bdf7992f8f35c14553b
1099
1354
  function contentParts(content: readonly unknown[]): {
1100
1355
  text: string
1101
1356
  images: { attachment: UserImage['attachment'] }[]
1102
- + documents: { name: string; mediaType: string; bytes: number }[]
1357
+ + documents: MessageDocumentSource[]
1103
1358
  rest: unknown[]
1104
1359
  } {
1105
1360
  const texts: string[] = []
1106
1361
  const images: { attachment: UserImage['attachment'] }[] = []
1107
- + const documents: { name: string; mediaType: string; bytes: number }[] = []
1362
+ + const documents: MessageDocumentSource[] = []
1108
1363
  const rest: unknown[] = []
1109
1364
  for (const block of content) {
1110
1365
  const b = block as { type?: string; text?: string; attachment?: unknown }
1111
- @@ -26,9 +29,13 @@ function contentParts(content: readonly unknown[]): {
1366
+ @@ -26,9 +29,19 @@ function contentParts(content: readonly unknown[]): {
1112
1367
  else if (b.type === 'image' && b.attachment !== undefined) {
1113
1368
  images.push({ attachment: (b as UserImage).attachment })
1114
1369
  }
1115
1370
  + else if (b.type === 'document' && b.attachment !== undefined) {
1116
- + const attachment = (b as UserDocument).attachment
1117
- + documents.push({ name: attachment.name, mediaType: attachment.mediaType, bytes: attachment.bytes })
1371
+ + const document = b as UserDocument
1372
+ + const attachment = document.attachment
1373
+ + documents.push({
1374
+ + previewAttachmentId: document.parsed.markdown.attachmentId,
1375
+ + name: attachment.name,
1376
+ + mediaType: attachment.mediaType,
1377
+ + bytes: attachment.bytes,
1378
+ + })
1118
1379
  + }
1119
1380
  else rest.push(block)
1120
1381
  }
@@ -1123,8 +1384,8 @@ index 467e486ecb3b28ddf7aa99281ad231032a98a76c..d89c77009fb52bdf7992f8f35c14553b
1123
1384
  }
1124
1385
 
1125
1386
  function retrySeconds(milliseconds: number): number {
1126
- @@ -164,7 +171,7 @@ function UserStyleBubble({
1127
- previewImages?: readonly MessageImageSource[]
1387
+ @@ -166,7 +179,7 @@ function UserStyleBubble({
1388
+ reveal?: 'always' | 'hover'
1128
1389
  t: ChatViewSlotProps['t']
1129
1390
  }): ReactNode {
1130
1391
  - const { text, images: contentImages, rest } = contentParts(content)
@@ -1132,8 +1393,8 @@ index 467e486ecb3b28ddf7aa99281ad231032a98a76c..d89c77009fb52bdf7992f8f35c14553b
1132
1393
  const images = previewImages ?? contentImages
1133
1394
  const truncated = (total: number): string => t('json.truncated', { total })
1134
1395
  const showBubble = text !== '' || rest.length > 0
1135
- @@ -176,7 +183,7 @@ function UserStyleBubble({
1136
- data-time-hover-root
1396
+ @@ -178,7 +191,7 @@ function UserStyleBubble({
1397
+ data-actions-reveal={reveal}
1137
1398
  >
1138
1399
  <div className={css.userStack}>
1139
1400
  - {renderMessageImages({ images, align: 'end' })}
@@ -1142,7 +1403,7 @@ index 467e486ecb3b28ddf7aa99281ad231032a98a76c..d89c77009fb52bdf7992f8f35c14553b
1142
1403
  {projectUserText(text, referenceLabels)}
1143
1404
  {rest.map((block, i) => <JsonBlock key={i} label={t('message.extraBlock')} payload={block} truncatedLabel={truncated} />)}
1144
1405
  diff --git a/packages/client/ui-chat/src/client/contract/slots.ts b/packages/client/ui-chat/src/client/contract/slots.ts
1145
- index 307d8f703b013962d19212618c522ef35feec0d2..0183aede0996a91d4de2f17bdf54e0b333708eac 100644
1406
+ index 307d8f703b..7679be0fba 100644
1146
1407
  --- a/packages/client/ui-chat/src/client/contract/slots.ts
1147
1408
  +++ b/packages/client/ui-chat/src/client/contract/slots.ts
1148
1409
  @@ -1,7 +1,7 @@
@@ -1165,19 +1426,33 @@ index 307d8f703b013962d19212618c522ef35feec0d2..0183aede0996a91d4de2f17bdf54e0b3
1165
1426
 
1166
1427
  /** Details-panel callbacks. */
1167
1428
  export interface DetailsInjected {
1168
- @@ -182,6 +184,7 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
1429
+ @@ -182,6 +184,13 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
1169
1430
  * registration replaces the shipped gallery; without one, images are omitted.
1170
1431
  */
1171
1432
  'conversation.message.images': { kind: 'single'; scope: 'session'; owner: MessageImagesOwnerProps }
1433
+ + /**
1434
+ + * Render durable Document cards inside one Chat message's attachment rail.
1435
+ + * A contribution receives the message's projected Document metadata and
1436
+ + * replaces the prior single card renderer. Without one, Chat keeps image
1437
+ + * presentation but omits Document cards.
1438
+ + */
1172
1439
  + 'conversation.message.images.documents': { kind: 'single'; scope: 'session'; owner: { documents: readonly MessageDocumentSource[] } }
1173
1440
  /**
1174
1441
  * Command row keyed by the command name. The component receives the folded
1175
1442
  * command lifecycle and linked compaction when present. Reusing a key
1176
1443
  diff --git a/packages/client/ui-conversation/src/client/apply.ts b/packages/client/ui-conversation/src/client/apply.ts
1177
- index b154e73f4abc7034d93cac9b4634c65483731a08..8c9c1a2c7a2a7d2a3ea02e7931959065589d19dc 100644
1444
+ index b154e73f4a..480e242a30 100644
1178
1445
  --- a/packages/client/ui-conversation/src/client/apply.ts
1179
1446
  +++ b/packages/client/ui-conversation/src/client/apply.ts
1180
- @@ -254,6 +254,10 @@ export function apply(ctx: Context): void {
1447
+ @@ -244,6 +244,7 @@ export function apply(ctx: Context): void {
1448
+ locale: NS,
1449
+ children: {
1450
+ 'conversation.input.attachments': { kind: 'single', scope: 'session-maybe' },
1451
+ + 'conversation.input.attachment-picker': { kind: 'single', scope: 'session-maybe' },
1452
+ 'conversation.input.plan': { kind: 'single', scope: 'session' },
1453
+ 'conversation.input.model': { kind: 'single', scope: 'session' },
1454
+ },
1455
+ @@ -254,6 +255,10 @@ export function apply(ctx: Context): void {
1181
1456
  addImages: undefined,
1182
1457
  removeImage: undefined,
1183
1458
  draftImages: undefined,
@@ -1188,7 +1463,7 @@ index b154e73f4abc7034d93cac9b4634c65483731a08..8c9c1a2c7a2a7d2a3ea02e7931959065
1188
1463
  resolveSubmitMode: (running, gesture, steeringAvailable) =>
1189
1464
  submissionPolicy.resolve(running, gesture, steeringAvailable),
1190
1465
  toggleCommandMenu: undefined,
1191
- @@ -269,25 +273,51 @@ export function apply(ctx: Context): void {
1466
+ @@ -269,25 +274,51 @@ export function apply(ctx: Context): void {
1192
1467
  const conversation = concreteConversation(ctx)
1193
1468
  const shell = inputHub.shell(sessionId)
1194
1469
  const inputTriggers = inputHub.inputTriggers(sessionId)
@@ -1253,10 +1528,18 @@ index b154e73f4abc7034d93cac9b4634c65483731a08..8c9c1a2c7a2a7d2a3ea02e7931959065
1253
1528
  submissionPolicy.resolve(running, gesture, steeringAvailable),
1254
1529
  toggleCommandMenu: inputTriggers === undefined
1255
1530
  diff --git a/packages/client/ui-conversation/src/client/contract/slots.ts b/packages/client/ui-conversation/src/client/contract/slots.ts
1256
- index 377a75ee0019ac0b791bf659ac390df1bd46d81a..9b8cc42deb4c58fc8e00fc54aa244b829f9968f8 100644
1531
+ index 377a75ee00..08f4507c92 100644
1257
1532
  --- a/packages/client/ui-conversation/src/client/contract/slots.ts
1258
1533
  +++ b/packages/client/ui-conversation/src/client/contract/slots.ts
1259
- @@ -34,17 +34,45 @@ export interface ComposerAttachment {
1534
+ @@ -1,6 +1,6 @@
1535
+ /** Target-neutral Conversation slot declarations and composed component props. */
1536
+ import type { ReactNode, RefObject } from 'react'
1537
+ -import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
1538
+ +import type { AttachmentIdType, ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
1539
+ import type { SessionSnapshot } from '@deepseek-ai/dsh-api-session-controller/client'
1540
+ import type { WorkspaceSnapshot } from '@deepseek-ai/dsh-api-workspace-controller/client'
1541
+ import type {
1542
+ @@ -34,17 +34,53 @@ export interface ComposerAttachment {
1260
1543
  height?: number
1261
1544
  }
1262
1545
 
@@ -1280,6 +1563,14 @@ index 377a75ee0019ac0b791bf659ac390df1bd46d81a..9b8cc42deb4c58fc8e00fc54aa244b82
1280
1563
  + documents: readonly ComposerDocumentAttachment[]
1281
1564
  + onRemoveDocument: (id: DraftAttachmentId) => void
1282
1565
  +}
1566
+ +
1567
+ +/** Mixed file chooser state handed to the optional attachment UI plugin. */
1568
+ +export interface ComposerAttachmentPickerOwnerProps {
1569
+ + /** Whether this composer can accept another browser file batch. */
1570
+ + canAcceptFiles: boolean
1571
+ + /** Add one selected file batch through the composer's validation path. */
1572
+ + onAddFiles: (files: readonly File[]) => void
1573
+ +}
1283
1574
  +
1284
1575
  export interface ComposerAttachmentsOwnerProps {
1285
1576
  /** Browser-owned draft images in input order. */
@@ -1304,13 +1595,14 @@ index 377a75ee0019ac0b791bf659ac390df1bd46d81a..9b8cc42deb4c58fc8e00fc54aa244b82
1304
1595
  dropLimits?: { readonly count: number; readonly size: string } | undefined
1305
1596
  }
1306
1597
 
1307
- @@ -71,10 +99,19 @@ export type MessageImageLoader = ((attachment: ImageAttachmentRef) => Promise<st
1598
+ @@ -71,10 +107,20 @@ export type MessageImageLoader = ((attachment: ImageAttachmentRef) => Promise<st
1308
1599
  peek?: (attachment: ImageAttachmentRef) => string | undefined
1309
1600
  }
1310
1601
 
1311
1602
  -/** Message image group handed to the optional attachment presentation plugin. */
1312
1603
  +/** Display metadata for one durable Document history card. */
1313
1604
  +export interface MessageDocumentSource {
1605
+ + readonly previewAttachmentId: AttachmentIdType
1314
1606
  + readonly name: string
1315
1607
  + readonly mediaType: string
1316
1608
  + readonly bytes: number
@@ -1321,14 +1613,20 @@ index 377a75ee0019ac0b791bf659ac390df1bd46d81a..9b8cc42deb4c58fc8e00fc54aa244b82
1321
1613
  /** Durable references or submission-echo previews in source order. */
1322
1614
  images: readonly MessageImageSource[]
1323
1615
  + /** Durable Document cards associated with this user message. */
1324
- + documents?: readonly MessageDocumentSource[]
1616
+ + documents: readonly MessageDocumentSource[]
1325
1617
  /** Session-authorized image URL loader for the durable arm. */
1326
1618
  loadImage: MessageImageLoader
1327
1619
  /** Horizontal placement inside the owning record. */
1328
- @@ -141,6 +178,12 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
1620
+ @@ -141,6 +187,18 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
1329
1621
  scope: 'session-maybe'
1330
1622
  owner: ComposerAttachmentsOwnerProps
1331
1623
  }
1624
+ + /** Optional mixed file chooser placed immediately after the command button. */
1625
+ + 'conversation.input.attachment-picker': {
1626
+ + kind: 'single'
1627
+ + scope: 'session-maybe'
1628
+ + owner: ComposerAttachmentPickerOwnerProps
1629
+ + }
1332
1630
  + /** Document cards rendered inside the shared attachment rail. */
1333
1631
  + 'conversation.input.attachments.documents': {
1334
1632
  + kind: 'single'
@@ -1338,7 +1636,7 @@ index 377a75ee0019ac0b791bf659ac390df1bd46d81a..9b8cc42deb4c58fc8e00fc54aa244b82
1338
1636
  /** Plan control inside the composer tool row. */
1339
1637
  'conversation.input.plan': { kind: 'single'; scope: 'session'; owner: InputControlOwnerProps }
1340
1638
  /** Model selector inside the composer tool row. */
1341
- @@ -267,6 +310,14 @@ export interface ComposerBarInjected {
1639
+ @@ -267,6 +325,14 @@ export interface ComposerBarInjected {
1342
1640
  addImages: ((files: readonly File[]) => string | null) | undefined
1343
1641
  removeImage: ((id: DraftAttachmentId) => void) | undefined
1344
1642
  draftImages: ((ids: readonly DraftAttachmentId[]) => readonly ComposerAttachment[]) | undefined
@@ -1353,16 +1651,36 @@ index 377a75ee0019ac0b791bf659ac390df1bd46d81a..9b8cc42deb4c58fc8e00fc54aa244b82
1353
1651
  resolveSubmitMode: (
1354
1652
  running: boolean,
1355
1653
  gesture: ComposerSubmitGesture,
1654
+ @@ -292,7 +358,8 @@ export interface InputControlOwnerProps {
1655
+ export type ComposerBarProps =
1656
+ PropsRuntime<'conversation.composer.bar'>
1657
+ & PropsRenderSlots<
1658
+ - 'conversation.input.attachments' | 'conversation.input.plan' | 'conversation.input.model'
1659
+ + | 'conversation.input.attachments' | 'conversation.input.attachment-picker'
1660
+ + | 'conversation.input.plan' | 'conversation.input.model'
1661
+ >
1662
+ & InjectFace<ComposerBarInjected>
1663
+ & PropsLocale<'conversation'>
1664
+ @@ -356,6 +423,8 @@ export type ConversationSessionHeaderSlotProps =
1665
+ /** Full props of the draft-image attachment renderer. */
1666
+ export type ComposerAttachmentsProps =
1667
+ PropsRuntime<'conversation.input.attachments'> & PropsLocale<'conversation'>
1668
+ +export type ComposerAttachmentPickerProps =
1669
+ + PropsRuntime<'conversation.input.attachment-picker'> & PropsLocale<'conversation'>
1670
+
1671
+ /** Owner share common to blank-session Workspace pickers. */
1672
+ export interface EmptyWorkspaceOwnerProps {
1356
1673
  diff --git a/packages/client/ui-conversation/src/client/index.ts b/packages/client/ui-conversation/src/client/index.ts
1357
- index 102b385e8cfedd49b769c45446b31e0b96dd1782..1b08839b96a6a937b3216da2092ca6f2d4981c29 100644
1674
+ index 102b385e8c..a278db2780 100644
1358
1675
  --- a/packages/client/ui-conversation/src/client/index.ts
1359
1676
  +++ b/packages/client/ui-conversation/src/client/index.ts
1360
- @@ -46,14 +46,14 @@ export { ConversationViewRegistry } from './conversation/view-registry.ts'
1677
+ @@ -46,14 +46,15 @@ export { ConversationViewRegistry } from './conversation/view-registry.ts'
1361
1678
 
1362
1679
  export type { ConversationKey } from './locales.ts'
1363
1680
  export type {
1364
1681
  - ComposerAttachment, ComposerAttachmentsOwnerProps, ComposerAttachmentsProps,
1365
- + ComposerAttachment, ComposerDocumentAttachment, ComposerDocumentLimits, ComposerAttachmentsOwnerProps, ComposerAttachmentsProps,
1682
+ + ComposerAttachment, ComposerAttachmentPickerOwnerProps, ComposerAttachmentPickerProps,
1683
+ + ComposerDocumentAttachment, ComposerDocumentLimits, ComposerAttachmentsOwnerProps, ComposerAttachmentsProps,
1366
1684
  ComposerBarInjected, ComposerBarOwnerProps, ComposerBarProps, ComposerChainProps,
1367
1685
  ConversationHeaderActionOwnerProps, ConversationHeaderLineageOwnerProps,
1368
1686
  ConversationInjected, ConversationSessionHeaderInjected, ConversationSessionHeaderSlotProps,
@@ -1375,7 +1693,7 @@ index 102b385e8cfedd49b769c45446b31e0b96dd1782..1b08839b96a6a937b3216da2092ca6f2
1375
1693
  } from './contract/slots.ts'
1376
1694
  export type {
1377
1695
  diff --git a/packages/client/ui-conversation/src/client/locales.ts b/packages/client/ui-conversation/src/client/locales.ts
1378
- index 9eaf2a82a02fad08032dfc50d2720b8c77d26c69..1cf4726092bea967269029a8bc9c2b335014b457 100644
1696
+ index 3b1f9c1fc8..ab8e44d430 100644
1379
1697
  --- a/packages/client/ui-conversation/src/client/locales.ts
1380
1698
  +++ b/packages/client/ui-conversation/src/client/locales.ts
1381
1699
  @@ -27,6 +27,7 @@ export const zh = {
@@ -1386,7 +1704,7 @@ index 9eaf2a82a02fad08032dfc50d2720b8c77d26c69..1cf4726092bea967269029a8bc9c2b33
1386
1704
  'image.openOriginal': '查看原图',
1387
1705
  'image.openOriginalLabel': '{label},点击查看原图',
1388
1706
  'image.remove': '移除图片 {name}',
1389
- @@ -175,6 +176,7 @@ export const en = {
1707
+ @@ -177,6 +178,7 @@ export const en = {
1390
1708
  'image.dropDesc': 'Up to {count} images, {size} each',
1391
1709
  'image.dropBlocked': 'Images cannot be added right now',
1392
1710
  'image.pending': 'Pending images',
@@ -1395,7 +1713,7 @@ index 9eaf2a82a02fad08032dfc50d2720b8c77d26c69..1cf4726092bea967269029a8bc9c2b33
1395
1713
  'image.openOriginalLabel': '{label}, click to view original',
1396
1714
  'image.remove': 'Remove image {name}',
1397
1715
  diff --git a/packages/client/ui-conversation/src/client/service.ts b/packages/client/ui-conversation/src/client/service.ts
1398
- index 2fb0c99b51092091815af04a172699de24cd867d..0321824f6ada49487995ec01f4487e2b51d5ef47 100644
1716
+ index fd59a492f0..7357b9cc72 100644
1399
1717
  --- a/packages/client/ui-conversation/src/client/service.ts
1400
1718
  +++ b/packages/client/ui-conversation/src/client/service.ts
1401
1719
  @@ -18,7 +18,7 @@ import type {
@@ -1796,15 +2114,13 @@ index 2fb0c99b51092091815af04a172699de24cd867d..0321824f6ada49487995ec01f4487e2b
1796
2114
  if (url.startsWith('blob:')) URL.revokeObjectURL(url)
1797
2115
  }
1798
2116
  diff --git a/packages/client/ui-conversation/src/client/skeleton/InputBar.tsx b/packages/client/ui-conversation/src/client/skeleton/InputBar.tsx
1799
- index d6126acdfd62b2b9be0af3fbb99c63b4435859b8..cf99cdac86bc2f308032de81baa89c040761ff5e 100644
2117
+ index d4c06d8bf3..d0bce93cd1 100644
1800
2118
  --- a/packages/client/ui-conversation/src/client/skeleton/InputBar.tsx
1801
2119
  +++ b/packages/client/ui-conversation/src/client/skeleton/InputBar.tsx
1802
- @@ -40,7 +40,8 @@ import css from './InputBar.module.css'
1803
- export type InputBarProps = ComposerBarProps
2120
+ @@ -41,6 +41,7 @@ export type InputBarProps = ComposerBarProps
1804
2121
 
1805
2122
  export function InputBar({
1806
- - useSession, useInput, inputActions, keyboard, addImages, removeImage, draftImages,
1807
- + useSession, useInput, inputActions, keyboard, removeImage, draftImages,
2123
+ useSession, useInput, inputActions, keyboard, addImages, removeImage, draftImages,
1808
2124
  + addFiles, resolveDocumentDropLabels, removeDocument, draftDocuments,
1809
2125
  resolveSubmitMode, toggleCommandMenu, stop, command, t,
1810
2126
  renderSlot, useNotices, useLexicon, useMenuLauncher,
@@ -1830,7 +2146,7 @@ index d6126acdfd62b2b9be0af3fbb99c63b4435859b8..cf99cdac86bc2f308032de81baa89c04
1830
2146
  // Prompt failures are ordinary failures (no create/attach transaction exists
1831
2147
  // anymore): the toast announces promptError, the draft stays in the machine,
1832
2148
  // and the user resubmits. A remount over a session whose machine still holds
1833
- @@ -134,10 +140,13 @@ export function InputBar({
2149
+ @@ -136,10 +142,13 @@ export function InputBar({
1834
2150
 
1835
2151
  useEffect(() => {
1836
2152
  if (input === undefined || inputActions === undefined) return
@@ -1847,7 +2163,7 @@ index d6126acdfd62b2b9be0af3fbb99c63b4435859b8..cf99cdac86bc2f308032de81baa89c04
1847
2163
 
1848
2164
  // Scroll the draft scrollport the minimum that brings the selection focus
1849
2165
  // into view — the browser's own behavior for typing, performed for the
1850
- @@ -210,46 +219,55 @@ export function InputBar({
2166
+ @@ -212,46 +221,67 @@ export function InputBar({
1851
2167
  return () => { el.removeEventListener('wheel', onWheel) }
1852
2168
  }, [])
1853
2169
 
@@ -1856,8 +2172,29 @@ index d6126acdfd62b2b9be0af3fbb99c63b4435859b8..cf99cdac86bc2f308032de81baa89c04
1856
2172
  - // never enters the rail — no more submit-time failure rolling the rail
1857
2173
  - // back. The host enforces the same limits at submit for callers that bypass
1858
2174
  - // this composer.
1859
- - const intakeImages = useCallback((files: readonly File[]): void => {
1860
- - if (addImages === undefined || files.length === 0) return
2175
+ + // Intake pre-check: an addition that would break a projected image limit is
2176
+ + // refused before the single mixed draft transaction starts. The Host repeats
2177
+ + // every admission check for callers that bypass this composer.
2178
+ + const imageIntakeError = useCallback((files: readonly File[]): string | null => {
2179
+ + if (files.length === 0 || imageLimits === undefined) return null
2180
+ + if (files.some(file => !(imageLimits.mediaTypes as readonly string[]).includes(file.type))) {
2181
+ + return t('image.unsupportedType')
2182
+ + }
2183
+ + if (attachments.length + files.length > imageLimits.maxImagesPerMessage) {
2184
+ + return t('image.tooMany', { count: imageLimits.maxImagesPerMessage })
2185
+ + }
2186
+ + if (files.some(file => file.size > imageLimits.maxImageBytes)) {
2187
+ + return t('image.fileTooLarge', { size: imageSizeText(imageLimits.maxImageBytes) })
2188
+ + }
2189
+ + const total = attachments.reduce((sum, attachment) => sum + attachment.file.size, 0)
2190
+ + + files.reduce((sum, file) => sum + file.size, 0)
2191
+ + return total > imageLimits.maxMessageImageBytes
2192
+ + ? t('image.totalTooLarge', { size: imageSizeText(imageLimits.maxMessageImageBytes) })
2193
+ + : null
2194
+ + }, [attachments, imageLimits, t])
2195
+ +
2196
+ const intakeImages = useCallback((files: readonly File[]): void => {
2197
+ if (addImages === undefined || files.length === 0) return
1861
2198
  - const rejected = ((): string | null => {
1862
2199
  - if (imageLimits !== undefined) {
1863
2200
  - // Format precedes limits: a batch with
@@ -1880,29 +2217,21 @@ index d6126acdfd62b2b9be0af3fbb99c63b4435859b8..cf99cdac86bc2f308032de81baa89c04
1880
2217
  - }
1881
2218
  - return addImages(files)
1882
2219
  - })()
1883
- + // Intake pre-check: an addition that would break a projected image limit is
1884
- + // refused before the single mixed draft transaction starts. The Host repeats
1885
- + // every admission check for callers that bypass this composer.
1886
- + const imageIntakeError = useCallback((files: readonly File[]): string | null => {
1887
- + if (files.length === 0 || imageLimits === undefined) return null
1888
- + if (files.some(file => !(imageLimits.mediaTypes as readonly string[]).includes(file.type))) {
1889
- + return t('image.unsupportedType')
1890
- + }
1891
- + if (attachments.length + files.length > imageLimits.maxImagesPerMessage) {
1892
- + return t('image.tooMany', { count: imageLimits.maxImagesPerMessage })
1893
- + }
1894
- + if (files.some(file => file.size > imageLimits.maxImageBytes)) {
1895
- + return t('image.fileTooLarge', { size: imageSizeText(imageLimits.maxImageBytes) })
2220
+ + const imageRejected = imageIntakeError(files)
2221
+ + if (imageRejected !== null) {
2222
+ + showToast(imageRejected)
2223
+ + return
1896
2224
  + }
1897
- + const total = attachments.reduce((sum, attachment) => sum + attachment.file.size, 0)
1898
- + + files.reduce((sum, file) => sum + file.size, 0)
1899
- + return total > imageLimits.maxMessageImageBytes
1900
- + ? t('image.totalTooLarge', { size: imageSizeText(imageLimits.maxMessageImageBytes) })
1901
- + : null
1902
- + }, [attachments, imageLimits, t])
2225
+ + const rejected = addImages(files)
2226
+ + if (rejected !== null) showToast(rejected)
2227
+ + }, [addImages, imageIntakeError, showToast])
1903
2228
  +
1904
2229
  + const intakeFiles = useCallback((files: readonly File[]): void => {
1905
- + if (addFiles === undefined || files.length === 0) return
2230
+ + if (files.length === 0) return
2231
+ + if (addFiles === undefined) {
2232
+ + intakeImages(files)
2233
+ + return
2234
+ + }
1906
2235
  + const documentTypes = draftDocuments === undefined || documentLimits === undefined
1907
2236
  + ? []
1908
2237
  + : documentLimits.mediaTypes as readonly string[]
@@ -1916,14 +2245,11 @@ index d6126acdfd62b2b9be0af3fbb99c63b4435859b8..cf99cdac86bc2f308032de81baa89c04
1916
2245
  + const rejected = addFiles(imageFiles, documentFiles, documentLimits)
1917
2246
  if (rejected !== null) showToast(rejected)
1918
2247
  - }, [addImages, attachments, imageLimits, showToast, t])
1919
- + }, [addFiles, documentLimits, draftDocuments, imageIntakeError, showToast])
1920
- +
1921
- + const intakeImages = useCallback((files: readonly File[]): void => {
1922
- + intakeFiles(files)
1923
- + }, [intakeFiles])
2248
+ + }, [addFiles, documentLimits, draftDocuments, imageIntakeError, intakeImages, showToast])
1924
2249
 
1925
2250
  - const canAcceptDrop = !locked && !machineBusy && addImages !== undefined
1926
- + const canAcceptDrop = !locked && !machineBusy && addFiles !== undefined
2251
+ + const canAcceptDrop = !locked && !machineBusy && (addFiles !== undefined || addImages !== undefined)
2252
+ + const canAcceptFiles = !locked && !machineBusy && addFiles !== undefined
1927
2253
 
1928
2254
  // The keymap handlers read live bar state through this ref so the editor
1929
2255
  // registration survives re-renders without re-arming per keystroke.
@@ -1936,7 +2262,7 @@ index d6126acdfd62b2b9be0af3fbb99c63b4435859b8..cf99cdac86bc2f308032de81baa89c04
1936
2262
 
1937
2263
  useEffect(() => {
1938
2264
  if (editor === null || keyboard === undefined) return
1939
- @@ -276,7 +294,7 @@ export function InputBar({
2265
+ @@ -278,7 +308,7 @@ export function InputBar({
1940
2266
  g.subagent === null,
1941
2267
  ))
1942
2268
  },
@@ -1945,7 +2271,7 @@ index d6126acdfd62b2b9be0af3fbb99c63b4435859b8..cf99cdac86bc2f308032de81baa89c04
1945
2271
  pasteText: (text) => {
1946
2272
  if (gate.current.machineBusy || gate.current.locked) return
1947
2273
  keyboard.paste(text)
1948
- @@ -393,9 +411,13 @@ export function InputBar({
2274
+ @@ -395,9 +425,13 @@ export function InputBar({
1949
2275
  {accessory !== undefined && <div className={css.accessory}>{accessory}</div>}
1950
2276
  {renderSlot('conversation.input.attachments', {
1951
2277
  attachments,
@@ -1959,11 +2285,22 @@ index d6126acdfd62b2b9be0af3fbb99c63b4435859b8..cf99cdac86bc2f308032de81baa89c04
1959
2285
  dropLimits: imageLimits === undefined ? undefined : {
1960
2286
  count: imageLimits.maxImagesPerMessage,
1961
2287
  size: imageSizeText(imageLimits.maxImageBytes),
2288
+ @@ -450,6 +484,10 @@ export function InputBar({
2289
+ <IconPlusOutline16 size={14} />
2290
+ </button>
2291
+ </Tooltip>
2292
+ + {renderSlot('conversation.input.attachment-picker', {
2293
+ + canAcceptFiles,
2294
+ + onAddFiles: intakeFiles,
2295
+ + })}
2296
+ <div className={css.modes}>
2297
+ {accessSelect}
2298
+ {sessionId === undefined ? null : renderSlot('conversation.input.plan', { locked })}
1962
2299
  diff --git a/packages/client/ui-conversation/tests/input-bar.client.spec.tsx b/packages/client/ui-conversation/tests/input-bar.client.spec.tsx
1963
- index 9becd7f1095f135ecd3d24b6a06f52b956fe4834..b00b618d0bd1288a552cc4dcb46760c9e2198129 100644
2300
+ index fafa85283b..9a8ee83691 100644
1964
2301
  --- a/packages/client/ui-conversation/tests/input-bar.client.spec.tsx
1965
2302
  +++ b/packages/client/ui-conversation/tests/input-bar.client.spec.tsx
1966
- @@ -173,6 +173,10 @@ function bench(over?: BenchOptions) {
2303
+ @@ -175,6 +175,10 @@ function bench(over?: BenchOptions) {
1967
2304
  const attachment = over?.attachments?.find(candidate => candidate.id === id)
1968
2305
  return attachment === undefined ? [] : [attachment]
1969
2306
  }),
@@ -1974,8 +2311,46 @@ index 9becd7f1095f135ecd3d24b6a06f52b956fe4834..b00b618d0bd1288a552cc4dcb46760c9
1974
2311
  resolveSubmitMode: (running, gesture, steeringAvailable) => {
1975
2312
  if (!running || !steeringAvailable) return 'queue'
1976
2313
  const preferred = over?.busyEnter ?? 'queue'
2314
+ @@ -320,7 +324,7 @@ describe('image draft rail', () => {
2315
+ new File([new ArrayBuffer(64)], 'b.pdf', { type: 'application/pdf' }),
2316
+ ]
2317
+ act(() => { attachmentOwner(result.slotCalls).onAddImages(files) })
2318
+ - expect(addImages).toHaveBeenCalledWith(files)
2319
+ + expect(addImages).not.toHaveBeenCalled()
2320
+ expect(result.view.getByRole('alert').textContent).toContain('仅支持 PNG、JPG、WebP、GIF 格式的图片')
2321
+ })
2322
+
2323
+ @@ -1300,7 +1304,8 @@ describe('command launcher chrome and control seats', () => {
2324
+ // Every seat dispatched, nothing rendered (render passes may repeat; the
2325
+ // seat set is the contract).
2326
+ expect([...new Set(slotCalls.map(c => c.key))]).toEqual([
2327
+ - 'conversation.input.attachments', 'conversation.input.plan', 'conversation.input.model',
2328
+ + 'conversation.input.attachments', 'conversation.input.attachment-picker',
2329
+ + 'conversation.input.plan', 'conversation.input.model',
2330
+ ])
2331
+ expect(view.queryByLabelText('Plan mode')).toBeNull()
2332
+ expect(view.queryByLabelText('Model')).toBeNull()
2333
+ @@ -1466,12 +1471,16 @@ describe('command launcher chrome and control seats', () => {
2334
+ expect(view.getByTestId('plan-entry')).toBeTruthy()
2335
+ expect(view.getByTestId('model-entry')).toBeTruthy()
2336
+ // The bar hands its chrome disable state to the filling entry.
2337
+ - const controls = slotCalls.filter(call => call.key !== 'conversation.input.attachments')
2338
+ + const controls = slotCalls.filter(call =>
2339
+ + call.key !== 'conversation.input.attachments'
2340
+ + && call.key !== 'conversation.input.attachment-picker')
2341
+ expect(controls.every(c => (c.owner as { locked: boolean }).locked)).toBe(true)
2342
+ expect(attachmentOwner(slotCalls).canAcceptDrop).toBe(false)
2343
+ cleanup()
2344
+ const live = bench({ running: true })
2345
+ - const liveControls = live.slotCalls.filter(call => call.key !== 'conversation.input.attachments')
2346
+ + const liveControls = live.slotCalls.filter(call =>
2347
+ + call.key !== 'conversation.input.attachments'
2348
+ + && call.key !== 'conversation.input.attachment-picker')
2349
+ expect(liveControls.every(c => !(c.owner as { locked: boolean }).locked)).toBe(true)
2350
+ expect(attachmentOwner(live.slotCalls).canAcceptDrop).toBe(true)
2351
+ })
1977
2352
  diff --git a/packages/client/ui-conversation/tests/input-matrix.client.spec.tsx b/packages/client/ui-conversation/tests/input-matrix.client.spec.tsx
1978
- index bdb878672b080e06ec84b065b5bfbfea04a951ee..dc155229a154a0e23384ad821135146b8bf74866 100644
2353
+ index bdb878672b..dc155229a1 100644
1979
2354
  --- a/packages/client/ui-conversation/tests/input-matrix.client.spec.tsx
1980
2355
  +++ b/packages/client/ui-conversation/tests/input-matrix.client.spec.tsx
1981
2356
  @@ -71,6 +71,10 @@ function mountBar(shell: SessionInputShell, over?: { running?: boolean; disabled
@@ -1990,7 +2365,7 @@ index bdb878672b080e06ec84b065b5bfbfea04a951ee..dc155229a154a0e23384ad821135146b
1990
2365
  toggleCommandMenu: vi.fn(),
1991
2366
  useNotices: bindSnapshotSelector(shell.notices),
1992
2367
  diff --git a/packages/client/ui-conversation/tests/input-scenarios.client.spec.tsx b/packages/client/ui-conversation/tests/input-scenarios.client.spec.tsx
1993
- index ec7f586683f96d9374bc31cc0e6d8b39e4a4df43..266d2e15c742a9eb9dbbea670f98943d83b64198 100644
2368
+ index ec7f586683..266d2e15c7 100644
1994
2369
  --- a/packages/client/ui-conversation/tests/input-scenarios.client.spec.tsx
1995
2370
  +++ b/packages/client/ui-conversation/tests/input-scenarios.client.spec.tsx
1996
2371
  @@ -160,6 +160,10 @@ async function scopedBench(register?: (inputTriggers: InputTriggerService) => vo
@@ -2005,7 +2380,7 @@ index ec7f586683f96d9374bc31cc0e6d8b39e4a4df43..266d2e15c742a9eb9dbbea670f98943d
2005
2380
  toggleCommandMenu: (selection) => {
2006
2381
  const snapshot = shell.snapshot
2007
2382
  diff --git a/packages/client/ui-conversation/tests/skeleton.client.spec.tsx b/packages/client/ui-conversation/tests/skeleton.client.spec.tsx
2008
- index 40f6dfda6d1b6d04003b7c210cd600f6cbaa1d83..24632b55e94b1053793b761e047e03db40ddbe14 100644
2383
+ index 1d3f4159f0..08a36c8912 100644
2009
2384
  --- a/packages/client/ui-conversation/tests/skeleton.client.spec.tsx
2010
2385
  +++ b/packages/client/ui-conversation/tests/skeleton.client.spec.tsx
2011
2386
  @@ -250,6 +250,10 @@ function mount(
@@ -2020,14 +2395,16 @@ index 40f6dfda6d1b6d04003b7c210cd600f6cbaa1d83..24632b55e94b1053793b761e047e03db
2020
2395
  toggleCommandMenu={vi.fn()}
2021
2396
  useNotices={bindSnapshotSelector(wiring.notices)}
2022
2397
  diff --git a/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx b/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx
2023
- index 81a428aa35fada3286b0f7b025347dc6a51a3766..8f40cb75c119394dc32963ce6733775ad7996515 100644
2398
+ index 81a428aa35..ad9d38ff64 100644
2024
2399
  --- a/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx
2025
2400
  +++ b/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx
2026
- @@ -1164,18 +1164,26 @@ function SourceBlocks({
2401
+ @@ -1163,19 +1163,27 @@ function SourceBlocks({
2402
+ {/* The Raw view keeps model block order and granularity: one
2027
2403
  gallery per image block, unlike the aggregated record gallery. */}
2028
2404
  {block.attachment !== undefined
2029
- ? renderImages({ images: [{ attachment: block.attachment }], align: 'start' })
2405
+ - ? renderImages({ images: [{ attachment: block.attachment }], align: 'start' })
2030
2406
  - : <pre className={css.sourceBlockContent}>{block.content}</pre>}
2407
+ + ? renderImages({ images: [{ attachment: block.attachment }], documents: [], align: 'start' })
2031
2408
  + : block.document !== undefined
2032
2409
  + ? renderImages({ images: [], documents: [block.document], align: 'start' })
2033
2410
  + : <pre className={css.sourceBlockContent}>{block.content}</pre>}
@@ -2082,7 +2459,7 @@ index 81a428aa35fada3286b0f7b025347dc6a51a3766..8f40cb75c119394dc32963ce6733775a
2082
2459
  - {renderImages({ images: [{ attachment: block.attachment }], align: 'start' })}
2083
2460
  + {renderImages({
2084
2461
  + images: block.attachment === undefined ? [] : [{ attachment: block.attachment }],
2085
- + ...(block.document === undefined ? {} : { documents: [block.document] }),
2462
+ + documents: block.document === undefined ? [] : [block.document],
2086
2463
  + align: 'start',
2087
2464
  + })}
2088
2465
  </div>
@@ -2111,20 +2488,47 @@ index 81a428aa35fada3286b0f7b025347dc6a51a3766..8f40cb75c119394dc32963ce6733775a
2111
2488
  }
2112
2489
  return (
2113
2490
  diff --git a/packages/client/ui-trajectory/src/client/layout.ts b/packages/client/ui-trajectory/src/client/layout.ts
2114
- index a811e915f20d7a2247726dc6a750bb67e0697304..8676ea46dcf367243f96cf53ffa29b135e8ceffd 100644
2491
+ index a811e915f2..0353bce987 100644
2115
2492
  --- a/packages/client/ui-trajectory/src/client/layout.ts
2116
2493
  +++ b/packages/client/ui-trajectory/src/client/layout.ts
2117
- @@ -841,6 +841,16 @@ function sourceBlock(value: unknown): TrajectorySourceBlock {
2494
+ @@ -6,6 +6,7 @@ import type {
2495
+ AssistantBlock,
2496
+ AssistantMessageNode,
2497
+ ConversationLocation,
2498
+ + MessageDocumentSource,
2499
+ RequestInspectionSnapshot,
2500
+ RequestPromptChange,
2501
+ RequestView,
2502
+ @@ -841,6 +842,35 @@ function sourceBlock(value: unknown): TrajectorySourceBlock {
2118
2503
  // wire-shaped 'other' blocks with an unrelated `attachment` member out.
2119
2504
  return { type, content: '', attachment: block.attachment as ImageAttachmentRef }
2120
2505
  }
2121
- + if (type === 'document' && typeof block.attachment === 'object' && block.attachment !== null) {
2506
+ + if (
2507
+ + type === 'document'
2508
+ + && typeof block.attachment === 'object' && block.attachment !== null
2509
+ + && typeof block.parsed === 'object' && block.parsed !== null
2510
+ + ) {
2122
2511
  + const attachment = block.attachment as Record<string, unknown>
2123
- + if (typeof attachment.name === 'string' && typeof attachment.mediaType === 'string' && typeof attachment.bytes === 'number') {
2512
+ + const parsed = block.parsed as Record<string, unknown>
2513
+ + const markdown = typeof parsed.markdown === 'object' && parsed.markdown !== null
2514
+ + ? parsed.markdown as Record<string, unknown>
2515
+ + : null
2516
+ + if (
2517
+ + typeof attachment.attachmentId === 'string'
2518
+ + && typeof markdown?.attachmentId === 'string'
2519
+ + && typeof attachment.name === 'string'
2520
+ + && typeof attachment.mediaType === 'string'
2521
+ + && typeof attachment.bytes === 'number'
2522
+ + ) {
2124
2523
  + return {
2125
2524
  + type,
2126
2525
  + content: '',
2127
- + document: { name: attachment.name, mediaType: attachment.mediaType, bytes: attachment.bytes },
2526
+ + document: {
2527
+ + previewAttachmentId: markdown.attachmentId as MessageDocumentSource['previewAttachmentId'],
2528
+ + name: attachment.name,
2529
+ + mediaType: attachment.mediaType,
2530
+ + bytes: attachment.bytes,
2531
+ + },
2128
2532
  + }
2129
2533
  + }
2130
2534
  + }
@@ -2132,7 +2536,7 @@ index a811e915f20d7a2247726dc6a750bb67e0697304..8676ea46dcf367243f96cf53ffa29b13
2132
2536
  }
2133
2537
 
2134
2538
  diff --git a/packages/client/ui-trajectory/src/client/trajectory-contract.ts b/packages/client/ui-trajectory/src/client/trajectory-contract.ts
2135
- index 37571f2b83403944c2cb0e1e8a71b17972461994..1c326ebfca6e1b8636457d04c4517f39630e4d8b 100644
2539
+ index 37571f2b83..6a327454c4 100644
2136
2540
  --- a/packages/client/ui-trajectory/src/client/trajectory-contract.ts
2137
2541
  +++ b/packages/client/ui-trajectory/src/client/trajectory-contract.ts
2138
2542
  @@ -1,6 +1,6 @@
@@ -2143,15 +2547,21 @@ index 37571f2b83403944c2cb0e1e8a71b17972461994..1c326ebfca6e1b8636457d04c4517f39
2143
2547
  RequestView, RunningToolCall, ToolCallBlock,
2144
2548
  } from '@deepseek-ai/dsh-client-ui-conversation/client'
2145
2549
  import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots'
2146
- @@ -93,5 +93,6 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
2550
+ @@ -93,5 +93,12 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
2147
2551
  * images are omitted.
2148
2552
  */
2149
2553
  'conversation.trajectory.images': { kind: 'single'; scope: 'session'; owner: MessageImagesOwnerProps }
2554
+ + /**
2555
+ + * Render durable Document cards inside one Trajectory attachment rail.
2556
+ + * A contribution receives the record's projected Document metadata and
2557
+ + * replaces the prior single card renderer. Without one, Trajectory keeps
2558
+ + * image presentation but omits Document cards.
2559
+ + */
2150
2560
  + 'conversation.trajectory.images.documents': { kind: 'single'; scope: 'session'; owner: { documents: readonly MessageDocumentSource[] } }
2151
2561
  }
2152
2562
  }
2153
2563
  diff --git a/packages/client/ui-trajectory/src/client/trajectory-record.ts b/packages/client/ui-trajectory/src/client/trajectory-record.ts
2154
- index da330f2ae43a55ca813bfffe867dbfc33a2c15a9..0bb73888c955883403d785e7bcc737c8cb0f7054 100644
2564
+ index da330f2ae4..0bb73888c9 100644
2155
2565
  --- a/packages/client/ui-trajectory/src/client/trajectory-record.ts
2156
2566
  +++ b/packages/client/ui-trajectory/src/client/trajectory-record.ts
2157
2567
  @@ -2,7 +2,7 @@
@@ -2171,8 +2581,21 @@ index da330f2ae43a55ca813bfffe867dbfc33a2c15a9..0bb73888c955883403d785e7bcc737c8
2171
2581
  callId?: string
2172
2582
  toolName?: string
2173
2583
  }
2584
+ diff --git a/packages/extensions/cordis-client-runner/src/client/api-catalog.ts b/packages/extensions/cordis-client-runner/src/client/api-catalog.ts
2585
+ index bef6356509..9a79565c0c 100644
2586
+ --- a/packages/extensions/cordis-client-runner/src/client/api-catalog.ts
2587
+ +++ b/packages/extensions/cordis-client-runner/src/client/api-catalog.ts
2588
+ @@ -559,7 +559,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
2589
+ },
2590
+ {
2591
+ name: 'ISession',
2592
+ - declaration: 'export interface ISession {\n readonly sessionId: SessionId;\n readonly projections: ProjectionsFace;\n beginSubmission(input: BeginSubmissionInput): SubmissionHandle;\n prompt(content: PromptContentPart[], mode: \'queue\' | \'steer\', signal?: AbortSignal, requestId?: SessionRequestId): Promise<RemoteResult<{\n accepted: true;\n }>>;\n readAttachment(attachmentId: AttachmentIdType): Promise<RemoteResult<{\n attachment: ImageAttachmentRef;\n data: Uint8Array;\n }>>;\n updateQueue(itemId: MessageId, action: QueueAction): Promise<RemoteResult<{\n accepted: true;\n }>>;\n cancel(): Promise<RemoteResult<{\n accepted: true;\n }>>;\n rename(title: string): Promise<RemoteResult<{\n title: string;\n seq: number;\n }>>;\n loadOlder(): Promise<void>;\n command(line: string): Promise<RemoteResult<{\n matched: boolean;\n }>>;\n}',
2593
+ + declaration: 'export interface ISession {\n readonly sessionId: SessionId;\n readonly projections: ProjectionsFace;\n beginSubmission(input: BeginSubmissionInput): SubmissionHandle;\n prompt(content: PromptContentPart[], mode: \'queue\' | \'steer\', signal?: AbortSignal, requestId?: SessionRequestId): Promise<RemoteResult<{\n accepted: true;\n }>>;\n readAttachment(attachmentId: AttachmentIdType): Promise<RemoteResult<{\n attachment: FileAttachmentRef;\n data: Uint8Array;\n }>>;\n updateQueue(itemId: MessageId, action: QueueAction): Promise<RemoteResult<{\n accepted: true;\n }>>;\n cancel(): Promise<RemoteResult<{\n accepted: true;\n }>>;\n rename(title: string): Promise<RemoteResult<{\n title: string;\n seq: number;\n }>>;\n loadOlder(): Promise<void>;\n command(line: string): Promise<RemoteResult<{\n matched: boolean;\n }>>;\n}',
2594
+ },
2595
+ {
2596
+ name: 'KeyPropsOf',
2174
2597
  diff --git a/packages/llm/llm-deepseek/src/adapter.ts b/packages/llm/llm-deepseek/src/adapter.ts
2175
- index e2116556331159ee348aacfb194d46e2ae7b5920..4e468b655b20fd8a7664334be3365be1d1438237 100644
2598
+ index e211655633..4e468b655b 100644
2176
2599
  --- a/packages/llm/llm-deepseek/src/adapter.ts
2177
2600
  +++ b/packages/llm/llm-deepseek/src/adapter.ts
2178
2601
  @@ -8,7 +8,7 @@
@@ -2235,7 +2658,7 @@ index e2116556331159ee348aacfb194d46e2ae7b5920..4e468b655b20fd8a7664334be3365be1
2235
2658
  )[Symbol.asyncIterator]()
2236
2659
  let exhausted = false
2237
2660
  diff --git a/packages/llm/llm-pi-ai/src/adapter.ts b/packages/llm/llm-pi-ai/src/adapter.ts
2238
- index e20b8e00720d52ef9cbb3712884661e8025f3dde..acd9dcda4fd57e0effe1f2bb6c3f406340219b93 100644
2661
+ index e20b8e0072..058e8f2824 100644
2239
2662
  --- a/packages/llm/llm-pi-ai/src/adapter.ts
2240
2663
  +++ b/packages/llm/llm-pi-ai/src/adapter.ts
2241
2664
  @@ -40,9 +40,11 @@ import type {
@@ -2250,7 +2673,40 @@ index e20b8e00720d52ef9cbb3712884661e8025f3dde..acd9dcda4fd57e0effe1f2bb6c3f4063
2250
2673
  ReasoningEffortId,
2251
2674
  } from '@deepseek-ai/dsh-llm'
2252
2675
  import type {
2253
- @@ -350,20 +352,24 @@ export class PiAiAdapter extends LlmAdapter {
2676
+ @@ -111,6 +113,24 @@ export interface PiAiAuthInjection {
2677
+ authContext: AuthContext
2678
+ }
2679
+
2680
+ +/** Whether one Responses input item is replayed reasoning with response status metadata. */
2681
+ +function hasReasoningInputStatus(item: unknown): boolean {
2682
+ + const inputItem = item as Record<string, unknown>
2683
+ + return inputItem.type === 'reasoning' && Object.hasOwn(inputItem, 'status')
2684
+ +}
2685
+ +
2686
+ +/** Remove response-only status metadata from replayed reasoning input items. */
2687
+ +function omitReasoningInputStatus(payload: unknown): unknown {
2688
+ + const request = payload as Record<string, unknown> & { input: unknown[] }
2689
+ + if (!request.input.some(hasReasoningInputStatus)) return undefined
2690
+ + const input = request.input.map((item) => {
2691
+ + if (!hasReasoningInputStatus(item)) return item
2692
+ + const { status: _status, ...withoutStatus } = item as Record<string, unknown>
2693
+ + return withoutStatus
2694
+ + })
2695
+ + return { ...request, input }
2696
+ +}
2697
+ +
2698
+ /** Copy profile stream knobs into pi-ai's common option vocabulary. */
2699
+ function profileOptions(
2700
+ profile: ResolvedPiAiProviderProfile,
2701
+ @@ -126,6 +146,7 @@ function profileOptions(
2702
+ ...profile.transport === undefined ? {} : { transport: profile.transport },
2703
+ ...profile.timeoutMs === undefined ? {} : { timeoutMs: profile.timeoutMs },
2704
+ ...profile.websocketConnectTimeoutMs === undefined ? {} : { websocketConnectTimeoutMs: profile.websocketConnectTimeoutMs },
2705
+ + ...profile.responsesCompatibility?.omitReasoningInputStatus === true ? { onPayload: omitReasoningInputStatus } : {},
2706
+ // The agent recovery layer owns visible attempts; one adapter call is one SDK attempt.
2707
+ maxRetries: 0,
2708
+ }
2709
+ @@ -350,20 +371,24 @@ export class PiAiAdapter extends LlmAdapter {
2254
2710
  using watchdog = idleWatchdog(upstream, streamIdleTimeoutMs, 'LLM_STREAM_IDLE_TIMEOUT')
2255
2711
 
2256
2712
  try {
@@ -2281,7 +2737,7 @@ index e20b8e00720d52ef9cbb3712884661e8025f3dde..acd9dcda4fd57e0effe1f2bb6c3f4063
2281
2737
  resolveImageAccess: ref => this.config.resolveImageAccess?.(attachments, ref),
2282
2738
  maxRequestImageBytes: profile.maxRequestImageBytes,
2283
2739
  diff --git a/packages/llm/llm/src/content.ts b/packages/llm/llm/src/content.ts
2284
- index 43c392a64129981a1dd99f6f38fcae39df5e4228..96e9fece476649b8e8dfeb13979e40d456f0f903 100644
2740
+ index 5880f82c98..3e46284c5c 100644
2285
2741
  --- a/packages/llm/llm/src/content.ts
2286
2742
  +++ b/packages/llm/llm/src/content.ts
2287
2743
  @@ -1,10 +1,71 @@
@@ -2291,7 +2747,7 @@ index 43c392a64129981a1dd99f6f38fcae39df5e4228..96e9fece476649b8e8dfeb13979e40d4
2291
2747
  +import type { ContentBlock, DocumentBlock } from './types.ts'
2292
2748
  import type { Message } from './message.ts'
2293
2749
  import type { AttachmentStore, ImageAttachmentRef, ImageMediaType, RequestImageAttachment } from '@deepseek-ai/dsh-attachment'
2294
- import { assertNever } from './never.ts'
2750
+ import { assertNever } from '@deepseek-ai/dsh-util-values'
2295
2751
 
2296
2752
  +/** Return whether typed model content contains a durable parsed document. */
2297
2753
  +export function contentHasDocument(content: readonly ContentBlock[]): boolean {
@@ -2308,7 +2764,7 @@ index 43c392a64129981a1dd99f6f38fcae39df5e4228..96e9fece476649b8e8dfeb13979e40d4
2308
2764
  + return new TextDecoder('utf-8', { fatal: true }).decode(stored.data)
2309
2765
  +}
2310
2766
  +
2311
- +/** 中文约束:只生成provider request的transient text,不修改Session中的durable reference blocks。 */
2767
+ +/** Project request-only text without changing durable reference blocks in the Session. */
2312
2768
  +async function projectDocumentsWithAttachments(
2313
2769
  + blocks: readonly ContentBlock[],
2314
2770
  + attachments: AttachmentStore,
@@ -2358,7 +2814,7 @@ index 43c392a64129981a1dd99f6f38fcae39df5e4228..96e9fece476649b8e8dfeb13979e40d4
2358
2814
  export interface ImageAttachmentAccess {
2359
2815
  /** Absolute path to immutable normalized bytes; callers must treat it as read-only. */
2360
2816
  diff --git a/packages/llm/llm/src/types.ts b/packages/llm/llm/src/types.ts
2361
- index 438fcaa67baed51a6b1c8f867e802d2a6aa46023..f82ec614c131c23b8810422017ab1e4fd6415b09 100644
2817
+ index 4303538892..7c30dac96f 100644
2362
2818
  --- a/packages/llm/llm/src/types.ts
2363
2819
  +++ b/packages/llm/llm/src/types.ts
2364
2820
  @@ -5,7 +5,7 @@