refactor(ui-agent-preset): one home for the roster read every surface starts from

The row and the management section opened `load` the same way — refuse a
concurrent read, mark the store loading, read, fold both refusal shapes into
the store's error. What differs between them starts after that.
This commit is contained in:
Yichen Jiang
2026-08-07 15:52:02 +08:00
parent 0f27a505c4
commit 82ae1a8a37
2 changed files with 91 additions and 30 deletions

View File

@@ -10,7 +10,7 @@
import type { IApiClient } from '@deepseek-ai/dsh-client-connection/client'
import { createSnapshotStore, type SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
import { messageOf, writeDefaultPreset } from './settings-store.ts'
import { beginRosterRead, messageOf, writeDefaultPreset } from './settings-store.ts'
/** Ids a preset directory may be named, mirroring the host's own rule. */
const PRESET_ID = /^[a-z0-9][a-z0-9-]*$/
@@ -137,23 +137,16 @@ export class AgentPresetSectionController {
* @returns once the snapshot reflects the host.
*/
async load(): Promise<void> {
if (this.store.getSnapshot().status === 'loading') return
this.set({ status: 'loading', error: null })
try {
const response = await this.api.agentPresets.list({})
if (!response.result.ok) {
this.set({ status: 'error', error: response.result.error.message })
return
}
const { presets, authorable } = response.result.value
if (presets.length === 0) {
this.set({ status: 'unavailable', rows: [], authorable, draft: null })
return
}
this.set({ status: 'ready', error: null, authorable, rows: presets.map(preset => ({ ...preset })) })
} catch (error) {
this.set({ status: 'error', error: messageOf(error) })
const roster = await beginRosterRead(
this.api, () => this.store.getSnapshot().status, patch => { this.set(patch) },
)
if (roster === undefined) return
const { presets, authorable } = roster
if (presets.length === 0) {
this.set({ status: 'unavailable', rows: [], authorable, draft: null })
return
}
this.set({ status: 'ready', error: null, authorable, rows: presets.map(preset => ({ ...preset })) })
}
/**

View File

@@ -61,6 +61,77 @@ export interface AgentPresetOption {
description?: string
}
/** 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
}
/** 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
}
/** The roster, or the message to show in its place. */
export type RosterRead = { ok: true, value: RosterValue } | { ok: false, error: string }
/**
* Read the roster, folding both refusal shapes into one message.
*
* The wire refuses in two ways — the transport rejects, or it answers an
* `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.
* @returns the roster, or the message to show in its place.
*/
export async function readRoster(api: Pick<IApiClient, '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 }
} catch (error) {
return { ok: false, error: messageOf(error) }
}
}
/**
* The opening move every roster-backed surface makes: refuse a read that is
* already in flight, mark the store loading, then read.
*
* 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 status - reads the store's current status.
* @param set - patches the store's status and error.
* @returns the roster, or undefined when the caller should return.
*/
export async function beginRosterRead(
api: Pick<IApiClient, 'agentPresets'>,
status: () => string,
set: (patch: { status?: 'loading' | 'error', error?: string | null }) => void,
): Promise<RosterValue | undefined> {
if (status() === 'loading') return undefined
set({ status: 'loading', error: null })
const roster = await readRoster(api)
if (roster.ok) return roster.value
set({ status: 'error', error: roster.error })
return undefined
}
/**
* The roster entries as every surface renders them.
*
@@ -125,20 +196,17 @@ export class AgentPresetSettingsController {
* @returns once the snapshot reflects the host.
*/
async load(): Promise<void> {
if (this.store.getSnapshot().status === 'loading') return
this.set({ status: 'loading', error: null })
const roster = await beginRosterRead(
this.api, () => this.store.getSnapshot().status, patch => { this.set(patch) },
)
if (roster === undefined) return
const { presets } = roster
const [first] = presets
if (first === undefined) {
this.set({ status: 'unavailable', options: [], currentValue: '' })
return
}
try {
const response = await this.api.agentPresets.list({})
if (!response.result.ok) {
this.set({ status: 'error', error: response.result.error.message })
return
}
const presets = response.result.value.presets
const [first] = presets
if (first === undefined) {
this.set({ status: 'unavailable', options: [], currentValue: '' })
return
}
// The roster says what may be chosen; `settings.describe` says whether
// this browser may write the choice down. A non-loopback browser reaches
// neither method, so a refused describe leaves the row read-only rather