Merge remote-tracking branch 'origin/master' into web2-todo

# Conflicts:
#	packages/client/connection/src/client/fixture.ts
#	packages/client/runtime/README.i18n.yaml
#	packages/client/runtime/README.md
#	packages/client/runtime/README.zh.md
#	packages/client/runtime/src/client/index.ts
#	packages/client/runtime/src/client/sessions/session.ts
#	packages/client/ui-conversation/src/client/apply.ts
#	packages/client/ui-conversation/src/client/skeleton/ConversationRoot.tsx
#	packages/client/ui-conversation/tests/chat-stats-bash-sample.spec.tsx
#	packages/client/ui-conversation/tests/chat-toolview-slot.spec.tsx
#	packages/client/ui-conversation/tests/chat-view.spec.tsx
#	packages/client/ui-conversation/tests/gate-branch-tails.spec.tsx
#	packages/client/ui-conversation/tests/skeleton.spec.tsx
#	packages/client/ui-trajectory/tests/views.spec.tsx
This commit is contained in:
imccyu
2026-07-27 10:07:51 +08:00
887 changed files with 28254 additions and 9617 deletions

View File

@@ -0,0 +1,45 @@
/**
* commands domain zod schemas (names derived from map keys: commandListRequestSchema /
* commandListValueSchema / commandExecuteRequestSchema / commandExecuteValueSchema).
*/
import { z } from 'zod'
import type { RequestPayload, ResponseValue } from './rpc-map.ts'
import type { Wire } from './rpc.schema.ts'
import { sessionIdSchema } from './sessions.schema.ts'
import type { CommandDescriptor, CommandExecuteResult } from './commands.ts'
/** CommandDescriptor row of command.list. */
export const commandDescriptorSchema = z.object({
name: z.string().min(1),
description: z.string(),
input: z.object({ hint: z.string() }).optional(),
}) satisfies z.ZodType<Wire<CommandDescriptor>>
/** command.list request payload. */
export const commandListRequestSchema = z.object({
sessionId: sessionIdSchema,
}) satisfies z.ZodType<Wire<RequestPayload<'command.list'>>>
/** command.list response value. */
export const commandListValueSchema = z.object({
commands: z.array(commandDescriptorSchema),
}) satisfies z.ZodType<Wire<ResponseValue<'command.list'>>>
/** command.execute request payload. */
export const commandExecuteRequestSchema = z.object({
sessionId: sessionIdSchema,
line: z.string(),
}) satisfies z.ZodType<Wire<RequestPayload<'command.execute'>>>
/** Detached command outcome (result slot of command.execute's value). */
export const commandExecuteResultSchema = z.object({
kind: z.union([z.literal('success'), z.literal('error')]),
text: z.string().optional(),
}) satisfies z.ZodType<Wire<CommandExecuteResult>>
/** command.execute response value (matched=false carries no result). */
export const commandExecuteValueSchema = z.object({
matched: z.boolean(),
result: commandExecuteResultSchema.optional(),
}) satisfies z.ZodType<Wire<ResponseValue<'command.execute'>>>

View File

@@ -0,0 +1,48 @@
/**
* commands domain contract: the web catalog/dispatch face of the host command
* registry (`ctx.commands`). Both methods address one session's agent via
* `sessionId` — every served session has an Agent (Session+Agent are born
* together), so there is no agent-less surface on this wire.
*/
import type { SessionId } from '@deepseek-ai/dsh-session/types'
import type { RpcRequest, RpcResponse } from './rpc.ts'
/**
* Handler-free command view served to clients. Wire mirror of the host
* registry descriptor (which stays host-side with its cordis dependencies);
* no source field — the host descriptor has none.
*/
export interface CommandDescriptor {
/** Lowercase command name without the leading slash. */
readonly name: string
/** Human-readable summary used in discovery UI. */
readonly description: string
/** Optional free-form input hint advertised to capable clients. */
readonly input?: { readonly hint: string }
}
/** Detached command outcome rendered directly by the requesting client. */
export interface CommandExecuteResult {
readonly kind: 'success' | 'error'
readonly text?: string
}
/** Command-domain unary methods (the map keys command.* of RpcMethodMap). */
export interface CommandsApi {
/**
* Lists the addressed agent's effective command catalog (name-sorted,
* globals plus its scoped shadows).
*/
list(request: RpcRequest<{ sessionId: SessionId }>): Promise<RpcResponse<{ commands: readonly CommandDescriptor[] }>>
/**
* Parses and executes one slash-command line against the addressed agent
* without sending it to the model. matched=false when syntax or name does
* not resolve (the client falls back to its default sink). The signal rides
* beside the request, never on the wire: the fetch carrier's request signal
* cancels the running handler.
*/
execute(request: RpcRequest<{ sessionId: SessionId; line: string }>, signal: AbortSignal):
Promise<RpcResponse<{ matched: boolean; result?: CommandExecuteResult }>>
}

