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

@@ -8,6 +8,7 @@
export type {
ApiProxy, SessionsApi, SessionSummary, HostApi, EventsApi, MuxFrame, HostFrame,
ApprovalResponsePayload, QuestionResponsePayload, HistoryEntry, ToolEventView,
GoalsApi, GoalView, GoalRef, GoalPhase, GoalBlockReason,
} from '@deepseek-ai/dsh-host-apiproxy/api'
export type { ToolCallView, ToolResultView } from '@deepseek-ai/dsh-tools/presentation'
export type {

View File

@@ -423,6 +423,15 @@ export function createFixtureApi(): ApiProxy {
host: {
describe: request => ok(request, { version: '0.0.0-fixture', cwd: '/tmp/fixture', attachedSessions: 1 }),
},
goals: {
get: request => err(request, { code: 'internal', message: 'fixture: goals not implemented', details: {} }),
create: request => err(request, { code: 'internal', message: 'fixture: goals not implemented', details: {} }),
edit: request => err(request, { code: 'internal', message: 'fixture: goals not implemented', details: {} }),
pause: request => err(request, { code: 'internal', message: 'fixture: goals not implemented', details: {} }),
resume: request => err(request, { code: 'internal', message: 'fixture: goals not implemented', details: {} }),
complete: request => err(request, { code: 'internal', message: 'fixture: goals not implemented', details: {} }),
clear: request => err(request, { code: 'internal', message: 'fixture: goals not implemented', details: {} }),
},
events: {
async *mux(_request, signal) {
const conn = new FxInbox<MuxFrame>()
@@ -514,6 +523,13 @@ export class FixtureApiClient extends AbstractApiClient {
case 'session.prompt': return this.api.sessions.prompt(request)
case 'session.cancel': return this.api.sessions.cancel(request)
case 'host.describe': return this.api.host.describe(request)
case 'goal.get': return this.api.goals.get(request)
case 'goal.create': return this.api.goals.create(request)
case 'goal.edit': return this.api.goals.edit(request)
case 'goal.pause': return this.api.goals.pause(request)
case 'goal.resume': return this.api.goals.resume(request)
case 'goal.complete': return this.api.goals.complete(request)
case 'goal.clear': return this.api.goals.clear(request)
}
}

View File

@@ -20,6 +20,7 @@ export type {
RpcRequest, RpcResponse, RpcResult, RpcError, RpcErrorCode,
ClientRequest, ServerResponse, ServerRequest, ClientResponse, RpcMessage, RpcReceipt,
IApiClient, SessionId, SessionEvent, ContentBlock, StreamChunk,
GoalsApi, GoalView, GoalRef, GoalPhase, GoalBlockReason,
} from './api.ts'
export { RpcId, AbstractApiClient, resultOf, transportError } from './api.ts'

View File

@@ -71,6 +71,16 @@ export class FakeApiClient implements IApiClient {
describe: payload => this.record('host.describe', payload, this.onDescribe(payload)),
}
readonly goals: IApiClient['goals'] = {
get: payload => this.record('goal.get', payload, Promise.resolve(ok({ goal: null }))),
create: payload => this.record('goal.create', payload, Promise.resolve(ok({ goal: null as unknown as never }))),
edit: payload => this.record('goal.edit', payload, Promise.resolve(ok({ goal: null as unknown as never }))),
pause: payload => this.record('goal.pause', payload, Promise.resolve(ok({ goal: null as unknown as never }))),
resume: payload => this.record('goal.resume', payload, Promise.resolve(ok({ goal: null as unknown as never }))),
complete: payload => this.record('goal.complete', payload, Promise.resolve(ok({ goal: null as unknown as never }))),
clear: payload => this.record('goal.clear', payload, Promise.resolve(ok({ cleared: true as const }))),
}
/** When true, streams never fire onOpen (misbehaving-carrier material for the handshake timeout guard). */
suppressStreamOpen = false