Merge remote-tracking branch 'origin/master' into worktree/process-plugin-streaming-72f9b3

# Conflicts:
#	docs/event-producer-consumer.i18n.yaml
#	docs/event-producer-consumer.md
#	docs/event-producer-consumer.zh.md
#	docs/module-graph.i18n.yaml
#	docs/module-graph.md
#	docs/module-graph.zh.md
#	packages/api/remotes/package.json
#	packages/api/remotes/src/client/index.ts
#	packages/api/session-controller/README.i18n.yaml
#	packages/api/session-controller/README.md
#	packages/api/session-controller/README.zh.md
#	packages/api/session-controller/tsconfig.host.json
#	packages/client/ui-approval/src/client/contract/slots.ts
#	packages/client/ui-chat/src/client/chat/MessageItem.tsx
#	packages/client/ui-chat/src/client/conversation-nodes/assistant.ts
#	packages/client/ui-chat/src/client/conversation-nodes/partial.ts
#	packages/client/ui-chat/src/client/conversation-nodes/tool.ts
#	packages/client/ui-chat/src/client/model/tool-call-tree.ts
This commit is contained in:
Yichen Jiang committed 2026-09-04 17:28:12 +08:00
commit d3b1f2d1bc
461 files changed
+12176 -1942

No files matched your search