View File

@@ -10,7 +10,7 @@ import type { HostFrame, MuxFrame } from './events.ts'
import type { Wire } from './rpc.schema.ts'
import { rpcErrorSchema, rpcIdSchema } from './rpc.schema.ts'
import { approvalRequestIdSchema } from './approvals.schema.ts'
import { sessionEventSchema, sessionIdSchema, toolEventViewSchema } from './sessions.schema.ts'
import { contentBlockSchema, sessionEventSchema, sessionIdSchema, toolEventViewSchema } from './sessions.schema.ts'
import { workspaceViewSchema } from './workspace.schema.ts'
/** Question shape validated strictly against core dsh-user-interaction. */
@@ -35,15 +35,18 @@ export const muxFrameSchema = z.discriminatedUnion('type', [
// and must fail loud here, not reach the composer.
z.object({ type: z.literal('question/requested'), sessionId: sessionIdSchema, questions: z.array(askUserQuestionItemSchema).min(1) }),
z.object({ type: z.literal('question/resolved'), sessionId: sessionIdSchema, questionRpcId: rpcIdSchema, outcome: z.union([z.literal('answered'), z.literal('cancelled')]) }),
// content/source reuse the wide passthroughs (both are merge-extensible in core).
z.object({ type: z.literal('session/queued'), sessionId: sessionIdSchema, content: z.array(contentBlockSchema), source: z.looseObject({ kind: z.string() }), steering: z.boolean() }),
z.object({ type: z.literal('stream/error'), error: rpcErrorSchema }),
]) as unknown as z.ZodType<MuxFrame>
/** HostFrame union (payload slot of a host-stream ServerRequest). */
export const hostFrameSchema = z.discriminatedUnion('type', [
z.object({ type: z.literal('host/session-added'), sessionId: sessionIdSchema, parentSessionId: sessionIdSchema.optional(), cwd: z.string().optional() }),
z.object({ type: z.literal('host/session-added'), sessionId: sessionIdSchema, blank: z.boolean(), parentSessionId: sessionIdSchema.optional(), cwd: z.string().optional() }),
z.object({ type: z.literal('host/session-removed'), sessionId: sessionIdSchema }),
z.object({ type: z.literal('host/session-status'), sessionId: sessionIdSchema, running: z.boolean() }),
z.object({ type: z.literal('host/agent-error'), sessionId: sessionIdSchema, message: z.string() }),
z.object({ type: z.literal('host/workspace-changed'), workspace: workspaceViewSchema }),
z.object({ type: z.literal('host/commands-changed') }),
z.object({ type: z.literal('stream/error'), error: rpcErrorSchema }),
]) as unknown as z.ZodType<HostFrame>

View File

