refactor(ui-agent-preset): consume the preset Remote

This commit is contained in:
imccyu
2026-08-26 11:18:28 +08:00
parent 306419cc84
commit ef1c812d93
5 changed files with 89 additions and 95 deletions
+4 -1
View File
@@ -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,
@@ -67,32 +67,28 @@ export class AgentPresetSeatController {
/**
* Read the roster and open the chip on the deployment default.
* @returns once the snapshot reflects the host.
*/
* @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 })
return
}
const { presets } = response.result.value
this.fallback = presets.find(preset => preset.isDefault)?.id ?? presets[0]?.id ?? ''
const session = this.currentSession()
this.set({
options: presetOptions(presets),
// Staged pick first, then the composition the current session
// already carries, then the deployment default. The middle term is
// what keeps a late-landing load from regressing the display after
// an applied stage was consumed — the chip mounts (and loads) only
// once the flow's session is current, so the reply can arrive after
// apply() already composed it.
current: this.staged ?? (session === undefined ? this.fallback : presetOf(session) ?? ''),
error: null,
})
} catch (error) {
this.set({ error: messageOf(error) })
const roster = await readRoster(this.remote)
if (!roster.ok) {
this.set({ error: roster.error })
return
}
const { presets } = roster.value
this.fallback = presets.find(preset => preset.isDefault)?.id ?? presets[0]?.id ?? ''
const session = this.currentSession()
this.set({
options: presetOptions(presets),
// Staged pick first, then the composition the current session
// already carries, then the deployment default. The middle term is
// what keeps a late-landing load from regressing the display after
// an applied stage was consumed — the chip mounts (and loads) only
// once the flow's session is current, so the reply can arrive after
// apply() already composed it.
current: this.staged ?? (session === undefined ? this.fallback : presetOf(session) ?? ''),
error: null,
})
}
/**
@@ -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