feat(gui): goal wire domain, client goal state, and docked goal bar

- apiproxy goals RPC domain (get/create/edit/pause/resume/complete/clear)
  with CAS refs, zod schemas, and fetch client/handler wiring
- client runtime session goal state: live goal/change meta triggers a
  coalesced refetch; mutations fold transport errors into RpcResult
- web GoalBar: docked strip above the composer (sparkle, phase label,
  truncated objective, inline edit, clear; resume when paused); creation
  stays on the /goal command
- GoalBarActions in the ui-conversation contract layer; IconSparkle16
  moves to ui-primitives icons
This commit is contained in:
_Kerman
2026-07-22 20:51:44 +08:00
parent 0681ac47de
commit 923535fa7a
40 changed files with 1358 additions and 28 deletions

View File

@@ -0,0 +1,112 @@
/**
* goals domain zod schemas.
*/
import { z } from 'zod'
import type { Wire } from './rpc.schema.ts'
import type { GoalRef, GoalView, RequestPayload, ResponseValue } from './index.ts'
/** GoalRef schema. */
export const goalRefSchema = z.object({
id: z.string(),
revision: z.number().int().positive(),
}) as unknown as z.ZodType<Wire<GoalRef>>
/** Goal block reason schema. */
export const goalBlockReasonSchema = z.object({
code: z.string(),
message: z.string(),
})
/** GoalView schema. */
export const goalViewSchema = z.object({
id: z.string(),
revision: z.number().int().positive(),
objective: z.string(),
phase: z.union([z.literal('active'), z.literal('paused'), z.literal('blocked'), z.literal('complete')]),
blockedReason: goalBlockReasonSchema.optional(),
maxGoalRounds: z.number().int().positive(),
roundsStarted: z.number().int().nonnegative(),
createdAt: z.number(),
updatedAt: z.number(),
activation: z.union([z.literal('armed'), z.literal('disarmed')]),
}) as unknown as z.ZodType<Wire<GoalView>>
/** goal.get request payload. */
export const goalGetRequestSchema = z.object({
sessionId: z.string(),
}) as unknown as z.ZodType<Wire<RequestPayload<'goal.get'>>>
/** goal.get response value. */
export const goalGetValueSchema = z.object({
goal: goalViewSchema.nullable(),
}) as unknown as z.ZodType<Wire<ResponseValue<'goal.get'>>>
/** goal.create request payload. */
export const goalCreateRequestSchema = z.object({
sessionId: z.string(),
objective: z.string().min(1),
maxGoalRounds: z.number().int().positive().optional(),
}) as unknown as z.ZodType<Wire<RequestPayload<'goal.create'>>>
/** goal.create response value. */
export const goalCreateValueSchema = z.object({
goal: goalViewSchema,
}) as unknown as z.ZodType<Wire<ResponseValue<'goal.create'>>>
/** goal.edit request payload. */
export const goalEditRequestSchema = z.object({
sessionId: z.string(),
ref: goalRefSchema,
objective: z.string().min(1).optional(),
maxGoalRounds: z.number().int().positive().optional(),
}) as unknown as z.ZodType<Wire<RequestPayload<'goal.edit'>>>
/** goal.edit response value. */
export const goalEditValueSchema = z.object({
goal: goalViewSchema,
}) as unknown as z.ZodType<Wire<ResponseValue<'goal.edit'>>>
/** goal.pause request payload. */
export const goalPauseRequestSchema = z.object({
sessionId: z.string(),
ref: goalRefSchema,
}) as unknown as z.ZodType<Wire<RequestPayload<'goal.pause'>>>
/** goal.pause response value. */
export const goalPauseValueSchema = z.object({
goal: goalViewSchema,
}) as unknown as z.ZodType<Wire<ResponseValue<'goal.pause'>>>
/** goal.resume request payload. */
export const goalResumeRequestSchema = z.object({
sessionId: z.string(),
ref: goalRefSchema,
}) as unknown as z.ZodType<Wire<RequestPayload<'goal.resume'>>>
/** goal.resume response value. */
export const goalResumeValueSchema = z.object({
goal: goalViewSchema,
}) as unknown as z.ZodType<Wire<ResponseValue<'goal.resume'>>>
/** goal.complete request payload. */
export const goalCompleteRequestSchema = z.object({
sessionId: z.string(),
ref: goalRefSchema,
}) as unknown as z.ZodType<Wire<RequestPayload<'goal.complete'>>>
/** goal.complete response value. */
export const goalCompleteValueSchema = z.object({
goal: goalViewSchema,
}) as unknown as z.ZodType<Wire<ResponseValue<'goal.complete'>>>
/** goal.clear request payload. */
export const goalClearRequestSchema = z.object({
sessionId: z.string(),
ref: goalRefSchema,
}) as unknown as z.ZodType<Wire<RequestPayload<'goal.clear'>>>
/** goal.clear response value. */
export const goalClearValueSchema = z.object({
cleared: z.literal(true),
}) as unknown as z.ZodType<Wire<ResponseValue<'goal.clear'>>>

View File