@@ -8,6 +8,7 @@
import type { AskUserQuestionItem } from '@deepseek-ai/dsh-user-interaction/types'
import type { ApprovalOutcome, ApprovalRequestId } from '@deepseek-ai/dsh-user-approval/types'
import type { ContentBlock, MessageSource } from '@deepseek-ai/dsh-llm/types'
import type { CallId } from '@deepseek-ai/dsh-llm/brand'
import type { SessionEvent, SessionId } from '@deepseek-ai/dsh-session/types'
import type { ToolCallView, ToolResultView } from '@deepseek-ai/dsh-tools/presentation'
@@ -61,20 +62,41 @@ export type MuxFrame =
| { type: 'approval/resolved'; sessionId: SessionId; approvalId: ApprovalRequestId; outcome: ApprovalOutcome }
| { type: 'question/requested'; sessionId: SessionId; questions: AskUserQuestionItem[] }
| { type: 'question/resolved'; sessionId: SessionId; questionRpcId: RpcId; outcome: 'answered' | 'cancelled' }
/**
* A message entered the addressed agent's inbox (`agent/queued` passthrough:
* a queued message is not model-visible, so there is no session event to
* ride — this transient frame is the only wire signal). On stream open the
* host replays the current queue snapshot for every attached session (same
* refresh-recovery baseline as pending questions); queue clearing on cancel
* has no dedicated frame — clients fold it from the status flip.
* source carries the prompt's rpcId when the message came over this wire
* (the client's provisional-echo reconciliation key).
*/
| { type: 'session/queued'; sessionId: SessionId; content: ContentBlock[]; source: MessageSource; steering: boolean }
| { type: 'stream/error'; error: RpcError }
/**
* Host stream frames. session-added carries the lineage anchor and the
* project cwd (the list-summary fields a client cannot wait for a refresh to
* learn); agent-error is the only outlet for live failures with no turn
* position; workspace-changed pushes the full new snapshot after every
* durable workspace mutation (create/attach/order change — the client
* upserts, while `workspace.list` provides the reconnect baseline).
* Host stream frames. session-added carries the lineage anchor, the project
* cwd, and the blank bit (the list-summary fields a client cannot wait for a
* refresh to learn); the frame fires at session/created, so blank is
* constantly true — clients flip it on the session's first
* `host/session-status(running:true)` (a blank session never runs), and a
* reconnecting client takes `session.list`'s summary.blank as authoritative.
* agent-error is the only outlet for live failures with no turn position;
* workspace-changed pushes the full new snapshot after every durable
* workspace mutation (create/attach/order change — the client upserts, while
* `workspace.list` provides the reconnect baseline).
*/
export type HostFrame =
| { type: 'host/session-added'; sessionId: SessionId; parentSessionId?: SessionId; cwd?: string }
| { type: 'host/session-added'; sessionId: SessionId; blank: boolean; parentSessionId?: SessionId; cwd?: string }
| { type: 'host/session-removed'; sessionId: SessionId }
| { type: 'host/session-status'; sessionId: SessionId; running: boolean }
| { type: 'host/agent-error'; sessionId: SessionId; message: string }
| { type: 'host/workspace-changed'; workspace: WorkspaceView }
/**
* The command registry changed (`commands/change` passthrough). Pure
* invalidation signal, no payload: clients refetch `command.list` in the
* background rather than diffing.
*/
| { type: 'host/commands-changed' }
| { type: 'stream/error'; error: RpcError }

View File

