From 82ae1a8a379b038e43039b942c0df5e5a6cef884 Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Fri, 7 Aug 2026 15:52:02 +0800 Subject: [PATCH] refactor(ui-agent-preset): one home for the roster read every surface starts from MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../src/client/section-store.ts | 27 ++---- .../src/client/settings-store.ts | 94 ++++++++++++++++--- 2 files changed, 91 insertions(+), 30 deletions(-) diff --git a/packages/client/ui-agent-preset/src/client/section-store.ts b/packages/client/ui-agent-preset/src/client/section-store.ts index 15adb17e60..5f2662844c 100644 --- a/packages/client/ui-agent-preset/src/client/section-store.ts +++ b/packages/client/ui-agent-preset/src/client/section-store.ts @@ -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 { - 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 })) }) } /** diff --git a/packages/client/ui-agent-preset/src/client/settings-store.ts b/packages/client/ui-agent-preset/src/client/settings-store.ts index d14c8b89b3..bd173a254c 100644 --- a/packages/client/ui-agent-preset/src/client/settings-store.ts +++ b/packages/client/ui-agent-preset/src/client/settings-store.ts @@ -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): Promise { + 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, + status: () => string, + set: (patch: { status?: 'loading' | 'error', error?: string | null }) => void, +): Promise { + 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 { - 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