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:
112
packages/host/apiproxy/src/api/goals.schema.ts
Normal file
112
packages/host/apiproxy/src/api/goals.schema.ts
Normal 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'>>>
|
||||
88
packages/host/apiproxy/src/api/goals.ts
Normal file
88
packages/host/apiproxy/src/api/goals.ts
Normal 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 }>>
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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). */
|
||||
|
||||
Reference in New Issue
Block a user