Files
deepseek-harness/packages/host/apiproxy/src/api/events.schema.ts
imccyu 6e94810973 feat(gui): carry the question detail field over the wire and render it
AskUserQuestionItem.detail is part of the user-interaction seam contract
but the web frame schema dropped it and the composer never rendered it
(review r3635427108). askUserQuestionItemSchema now forwards detail, the
composer renders it under the title in the description text style, and
the fixture's multi-select question carries one.
2026-07-23 20:26:48 +08:00

47 lines
3.2 KiB
TypeScript

/**
* events domain zod schemas: MuxFrame / HostFrame unions (discriminatedUnion('type')).
* A frame is the payload slot of the ServerRequest full form; the SessionEvent inside
* a session/event frame reuses sessions.schema's strict-envelope + wide-data passthrough branch.
*/
import { z } from 'zod'
import type { AskUserQuestionItem } from '@deepseek-ai/dsh-user-interaction/types'
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'
/** Question shape validated strictly against core dsh-user-interaction. */
export const askUserQuestionItemSchema = z.object({
id: z.string(),
question: z.string(),
header: z.string().optional(),
detail: z.string().optional(),
options: z.array(z.object({ label: z.string(), description: z.string().optional() })).optional(),
multiSelect: z.boolean().optional(),
}) satisfies z.ZodType<Wire<AskUserQuestionItem>>
/** MuxFrame union (payload slot of a mux-stream ServerRequest). */
export const muxFrameSchema = z.discriminatedUnion('type', [
z.object({ type: z.literal('session/event'), sessionId: sessionIdSchema, event: sessionEventSchema, view: toolEventViewSchema.optional() }),
z.object({ type: z.literal('session/subscribed'), sessionId: sessionIdSchema, lastSeq: z.number().int() }),
z.object({ type: z.literal('approval/requested'), sessionId: sessionIdSchema, approvalId: approvalRequestIdSchema, toolName: z.string(), callId: z.string().optional(), reason: z.string().optional() }),
z.object({ type: z.literal('approval/resolved'), sessionId: sessionIdSchema, approvalId: approvalRequestIdSchema, outcome: z.union([z.literal('allowed-once'), z.literal('rejected'), z.literal('cancelled'), z.literal('unavailable')]) }),
// Non-empty by wire contract: the user-interaction service rejects empty
// batches at ask() (EMPTY_QUESTIONS), so an empty frame is host breakage
// 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')]) }),
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() }),
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('stream/error'), error: rpcErrorSchema }),
]) as unknown as z.ZodType<HostFrame>