mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
refactor(ui-agent-preset): consume the preset Remote
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
/** Platform-neutral assembly of generated Host Remote contributions. */
|
||||
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import agentPresetsRemote from '@deepseek-ai/dsh-agent-presets/remote'
|
||||
import commandsRemote from '@deepseek-ai/dsh-commands/remote'
|
||||
import goalsRemote from '@deepseek-ai/dsh-goal/remote'
|
||||
import dynamicRemote from '@deepseek-ai/dsh-cordis-host-runner/remote'
|
||||
@@ -14,6 +15,7 @@ import type { ClientRemote } from '@deepseek-ai/dsh-api-gateway/client'
|
||||
|
||||
export type { ClientRemote } from '@deepseek-ai/dsh-api-gateway/client'
|
||||
export type { PluginInventorySnapshot } from '@deepseek-ai/dsh-host-plugin-inventory/types'
|
||||
export type {} from '@deepseek-ai/dsh-agent-presets/remote'
|
||||
export type {} from '@deepseek-ai/dsh-commands/remote'
|
||||
export type {} from '@deepseek-ai/dsh-file-reference/remote'
|
||||
export type {} from '@deepseek-ai/dsh-goal/remote'
|
||||
@@ -106,6 +108,7 @@ export type { SessionReferenceMentionCandidate } from '@deepseek-ai/dsh-session-
|
||||
/** Failure vocabulary exposed by the assembled Client data layer. */
|
||||
export type ClientFailure =
|
||||
| import('@deepseek-ai/dsh-client-connection/client').RpcError
|
||||
| import('@deepseek-ai/dsh-agent-presets/types').AgentPresetError
|
||||
| import('@deepseek-ai/dsh-api-session-controller/types').SessionError
|
||||
| import('@deepseek-ai/dsh-api-workspace-controller/types').WorkspaceError
|
||||
|
||||
@@ -133,7 +136,7 @@ export async function apply(ctx: Context): Promise<() => Promise<void>> {
|
||||
const disposers: Array<() => Promise<void>> = []
|
||||
try {
|
||||
for (const contribution of [
|
||||
commandsRemote, goalsRemote, dynamicRemote, fileReferencesRemote,
|
||||
agentPresetsRemote, commandsRemote, goalsRemote, dynamicRemote, fileReferencesRemote,
|
||||
pluginInventoryRemote, messageFeedbackRemote, sessionReferencesRemote,
|
||||
sessionRemote, workspaceRemote,
|
||||
]) {
|
||||
|
||||
@@ -50,7 +50,7 @@ export type { AgentPresetOption, AgentPresetSettingsState } from './settings-sto
|
||||
export { AGENT_PRESET_SETTINGS_NS, writeDefaultPreset } from './settings-store.ts'
|
||||
|
||||
/** Required services (cordis fiber inject). */
|
||||
export const inject = ['slots', 'locale', 'connection', 'remote', 'settingsScope']
|
||||
export const inject = ['slots', 'locale', 'connection', 'remote', 'remote.agentPresets', 'settingsScope']
|
||||
|
||||
/**
|
||||
* Mount the General-settings row.
|
||||
@@ -58,11 +58,11 @@ export const inject = ['slots', 'locale', 'connection', 'remote', 'settingsScope
|
||||
*/
|
||||
export function apply(ctx: ClientContext): void {
|
||||
const { api } = ctx.get('connection') as ConnectionHandle
|
||||
const controller = new AgentPresetSettingsController(api, ctx.settingsScope.describe())
|
||||
const controller = new AgentPresetSettingsController(api, ctx.remote, ctx.settingsScope.describe())
|
||||
// One roster, four surfaces. The chip is registered in a later scope, so it
|
||||
// subscribes here rather than being reached from this one.
|
||||
const rosterReaders = new Set<() => void>()
|
||||
const section = new AgentPresetSectionController(api, () => {
|
||||
const section = new AgentPresetSectionController(api, ctx.remote, () => {
|
||||
void controller.load()
|
||||
for (const read of rosterReaders) read()
|
||||
})
|
||||
@@ -104,8 +104,7 @@ export function apply(ctx: ClientContext): void {
|
||||
// The new-session chip and the header label: one controller, because the
|
||||
// staged choice belongs to the flow rather than to any one session.
|
||||
ctx.inject(['slots', 'conversation', 'sessions', 'uiWorkspace'], (scope: ClientContext) => {
|
||||
const api = (scope.get('connection') as ConnectionHandle).api
|
||||
const seat = new AgentPresetSeatController(api, () => {
|
||||
const seat = new AgentPresetSeatController(scope.remote, () => {
|
||||
const state = scope.sessions.list.getSnapshot()
|
||||
return state.current === undefined ? undefined : state.byId[state.current]
|
||||
})
|
||||
|
||||
@@ -10,11 +10,11 @@
|
||||
* deployment default again, matching the workspace picker beside it.
|
||||
*/
|
||||
|
||||
import type { IApiClient } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import type { ClientRemote } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import type { SessionSummary } from '@deepseek-ai/dsh-api-session-controller/client'
|
||||
import { createSnapshotStore, type SnapshotStore } from '@deepseek-ai/dsh-client-store'
|
||||
import type {} from '@deepseek-ai/dsh-agent-presets/types'
|
||||
import { messageOf, presetOptions } from './settings-store.ts'
|
||||
import { messageOf, presetOptions, readRoster } from './settings-store.ts'
|
||||
import type { AgentPresetOption } from './settings-store.ts'
|
||||
|
||||
/** Hero-chip snapshot. */
|
||||
@@ -53,7 +53,7 @@ export class AgentPresetSeatController {
|
||||
private staged: string | undefined
|
||||
|
||||
constructor(
|
||||
private readonly api: Pick<IApiClient, 'agentPresets'>,
|
||||
private readonly remote: Pick<ClientRemote, 'agentPresets'>,
|
||||
/** The session the hero is about to hand over to, when there is one. */
|
||||
private readonly currentSession: () => Pick<
|
||||
SessionSummary,
|
||||
@@ -70,13 +70,12 @@ export class AgentPresetSeatController {
|
||||
* @returns once the snapshot reflects the host.
|
||||
*/
|
||||
async load(): Promise<void> {
|
||||
try {
|
||||
const response = await this.api.agentPresets.list({})
|
||||
if (!response.result.ok) {
|
||||
this.set({ error: response.result.error.message })
|
||||
const roster = await readRoster(this.remote)
|
||||
if (!roster.ok) {
|
||||
this.set({ error: roster.error })
|
||||
return
|
||||
}
|
||||
const { presets } = response.result.value
|
||||
const { presets } = roster.value
|
||||
this.fallback = presets.find(preset => preset.isDefault)?.id ?? presets[0]?.id ?? ''
|
||||
const session = this.currentSession()
|
||||
this.set({
|
||||
@@ -90,9 +89,6 @@ export class AgentPresetSeatController {
|
||||
current: this.staged ?? (session === undefined ? this.fallback : presetOf(session) ?? ''),
|
||||
error: null,
|
||||
})
|
||||
} catch (error) {
|
||||
this.set({ error: messageOf(error) })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -153,18 +149,18 @@ export class AgentPresetSeatController {
|
||||
}
|
||||
this.set({ busy: true, error: null })
|
||||
try {
|
||||
const response = await this.api.agentPresets.select({ sessionId: session.id, agentPreset: staged })
|
||||
const result = await this.remote.agentPresets.select(session.id, staged)
|
||||
this.staged = undefined
|
||||
if (!response.result.ok) {
|
||||
if (!result.ok) {
|
||||
this.set({
|
||||
busy: false,
|
||||
error: response.result.error.message,
|
||||
error: result.error.message,
|
||||
current: presetOf(session) ?? '',
|
||||
})
|
||||
return
|
||||
}
|
||||
// Consumed: the next new session opens on the deployment default again.
|
||||
this.set({ busy: false, current: response.result.value.agentPreset })
|
||||
this.set({ busy: false, current: result.value })
|
||||
} catch (error) {
|
||||
this.staged = undefined
|
||||
this.set({
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* more than the row it targeted.
|
||||
*/
|
||||
|
||||
import type { IApiClient } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import type { ClientRemote, IApiClient } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import { createSnapshotStore, type SnapshotStore } from '@deepseek-ai/dsh-client-store'
|
||||
import { beginRosterRead, messageOf, writeDefaultPreset } from './settings-store.ts'
|
||||
|
||||
@@ -133,7 +133,8 @@ export class AgentPresetSectionController {
|
||||
readonly store: SnapshotStore<AgentPresetSectionState> = createSnapshotStore(INITIAL)
|
||||
|
||||
constructor(
|
||||
private readonly api: Pick<IApiClient, 'agentPresets' | 'settings'>,
|
||||
private readonly api: Pick<IApiClient, 'agentPresets' | 'settings' | 'host'>,
|
||||
private readonly remote: Pick<ClientRemote, 'agentPresets'>,
|
||||
/**
|
||||
* Called after this page changes the roster DIRECTORY, so the other
|
||||
* surfaces reading the same roster re-read it. A settings field moving is
|
||||
@@ -162,9 +163,18 @@ export class AgentPresetSectionController {
|
||||
* @returns once the snapshot reflects the host.
|
||||
*/
|
||||
async load(): Promise<void> {
|
||||
const roster = await beginRosterRead(this.api, this.store)
|
||||
// Whether a preset's directory can be opened is the Host's opener
|
||||
// capability rather than a roster property, so the page joins the two.
|
||||
// Issued together: one round trip decides the page, and a load that waited
|
||||
// for them in turn would hold the section in `loading` twice as long,
|
||||
// where a concurrent reload silently returns instead of refreshing.
|
||||
const opener = this.api.host.describe({})
|
||||
const roster = await beginRosterRead(this.remote, this.store)
|
||||
// A refused describe leaves the reveal-the-path path, which needs no opener.
|
||||
const described = await opener.catch(() => undefined)
|
||||
if (roster === undefined) return
|
||||
const { presets, authorable, hasDocument } = roster
|
||||
const { presets, authorable } = roster
|
||||
const hasDocument = described?.result.ok === true && described.result.value.canOpenPath
|
||||
if (presets.length === 0) {
|
||||
// Nothing to manage leaves nothing to keep a dialog open over.
|
||||
this.set({ status: 'unavailable', rows: [], authorable, hasDocument, copy: null, view: null })
|
||||
@@ -193,12 +203,12 @@ export class AgentPresetSectionController {
|
||||
async view(id: string): Promise<void> {
|
||||
this.set({ error: null })
|
||||
try {
|
||||
const response = await this.api.agentPresets.read({ agentPreset: id })
|
||||
if (!response.result.ok) {
|
||||
this.set({ error: response.result.error.message })
|
||||
const result = await this.remote.agentPresets.read(id)
|
||||
if (!result.ok) {
|
||||
this.set({ error: result.error.message })
|
||||
return
|
||||
}
|
||||
const { name, content } = response.result.value
|
||||
const { name, content } = result.value
|
||||
this.set({ view: { id, title: name ?? id, content } })
|
||||
} catch (error) {
|
||||
this.set({ error: messageOf(error) })
|
||||
@@ -256,13 +266,14 @@ export class AgentPresetSectionController {
|
||||
this.patchCopy({ saving: true, error: null })
|
||||
try {
|
||||
const name = draft.name.trim()
|
||||
const response = await this.api.agentPresets.copy({
|
||||
from: draft.from,
|
||||
agentPreset: draft.id,
|
||||
...name === '' ? {} : { name },
|
||||
})
|
||||
if (!response.result.ok) {
|
||||
this.patchCopy({ saving: false, error: response.result.error.message })
|
||||
// Every declared parameter is passed even when optional: the Remote face
|
||||
// checks arity against the declaration and rejects a short call. An
|
||||
// empty display name goes as `undefined` — absent rather than empty, so
|
||||
// the host falls back to the id instead of labelling the row with ''.
|
||||
const result = await this.remote.agentPresets.copy(
|
||||
draft.from, draft.id, name === '' ? undefined : name)
|
||||
if (!result.ok) {
|
||||
this.patchCopy({ saving: false, error: result.error.message })
|
||||
return
|
||||
}
|
||||
this.set({ copy: null })
|
||||
@@ -318,9 +329,9 @@ export class AgentPresetSectionController {
|
||||
if (pendingDelete === null || deleting) return
|
||||
this.set({ deleting: true, error: null })
|
||||
try {
|
||||
const response = await this.api.agentPresets.remove({ agentPreset: pendingDelete })
|
||||
if (!response.result.ok) {
|
||||
this.set({ deleting: false, pendingDelete: null, error: response.result.error.message })
|
||||
const result = await this.remote.agentPresets.deletePreset(pendingDelete)
|
||||
if (!result.ok) {
|
||||
this.set({ deleting: false, pendingDelete: null, error: result.error.message })
|
||||
return
|
||||
}
|
||||
this.set({ deleting: false, pendingDelete: null })
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
/**
|
||||
* Agent-preset default-settings controller.
|
||||
*
|
||||
* Options and the current default both come from one `agentPreset.list` call:
|
||||
* Options and the current default both come from one `agentPresets.list` call:
|
||||
* the roster already reports which id a session with no explicit choice gets,
|
||||
* so the row needs no schema introspection. Writes target the settings
|
||||
* namespace's `default` field, which is what the host resolves at creation.
|
||||
*/
|
||||
|
||||
import type { IApiClient } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import type { ClientRemote, IApiClient } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import { createSnapshotStore, type SnapshotStore } from '@deepseek-ai/dsh-client-store'
|
||||
import type { AgentPresetRoster } from '@deepseek-ai/dsh-agent-presets/types'
|
||||
import type { SettingsDescribeFace } from '@deepseek-ai/dsh-client-ui-settings/client'
|
||||
|
||||
/** The agent-preset settings namespace on the host wire. */
|
||||
@@ -63,33 +64,12 @@ export interface AgentPresetOption {
|
||||
}
|
||||
|
||||
/** One roster entry exactly as the host reports it. */
|
||||
export interface RosterPreset {
|
||||
/** Preset id and directory name. */
|
||||
id: string
|
||||
/** Whether the preset ships with the deployment or was authored locally. */
|
||||
trust: 'system' | 'user'
|
||||
/** Whether a session that names no preset gets this one. */
|
||||
isDefault: boolean
|
||||
/** Display name the preset published, absent when it published none. */
|
||||
name?: string
|
||||
/** One sentence on what the preset is for. */
|
||||
description?: string
|
||||
/** Why the preset cannot compose a session, absent when it can. */
|
||||
broken?: string
|
||||
}
|
||||
|
||||
/** The roster the host answered with. */
|
||||
export interface RosterValue {
|
||||
/** Every preset the deployment composes, in the order the host lists them. */
|
||||
presets: readonly RosterPreset[]
|
||||
/** Whether this browser may author presets at all. */
|
||||
authorable: boolean
|
||||
/** Whether the host can open a preset directory on a native desktop. */
|
||||
hasDocument: boolean
|
||||
}
|
||||
export type RosterPreset = AgentPresetRoster['presets'][number]
|
||||
|
||||
/** The roster, or the message to show in its place. */
|
||||
export type RosterRead = { ok: true; value: RosterValue } | { ok: false; error: string }
|
||||
export type RosterRead = { ok: true; value: AgentPresetRoster } | { ok: false; error: string }
|
||||
|
||||
const EMPTY_ROSTER: AgentPresetRoster = { presets: [], authorable: false }
|
||||
|
||||
/**
|
||||
* Read the roster, folding both refusal shapes into one message.
|
||||
@@ -98,15 +78,18 @@ export type RosterRead = { ok: true; value: RosterValue } | { ok: false; error:
|
||||
* `ok: false` envelope — and every surface treats them identically. Folding
|
||||
* them here keeps each store's `load` about what it does with a roster rather
|
||||
* than about how the call can fail.
|
||||
* @param api - the agent-preset wire face.
|
||||
* @param remote - the agent-preset Remote namespace.
|
||||
* @returns the roster, or the message to show in its place.
|
||||
*/
|
||||
export async function readRoster(api: Pick<IApiClient, 'agentPresets'>): Promise<RosterRead> {
|
||||
export async function readRoster(remote: Pick<ClientRemote, 'agentPresets'>): Promise<RosterRead> {
|
||||
try {
|
||||
const response = await api.agentPresets.list({})
|
||||
return response.result.ok
|
||||
? { ok: true, value: response.result.value }
|
||||
: { ok: false, error: response.result.error.message }
|
||||
const result = await remote.agentPresets.list()
|
||||
if (result.ok) return { ok: true, value: result.value }
|
||||
// Agent presets are optional: without that service every session uses the
|
||||
// Host composition, so callers receive the same empty roster as a mounted
|
||||
// service with no configured roots.
|
||||
if (result.error.code === 'invocation-unavailable') return { ok: true, value: EMPTY_ROSTER }
|
||||
return { ok: false, error: result.error.message }
|
||||
} catch (error) {
|
||||
return { ok: false, error: messageOf(error) }
|
||||
}
|
||||
@@ -119,18 +102,18 @@ export async function readRoster(api: Pick<IApiClient, 'agentPresets'>): Promise
|
||||
* A surface that gets `undefined` returns without touching its snapshot
|
||||
* further — either another read owns it, or this one already wrote the
|
||||
* failure. What differs between surfaces starts after this.
|
||||
* @param api - the agent-preset wire face.
|
||||
* @param remote - the agent-preset Remote namespace.
|
||||
* @param store - the surface's own snapshot store.
|
||||
* @returns the roster, or undefined when the caller should return.
|
||||
*/
|
||||
export async function beginRosterRead<S extends { status: string; error: string | null }>(
|
||||
api: Pick<IApiClient, 'agentPresets'>,
|
||||
remote: Pick<ClientRemote, 'agentPresets'>,
|
||||
store: SnapshotStore<S>,
|
||||
): Promise<RosterValue | undefined> {
|
||||
): Promise<AgentPresetRoster | undefined> {
|
||||
const before = store.getSnapshot()
|
||||
if (before.status === 'loading') return undefined
|
||||
store.set({ ...before, status: 'loading', error: null })
|
||||
const roster = await readRoster(api)
|
||||
const roster = await readRoster(remote)
|
||||
if (roster.ok) return roster.value
|
||||
store.set({ ...store.getSnapshot(), status: 'error', error: roster.error })
|
||||
return undefined
|
||||
@@ -193,11 +176,13 @@ export class AgentPresetSettingsController {
|
||||
readonly store: SnapshotStore<AgentPresetSettingsState> = createSnapshotStore(INITIAL)
|
||||
|
||||
/**
|
||||
* @param api - the agent-preset and settings wire faces (roster and default write).
|
||||
* @param api - the settings wire face (the default write).
|
||||
* @param remote - the agent-preset Remote namespace (the roster read).
|
||||
* @param describeFace - the shared mirror's describe face (writability source).
|
||||
*/
|
||||
constructor(
|
||||
private readonly api: IApiClient,
|
||||
private readonly api: Pick<IApiClient, 'settings'>,
|
||||
private readonly remote: Pick<ClientRemote, 'agentPresets'>,
|
||||
private readonly describeFace: SettingsDescribeFace,
|
||||
) {}
|
||||
|
||||
@@ -212,7 +197,7 @@ export class AgentPresetSettingsController {
|
||||
* @returns once the snapshot reflects the host.
|
||||
*/
|
||||
async load(): Promise<void> {
|
||||
const roster = await beginRosterRead(this.api, this.store)
|
||||
const roster = await beginRosterRead(this.remote, this.store)
|
||||
if (roster === undefined) return
|
||||
const { presets } = roster
|
||||
const [first] = presets
|
||||
|
||||
Reference in New Issue
Block a user