feat(gui): host goal handlers and slash command dispatch on web prompts

- web host boots CommandService + command-goal; session.prompt intercepts
  a lone text block starting with '/' and executes it as a command
  (unknown -> RPC unknown-command, usage error -> RPC command-error, so
  the composer restores the draft and shows the error strip) instead of
  sending it to the model — symmetric with the ACP adapter
- prompt response carries command result text on the wire for future UI
- api-proxy also lands the host-side goals RPC handlers
This commit is contained in:
_Kerman
2026-07-22 20:52:11 +08:00
parent 923535fa7a
commit 158b8a8ead
14 changed files with 478 additions and 4 deletions
@@ -35,6 +35,8 @@ export const rpcErrorSchema: z.ZodType<RpcError> = z.discriminatedUnion('code',
z.object({ code: z.literal('bad-request'), message: z.string(), details: z.object({ issues: z.array(z.custom<ZodIssue>()) }) }),
z.object({ code: z.literal('session-not-found'), message: z.string(), details: z.object({ sessionId: z.string() }) }),
z.object({ code: z.literal('agent-busy'), message: z.string(), details: z.object({ reason: z.string() }) }),
z.object({ code: z.literal('command-error'), message: z.string(), details: z.object({}) }),
z.object({ code: z.literal('unknown-command'), message: z.string(), details: z.object({}) }),
z.object({ code: z.literal('internal'), message: z.string(), details: z.object({}) }),
]) as unknown as z.ZodType<RpcError>
+4
View File
@@ -32,6 +32,10 @@ export interface RpcErrorDetailsMap {
'bad-request': { issues: ZodIssue[] }
'session-not-found': { sessionId: SessionId }
'agent-busy': { reason: string }
/** A known slash command reported a usage/state error; the message is the command's own text. */
'command-error': {}
/** A leading-/ prompt named no registered command; the message names the token. */
'unknown-command': {}
'internal': {}
}
@@ -94,9 +94,13 @@ export const sessionPromptRequestSchema = z.object({
content: z.array(contentBlockSchema),
}) as unknown as z.ZodType<RequestPayload<'session.prompt'>>
/** session.prompt response value. */
/** session.prompt response value (the command slot appears only when the prompt dispatched a slash command). */
export const sessionPromptValueSchema = z.object({
accepted: z.literal(true),
command: z.object({
kind: z.literal('success'),
text: z.string().optional(),
}).optional(),
}) satisfies z.ZodType<Wire<ResponseValue<'session.prompt'>>>
/** session.cancel request payload. */
+9 -2
View File
@@ -64,9 +64,16 @@ export interface SessionsApi {
history(request: RpcRequest<{ sessionId: SessionId; beforeSeq?: number; maxMessages?: number }>):
Promise<RpcResponse<{ events: HistoryEntry[]; hasMore: boolean }>>
/** Sends a message. content is core's ContentBlock[] verbatim; mode maps 1:1 — queue→send, steer→steer. */
/**
* Sends a message. content is core's ContentBlock[] verbatim; mode maps 1:1 — queue→send, steer→steer.
* A prompt whose content is exactly one text block starting with '/' is a slash command: the host
* executes it through the command registry (mode-agnostic) and it is never sent to the model. A
* successful command returns ok with the command slot (its success text, when the command produced
* one — carried for future rendering; the state change is the feedback). A usage/state error is an
* RPC error with code command-error; an unrecognized name is an RPC error with code unknown-command.
*/
prompt(request: RpcRequest<{ sessionId: SessionId; mode: 'queue' | 'steer'; content: ContentBlock[] }>):
Promise<RpcResponse<{ accepted: true }>>
Promise<RpcResponse<{ accepted: true; command?: { kind: 'success'; text?: string } }>>
/** Stops: clears both FIFOs + aborts the current step (1:1 with agent.cancel). */
cancel(request: RpcRequest<{ sessionId: SessionId }>): Promise<RpcResponse<{ accepted: true }>>