@@ -0,0 +1,88 @@
/**
* goals domain contract. Method signatures are the source of truth:
* unary methods take the RpcRequest<P> narrow form and the impl echoes rpcId.
*/
import type { Branded } from '@deepseek-ai/dsh-brand'
import type { RpcRequest, RpcResponse } from './rpc.ts'
/** Identifies one goal across its durable revisions. */
export type GoalId = Branded<'GoalId'>
/** Compare-and-set identity for one exact goal revision. */
export interface GoalRef {
readonly id: GoalId
readonly revision: number
}
/** Durable continuation phase. */
export type GoalPhase =
| 'active'
| 'paused'
| 'blocked'
| 'complete'
/** Machine-routable and human-readable explanation for a blocked goal. */
export interface GoalBlockReason {
readonly code: string
readonly message: string
}
/** Whether this live process may automatically continue an active goal. */
export type GoalActivation = 'armed' | 'disarmed'
/** Current goal projection, including values derived from the session log. */
export interface GoalView {
readonly id: GoalId
readonly revision: number
readonly objective: string
readonly phase: GoalPhase
readonly blockedReason?: GoalBlockReason
readonly maxGoalRounds: number
readonly roundsStarted: number
readonly createdAt: number
readonly updatedAt: number
readonly activation: GoalActivation
}
/** Input whose omitted round cap is resolved by the service configuration. */
export interface CreateGoalRequest {
readonly objective: string
readonly maxGoalRounds?: number
}
/** Fields changed by an edit; at least one must be present. */
export interface EditGoalRequest {
readonly objective?: string
readonly maxGoalRounds?: number
}
/** Goal-domain unary methods. */
export interface GoalsApi {
/** Read the current goal for one session. Returns null when no goal is current. */
get(request: RpcRequest<{ sessionId: string }>): Promise<RpcResponse<{ goal: GoalView | null }>>
/** Create and arm a goal. */
create(request: RpcRequest<{ sessionId: string; objective: string; maxGoalRounds?: number }>):
Promise<RpcResponse<{ goal: GoalView }>>
/** Edit objective and/or round cap without changing phase. */
edit(request: RpcRequest<{ sessionId: string; ref: GoalRef; objective?: string; maxGoalRounds?: number }>):
Promise<RpcResponse<{ goal: GoalView }>>
/** Pause an active goal and disarm automatic continuation. */
pause(request: RpcRequest<{ sessionId: string; ref: GoalRef }>):
Promise<RpcResponse<{ goal: GoalView }>>
/** Resume and arm a stopped goal. */
resume(request: RpcRequest<{ sessionId: string; ref: GoalRef }>):
Promise<RpcResponse<{ goal: GoalView }>>
/** Mark a current non-complete goal complete and disarm it. */
complete(request: RpcRequest<{ sessionId: string; ref: GoalRef }>):
Promise<RpcResponse<{ goal: GoalView }>>
/** Clear the current goal while retaining a durable tombstone and history. */
clear(request: RpcRequest<{ sessionId: string; ref: GoalRef }>):
Promise<RpcResponse<{ cleared: true }>>
}

View File

@@ -7,6 +7,7 @@
import type { SessionsApi } from './sessions.ts'
import type { HostApi } from './host.ts'
import type { EventsApi } from './events.ts'
import type { GoalsApi } from './goals.ts'
import type { ClientResponse, RpcReceipt } from './rpc.ts'
/** Root interface of the unified API surface. New client-request domain = one new file pair + one field here + one map row. */
@@ -14,6 +15,7 @@ export interface ApiProxy {
sessions: SessionsApi
host: HostApi
events: EventsApi
goals: GoalsApi
/** Response entry for server-requests (client-response, echoing their rpcId); not a domain method (four-quadrant model). */
respond(message: ClientResponse): Promise<RpcReceipt>
}
@@ -22,6 +24,7 @@ export interface ApiProxy {
export type { HistoryEntry, SessionsApi, SessionSummary } from './sessions.ts'
export type { HostApi } from './host.ts'
export type { EventsApi, MuxFrame, HostFrame, ToolCallView, ToolEventView, ToolResultView } from './events.ts'
export type { GoalsApi, GoalView, GoalRef, GoalPhase, GoalBlockReason, CreateGoalRequest, EditGoalRequest } from './goals.ts'
export type { ApprovalResponsePayload } from './approvals.ts'
export type { QuestionResponsePayload } from './questions.ts'

View File

@@ -6,6 +6,7 @@
import type { SessionsApi } from './sessions.ts'
import type { HostApi } from './host.ts'
import type { GoalsApi } from './goals.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. */
@@ -16,6 +17,13 @@ export interface RpcMethodMap {
'session.prompt': SessionsApi['prompt']
'session.cancel': SessionsApi['cancel']
'host.describe': HostApi['describe']
'goal.get': GoalsApi['get']
'goal.create': GoalsApi['create']
'goal.edit': GoalsApi['edit']
'goal.pause': GoalsApi['pause']
'goal.resume': GoalsApi['resume']
'goal.complete': GoalsApi['complete']
'goal.clear': GoalsApi['clear']
}
/** Business request payload of method K (reaches through the RpcRequest narrow form to payload). */