@@ -7,22 +7,25 @@
* must stub); implementation-internal entry points (history staging, wire-frame
* dispatch) stay on the class, invisible out here.
*/
import type { AttachmentIdType, ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
import type { AttachmentIdType, FileAttachmentRef, ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
import type { MessageId } from '@deepseek-ai/dsh-llm/brand'
import type { SessionId, SessionSeq } from '@deepseek-ai/dsh-session/types'
import type { RemoteResult } from '@deepseek-ai/dsh-typert-protocol'
import type { ObservableSnapshot } from '@deepseek-ai/dsh-client-store'
import type { PromptContentPart, QueueAction, SessionRequestId } from '../../types.ts'
import type { PendingSubmissionImage, SessionSnapshot } from './snapshot.ts'
import type { PendingSubmissionAttachment, SessionSnapshot } from './snapshot.ts'
/**
* Why a local submission echo left the snapshot: `observed` when its durable
* `user/message` event or host queue occurrence arrived (with the admitted
* image references in prompt order), `failed` when the prompt was rejected,
* attachment references in prompt order), `failed` when the prompt was rejected,
* threw, or was aborted before acceptance.
*/
export type PendingSubmissionRetirement =
| { readonly reason: 'observed'; readonly attachments: readonly ImageAttachmentRef[] }
| {
readonly reason: 'observed'
readonly attachments: readonly (ImageAttachmentRef | FileAttachmentRef)[]
}
| { readonly reason: 'failed' }
/** Input registering one local submission echo ahead of its prompt call. */
@@ -31,8 +34,8 @@ export interface BeginSubmissionInput {
readonly mode: 'queue' | 'steer'
/** Prompt text exactly as the upcoming prompt will send it. */
readonly text: string
/** Ordered image previews matching the upcoming prompt's image parts. */
readonly images: readonly PendingSubmissionImage[]
/** Ordered image previews and durable file metadata matching the upcoming prompt attachments. */
readonly attachments: readonly PendingSubmissionAttachment[]
/** Settlement callback fired exactly once when the echo retires. */
readonly onRetire?: (retirement: PendingSubmissionRetirement) => void
}
@@ -1,5 +1,6 @@
/** Session-owned observable state excluding Conversation target data. */
import type { ContentBlock } from '@deepseek-ai/dsh-llm/types'
import type { FileAttachmentRef } from '@deepseek-ai/dsh-attachment'
import type { MessageId } from '@deepseek-ai/dsh-llm/brand'
import type { SessionId } from '@deepseek-ai/dsh-session/types'
import type { SubagentAddress } from '@deepseek-ai/dsh-subagent/client'
@@ -30,6 +31,23 @@ export interface PendingSubmissionImage {
readonly height?: number
}
/** Image branch of a local submission echo attachment. */
export interface PendingSubmissionImageAttachment {
readonly type: 'image'
readonly value: PendingSubmissionImage
}
/** File branch of a local submission echo attachment. */
export interface PendingSubmissionFileAttachment {
readonly type: 'file'
readonly value: FileAttachmentRef
}
/** One attachment displayed by a local submission echo, in prompt order. */
export type PendingSubmissionAttachment =
| PendingSubmissionImageAttachment
| PendingSubmissionFileAttachment
/** Client surface selected when a local submission begins. */
export type PendingSubmissionPlacement = 'transcript' | 'queued' | 'steering'
@@ -48,8 +66,8 @@ export interface PendingSubmission {
readonly time: number
/** Prompt text exactly as it will be sent (one text block). */
readonly text: string
/** Ordered image previews matching the prompt's image parts. */
readonly images: readonly PendingSubmissionImage[]
/** Ordered image previews and durable file metadata matching the prompt attachments. */
readonly attachments: readonly PendingSubmissionAttachment[]
}
/** History-open lifecycle of a Session event window. */
@@ -2,6 +2,8 @@
import type { Context } from '@deepseek-ai/cordis'
import type {} from '@deepseek-ai/dsh-agent/types'
import type {} from '@deepseek-ai/dsh-client-connection/client'
import type {} from '@deepseek-ai/dsh-client-file-upload/client'
import { createSessionControlStream } from './transport.ts'
import { ClientSessions } from './sessions/service.ts'
import type { SessionRemotes } from './sessions/remotes.ts'
@@ -62,7 +64,10 @@ export type {
export type {
OpenState,
PendingSubmission,
PendingSubmissionAttachment,
PendingSubmissionFileAttachment,
PendingSubmissionImage,
PendingSubmissionImageAttachment,
PendingSubmissionPlacement,
PromptError,
QueuedMessage,
@@ -78,6 +83,8 @@ declare module '@deepseek-ai/cordis' {
/** Required Remote and Context projection services. */
export const inject = [
'connection',
'fileUpload',
'typert',
'remote',
'remote.commands',
@@ -5,11 +5,11 @@ import type { QueuedMessage } from '../contract/snapshot.ts'
const QUEUE_PREVIEW_CHARS = 200
// Image blocks are excluded: queue presentation renders them as thumbnails
// from `content`, so the text preview covers only what has no visual form.
// Attachment blocks are excluded: queue presentation renders them from
// `content`, so the text preview covers only what has no visual form.
function previewOf(content: readonly ContentBlock[]): string {
const flat = content
.filter(block => block.type !== 'image')
.filter(block => block.type !== 'image' && block.type !== 'file')
.map(block => (block.type === 'text' ? block.text : `[${block.type}]`))
.join(' ').replace(/\s+/g, ' ').trim()
const chars = Array.from(flat)
@@ -5,8 +5,8 @@
* @module @deepseek-ai/dsh-api-session-controller/client/sessions/remotes
*/
import type { EncodedImageAttachment } from '@deepseek-ai/dsh-attachment/types'
import type { ClientRemote } from '@deepseek-ai/dsh-api-gateway/client'
import type { CommandSubmitAttachment } from '@deepseek-ai/dsh-commands/types'
import type { SessionId } from '@deepseek-ai/dsh-session/types'
import type {
SubagentCatalog, SubagentInterruptReceipt, SubagentPromptReceipt, SubagentPromptRequest,
@@ -19,7 +19,7 @@ export interface SessionCommandsRemote {
execute(
agentId: SessionId,
line: string,
images: readonly EncodedImageAttachment[],
attachments: readonly CommandSubmitAttachment[],
signal?: AbortSignal,
): Promise<RemoteResult<object | undefined>>
}
@@ -2,7 +2,7 @@
import type { Context } from '@deepseek-ai/cordis'
import { randomUUID } from '@deepseek-ai/dsh-util-crypto'
import type { AttachmentIdType, ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
import type { AttachmentIdType, FileAttachmentRef, ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
import type { SubagentAddress } from '@deepseek-ai/dsh-subagent/client'
import type { MessageId } from '@deepseek-ai/dsh-llm/brand'
import { SessionLogOffset, SessionSeq, type SessionId } from '@deepseek-ai/dsh-session/types'
@@ -30,6 +30,7 @@ import type {
} from '../contract/events.ts'
import { Notifier } from './notifier.ts'
import { isRemoteFailure } from '@deepseek-ai/dsh-api-gateway/client'
import { RemoteError } from '@deepseek-ai/dsh-typert-protocol'
import type { RemoteFailure, RemoteResult } from '@deepseek-ai/dsh-typert-protocol'
import type { SessionRemotes } from './remotes.ts'
import { ProjectionValueStore } from './projection-store.ts'
@@ -210,7 +211,7 @@ export class Session implements SessionFace {
: 'transcript',
time: Date.now(),
text: input.text,
images: input.images,
attachments: input.attachments,
}]
this.submissionSettlements.set(requestId, { onRetire: input.onRetire, retiring: false })
// The blank → engaging edge flips here, ahead of prompt(): the composer
@@ -222,7 +223,7 @@ export class Session implements SessionFace {
/**
* Send (queue/steer passed through 1:1); failures land in the snapshot's promptError.
* @param content - text plus browser-owned temporary image uploads.
* @param content - text, browser-owned temporary image uploads, and staged-file receipts.
* @param mode - queue appends after the current turn; steer interrupts it.
* @param signal - optional caller cancellation for the complete admission round-trip.
* @param requestId - identity from {@link beginSubmission}; a failed identified prompt retires its echo.
@@ -252,13 +253,25 @@ export class Session implements SessionFace {
content,
clientTimeZone,
}, signal)
} else if (content.some(part => part.type === 'file')) {
result = {
ok: false,
error: new RemoteError(
'subagent/attachment-invalid',
'subagent continuation does not accept files',
{ reason: 'SUBAGENT_FILE_UNSUPPORTED' },
),
}
} else {
// The preceding branch rejects file parts before the narrower subagent
// wire type is used; this array is not filtered or reordered.
const routedContent = content as Exclude<PromptContentPart, { readonly type: 'file' }>[]
const routed = await this.remote.subagents.prompt({
requestId: randomUUID() as SessionRequestId,
parentSessionId: this.address.parentSessionId,
childSessionId: this.address.childSessionId,
mode: 'continuable',
content,
content: routedContent,
clientTimeZone: resolvedClientTimeZone(),
}, signal)
result = routed.ok ? { ok: true, value: { accepted: true } } : routed
@@ -717,7 +730,7 @@ export class Session implements SessionFace {
const data = event.data as { readonly source?: unknown; readonly content?: unknown } | undefined
const source = data?.source as { readonly kind?: unknown; readonly rpcId?: unknown } | undefined
if (source?.kind !== 'user' || typeof source.rpcId !== 'string') return
this.scheduleObservedRetirement(source.rpcId as SessionRequestId, imageRefsIn(data?.content))
this.scheduleObservedRetirement(source.rpcId as SessionRequestId, attachmentRefsIn(data?.content))
}
/** Retire echoes whose prompts landed in the host inbox instead of the log (running-turn submissions). */
@@ -725,7 +738,7 @@ export class Session implements SessionFace {
if (this.submissionSettlements.size === 0) return
for (const item of items) {
if (item.rpcId !== undefined) {
this.scheduleObservedRetirement(item.rpcId, imageRefsIn(item.message.content))
this.scheduleObservedRetirement(item.rpcId, attachmentRefsIn(item.message.content))
}
}
}
@@ -738,7 +751,7 @@ export class Session implements SessionFace {
*/
private scheduleObservedRetirement(
requestId: SessionRequestId,
attachments: readonly ImageAttachmentRef[],
attachments: readonly (ImageAttachmentRef | FileAttachmentRef)[],
): void {
const settlement = this.submissionSettlements.get(requestId)
if (settlement === undefined || settlement.retiring) return
@@ -816,15 +829,16 @@ function scheduleFrame(fn: () => void): void {
else setTimeout(fn, 0)
}
/** Image attachment references in one structurally-read content block list, in block order. */
function imageRefsIn(content: unknown): readonly ImageAttachmentRef[] {
/** Attachment references in one structurally-read content block list, in block order. */
function attachmentRefsIn(content: unknown): readonly (ImageAttachmentRef | FileAttachmentRef)[] {
if (!Array.isArray(content)) return []
const refs: ImageAttachmentRef[] = []
const refs: Array<ImageAttachmentRef | FileAttachmentRef> = []
for (const block of content) {
if (typeof block !== 'object' || block === null) continue
const candidate = block as { readonly type?: unknown; readonly attachment?: unknown }
if (candidate.type === 'image' && typeof candidate.attachment === 'object' && candidate.attachment !== null) {
refs.push(candidate.attachment as ImageAttachmentRef)
if ((candidate.type === 'image' || candidate.type === 'file')
&& typeof candidate.attachment === 'object' && candidate.attachment !== null) {
refs.push(candidate.attachment as ImageAttachmentRef | FileAttachmentRef)
}
}
return refs