Merge remote-tracking branch 'origin/master' into worktree/web-plugin-config

# 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/client/ui-conversation/README.i18n.yaml
#	packages/client/ui-conversation/README.md
#	packages/client/ui-conversation/README.zh.md
#	packages/client/ui-conversation/package.json
#	pnpm-lock.yaml
This commit is contained in:
Yichen Jiang
2026-08-10 15:50:49 +08:00
352 changed files with 7220 additions and 781 deletions

View File

@@ -7,9 +7,9 @@
* must stub); runtime-internal entry points (history staging, wire-frame
* dispatch) stay on the class, invisible out here.
*/
import type { ContentBlock } from '@deepseek-ai/dsh-llm/types'
import type { AttachmentIdType, ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
import type {
MessageId, QueueAction, RpcResult, SessionId,
MessageId, PromptContentPart, QueueAction, RpcResult, SessionId,
} from '@deepseek-ai/dsh-client-connection/client'
import type { ConversationSnapshot } from '../sessions/conversation.ts'
import type { ObservableSnapshot } from './store.ts'
@@ -33,11 +33,19 @@ export interface ISession {
readonly projections: ProjectionsFace
/**
* Send a prompt into the session.
* @param content - model-facing content blocks.
* @param content - text plus browser-owned temporary image uploads.
* @param mode - 'queue' appends a turn; 'steer' interrupts the running one.
* @returns acceptance, or the business error (also mirrored into snapshot.promptError).
*/
prompt(content: ContentBlock[], mode: 'queue' | 'steer'): Promise<RpcResult<{ accepted: true }>>
prompt(content: PromptContentPart[], mode: 'queue' | 'steer'): Promise<RpcResult<{ accepted: true }>>
/**
* Resolve one durable image referenced by this session.
* @param attachmentId - opaque id found in the folded session log.
* @returns the authenticated reference and decoded bytes.
*/
readAttachment(
attachmentId: AttachmentIdType,
): Promise<RpcResult<{ attachment: ImageAttachmentRef; data: Uint8Array }>>
/**
* Apply one edit, remove, or strict steer action to a still-pending queue occurrence.
* @param itemId - agent-owned inbox occurrence identity.

View File

@@ -183,6 +183,18 @@ declare module 'cordis' {
* @mode emit
*/
'models/changed'(): void
/**
* One session's agent preset changed (host/session-preset-changed
* passthrough), so everything its composition decides — the command
* catalog, the skill catalog — is stale for that session and no other.
* Every connected client observes it, not only the one that issued the
* switch. Subscribers refetch their own session-keyed caches; the frame
* carries no catalog.
* @mode emit
* @param sessionId - the session whose composition changed.
* @param agentPreset - the preset it now runs.
*/
'session/preset-changed'(sessionId: SessionId, agentPreset: string): void
/**
* A connection generation was (re-)established. Wire-derived caches must
* treat their state as stale and repull (commands directory; the queue
@@ -246,6 +258,9 @@ export function apply(ctx: Context): void {
// and model surfaces) subscribe on ctx.
const frame = envelope.payload
if (frame.type === 'host/commands-changed') ctx.emit('commands/changed')
else if (frame.type === 'host/session-preset-changed') {
ctx.emit('session/preset-changed', frame.sessionId, frame.agentPreset)
}
else if (frame.type === 'host/settings-changed') ctx.emit('settings/changed', frame.ns)
else if (frame.type === 'host/credentials-changed') ctx.emit('credentials/changed', frame.ref)
else if (frame.type === 'host/models-changed') ctx.emit('models/changed')

View File

@@ -8,6 +8,7 @@
import type { CommandId } from '@deepseek-ai/dsh-commands/brand'
import type { MessageId } from '@deepseek-ai/dsh-llm/brand'
import type { ContentBlock } from '@deepseek-ai/dsh-llm/types'
import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
import type { LlmRetryEventData } from '@deepseek-ai/dsh-llm-retry/types'
import type { TodoItem } from '@deepseek-ai/dsh-session/types'
import type {
@@ -43,6 +44,7 @@ export interface AssistantProvenanceView {
export type AssistantBlock =
| { kind: 'text'; text: string }
| { kind: 'reasoning'; text: string }
| { kind: 'image'; attachment: ImageAttachmentRef }
| { kind: 'tool-call'; callId: string; name: string; argsRaw: string }
| { kind: 'other'; block: unknown }
@@ -64,6 +66,7 @@ export function toAssistantBlock(block: ContentBlock): AssistantBlock {
switch (block.type) {
case 'text': return { kind: 'text', text: block.text }
case 'reasoning': return { kind: 'reasoning', text: block.text }
case 'image': return { kind: 'image', attachment: block.attachment }
case 'tool-call': return { kind: 'tool-call', callId: String(block.id), name: block.name, argsRaw: block.arguments }
default: return { kind: 'other', block }
}

View File

@@ -780,6 +780,14 @@ export class SessionManager {
}
return
}
case 'host/session-preset-changed': {
// Every connected client observes the switch here; only the tab that
// issued it also gets the RPC echo. The merge keeps the row's own
// updatedAt and lowers `blank` only, so re-applying the switching
// tab's own frame is a no-op.
this.noteAgentPreset(frame.sessionId, frame.agentPreset)
return
}
case 'host/session-removed': {
const summary = this.summaries.find(candidate => candidate.sessionId === frame.sessionId)
const durableSubagent = summary?.origin === 'subagent' || this.addresses.has(frame.sessionId)
@@ -1005,7 +1013,7 @@ export class SessionManager {
const prev = this.entryCache.get(entry.sessionId)
if (
prev !== undefined && prev.updatedAt === entry.updatedAt && prev.running === entry.running
&& prev.blank === entry.blank
&& prev.blank === entry.blank && prev.agentPreset === entry.agentPreset
&& prev.parentSessionId === entry.parentSessionId && prev.cwd === entry.cwd
&& prev.origin === entry.origin && prev.title === entry.title && prev.depth === entry.depth
&& prev.pendingInteraction === entry.pendingInteraction

View File

@@ -1,10 +1,10 @@
// Sessions remain resident after creation so they continue consuming mux frames off-screen.
import type { Context } from 'cordis'
import type { ContentBlock } from '@deepseek-ai/dsh-llm/types'
import type { AttachmentIdType, ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
import type { SessionEvent } from '@deepseek-ai/dsh-session/types'
import type {
HistoryEntry, IApiClient, MessageId, MuxFrame, QueueAction, RpcError,
HistoryEntry, IApiClient, MessageId, MuxFrame, PromptContentPart, QueueAction, RpcError,
RpcId, RpcResponse, RpcResult, SessionId, SubagentAddress, ToolEventView,
} from '@deepseek-ai/dsh-client-connection/client'
// Value import from the inline-safe wire layer (not the connection plugin):
@@ -178,11 +178,11 @@ export class Session implements SessionFace {
/**
* Send (queue/steer passed through 1:1); failures land in the snapshot's promptError.
* @param content - core content blocks verbatim.
* @param content - text plus browser-owned temporary image uploads.
* @param mode - queue appends after the current turn; steer interrupts it.
* @returns the prompt result (also mirrored into promptError on failure).
*/
async prompt(content: ContentBlock[], mode: 'queue' | 'steer'): Promise<RpcResult<{ accepted: true }>> {
async prompt(content: PromptContentPart[], mode: 'queue' | 'steer'): Promise<RpcResult<{ accepted: true }>> {
this.promptError = null
this.lastAgentError = null
// Synchronous, before the first await: the blank → engaging edge must be
@@ -205,8 +205,24 @@ export class Session implements SessionFace {
},
}
} else {
const routed = (await this.api.subagents.prompt({ ...this.address, content })).result
result = routed.ok ? { ok: true, value: { accepted: true } } : routed
if (content.some(part => part.type === 'image')) {
result = {
ok: false,
error: {
code: 'attachment-error',
message: 'Image input is unavailable for subagent continuations.',
details: { reason: 'SUBAGENT_IMAGE_UNSUPPORTED' },
},
}
} else {
const routed = (await this.api.subagents.prompt({
...this.address,
content: content.flatMap(part => part.type === 'text'
? [{ type: 'text' as const, text: part.text }]
: []),
})).result
result = routed.ok ? { ok: true, value: { accepted: true } } : routed
}
}
} catch (error) {
result = transportError(error)
@@ -232,6 +248,28 @@ export class Session implements SessionFace {
return result
}
/**
* Resolve one image referenced by this session into browser-consumable bytes.
* @param attachmentId - opaque id found in the folded session log.
* @returns the authenticated reference and decoded bytes.
*/
async readAttachment(
attachmentId: AttachmentIdType,
): Promise<RpcResult<{ attachment: ImageAttachmentRef; data: Uint8Array }>> {
try {
const result = (await this.api.sessions.attachment({
sessionId: this.sessionId,
attachmentId,
})).result
if (!result.ok) return result
const binary = atob(result.value.data)
const data = Uint8Array.from(binary, char => char.charCodeAt(0))
return { ok: true, value: { attachment: result.value.attachment, data } }
} catch (error) {
return transportError(error)
}
}
/** Apply one operation to a still-pending queue occurrence. */
async updateQueue(itemId: MessageId, action: QueueAction): Promise<RpcResult<{ accepted: true }>> {
try {