@@ -7,6 +7,8 @@
import type { SessionsApi } from './sessions.ts'
import type { HostApi } from './host.ts'
import type { WorkspaceApi } from './workspace.ts'
import type { CommandsApi } from './commands.ts'
import type { SkillsApi } from './skills.ts'
import type { EventsApi } from './events.ts'
import type { ClientResponse, RpcReceipt } from './rpc.ts'
@@ -15,6 +17,8 @@ export interface ApiProxy {
sessions: SessionsApi
host: HostApi
workspace: WorkspaceApi
commands: CommandsApi
skills: SkillsApi
events: EventsApi
/** Response entry for server-requests (client-response, echoing their rpcId); not a domain method (four-quadrant model). */
respond(message: ClientResponse): Promise<RpcReceipt>
@@ -24,6 +28,8 @@ export interface ApiProxy {
export type { HistoryEntry, SessionsApi, SessionSummary } from './sessions.ts'
export type { HostApi } from './host.ts'
export type { WorkspaceApi, WorkspaceId, WorkspaceView } from './workspace.ts'
export type { CommandsApi, CommandDescriptor, CommandExecuteResult } from './commands.ts'
export type { SkillsApi, SkillEntry } from './skills.ts'
export type { EventsApi, MuxFrame, HostFrame, ToolCallView, ToolEventView, ToolResultView } from './events.ts'
export type { ApprovalResponsePayload } from './approvals.ts'
export type { QuestionResponsePayload } from './questions.ts'

View File

@@ -7,9 +7,15 @@
import type { SessionsApi } from './sessions.ts'
import type { HostApi } from './host.ts'
import type { WorkspaceApi } from './workspace.ts'
import type { CommandsApi } from './commands.ts'
import type { SkillsApi } from './skills.ts'
import type { RpcResponse } from './rpc.ts'
/** Method name → method signature. Signatures are the single source of truth; payload/value types are always derived from here. */
/**
* Method name → method signature. Signatures are the single source of truth; payload/value
* types are always derived from here. A method may declare a trailing AbortSignal after the
* request (command.execute): the carrier passes its request signal, never a wire field.
*/
export interface RpcMethodMap {
'session.list': SessionsApi['list']
'session.create': SessionsApi['create']
@@ -21,6 +27,9 @@ export interface RpcMethodMap {
'workspace.create': WorkspaceApi['create']
'workspace.rename': WorkspaceApi['rename']
'workspace.insertSessionBefore': WorkspaceApi['insertSessionBefore']
'command.list': CommandsApi['list']
'command.execute': CommandsApi['execute']
'skill.list': SkillsApi['list']
}
/** Business request payload of method K (reaches through the RpcRequest narrow form to payload). */

View File

@@ -39,6 +39,7 @@ export const sessionSummarySchema = z.object({
sessionId: sessionIdSchema,
updatedAt: z.number(),
running: z.boolean(),
blank: z.boolean(),
parentSessionId: sessionIdSchema.optional(),
cwd: z.string().optional(),
}) satisfies z.ZodType<Wire<SessionSummary>>

View File

@@ -39,6 +39,14 @@ export interface SessionSummary {
updatedAt: number
/** Status of the attached agent; always false for cold (unattached) sessions. */
running: boolean
/**
* Derived emptiness bit: true while the session log holds zero events (no
* user message yet). Clients hide blank sessions from lists and reuse them
* for New Session on the same workspace. Always false for cold sessions —
* lazy persistence keeps a never-appended session out of the store, so a
* listed cold session necessarily has events.
*/
blank: boolean
/** fork/spawn lineage (session.header.parentSession passthrough); absent for root sessions. */
parentSessionId?: SessionId
/** Session working directory (header.cwd passthrough); absent when unrecorded. */
@@ -54,9 +62,9 @@ export interface SessionsApi {
* Creates a real session and its idle agent. At most one of `workspaceId` /
* `cwd` is accepted; an omitted project uses the Host cwd. A caller may
* preallocate `sessionId`: retries with the same id and cwd return the same
* session, while a different cwd fails with `session-conflict`.
* Workspace creation attaches the session after publication; an attach
* failure returns `workspace-attach-failed` with the published session id.
* session, while a different cwd fails with `session-conflict`. Workspace
* creation attaches the session after publication; an attach failure
* returns `workspace-attach-failed` with the published session id.
*/
create(request: RpcRequest<{ workspaceId?: WorkspaceId; cwd?: string; sessionId?: SessionId }>):
Promise<RpcResponse<{ sessionId: SessionId }>>

View File

@@ -0,0 +1,27 @@
/**
* skills domain zod schemas (names derived from map keys: skillListRequestSchema /
* skillListValueSchema).
*/
import { z } from 'zod'
import type { RequestPayload, ResponseValue } from './rpc-map.ts'
import type { Wire } from './rpc.schema.ts'
import { sessionIdSchema } from './sessions.schema.ts'
import type { SkillEntry } from './skills.ts'
/** SkillEntry row of skill.list. */
export const skillEntrySchema = z.object({
name: z.string().min(1),
description: z.string(),
whenToUse: z.string().optional(),
}) satisfies z.ZodType<Wire<SkillEntry>>
/** skill.list request payload. */
export const skillListRequestSchema = z.object({
sessionId: sessionIdSchema,
}) satisfies z.ZodType<Wire<RequestPayload<'skill.list'>>>
/** skill.list response value. */
export const skillListValueSchema = z.object({
skills: z.array(skillEntrySchema),
}) satisfies z.ZodType<Wire<ResponseValue<'skill.list'>>>

View File

@@ -0,0 +1,25 @@
/**
* skills domain contract: read-only skill catalog lookup addressed by session.
* The session's header cwd resolves to the canonical project root host-side —
* the client never submits a raw path, and skill lookup never creates or
* resumes an Agent.
*/
import type { SessionId } from '@deepseek-ai/dsh-session/types'
import type { RpcRequest, RpcResponse } from './rpc.ts'
/** Skill catalog row (wire projection of the host SkillSummary; provider/source vocabulary stays host-side). */
export interface SkillEntry {
/** Kebab-case identifier referenced as `<skill>name</skill>` in prompts. */
readonly name: string
/** Short routing description. */
readonly description: string
/** Optional extra routing guidance. */
readonly whenToUse?: string
}
/** Skill-domain unary methods (the map key skill.* of RpcMethodMap). */
export interface SkillsApi {
/** Lists model-invocable skills for the addressed session's project root. */
list(request: RpcRequest<{ sessionId: SessionId }>): Promise<RpcResponse<{ skills: readonly SkillEntry[] }>>
}