feat(web): choose the default agent preset from General settings

One settings row picking which preset new sessions are composed from.

It is deliberately a new-session preference, not a live switch: a session's
preset is fixed at creation and the host refuses to adopt an existing session
under a different one, so the row says "applies to sessions you start from now
on" rather than implying it can retune a running agent.

Options and the current value come from one `agentPreset.list` call — the
roster already reports which id an unspecified session gets, so the row needs
no settings-schema introspection, unlike the permission row it is modelled on.
The write targets only the namespace's `default` field.

The menu marks `user` rows: a locally authored preset is exactly as privileged
as the plugins it names, and presenting it identically to a shipped one would
hide that.

An empty roster reads as `unavailable` and renders nothing, because composing
no presets is a valid deployment rather than a failure — distinct from a
roster call that failed, which surfaces its message.
This commit is contained in:
Yichen Jiang
2026-08-07 00:38:11 +08:00
parent e6fe32b3c3
commit 6758da87ae
24 changed files with 760 additions and 0 deletions
+1
View File
@@ -19,6 +19,7 @@
"@cordisjs/plugin-timer": "workspace:*",
"@deepseek-ai/dsh-app-boot": "workspace:^",
"@deepseek-ai/dsh-base": "workspace:^",
"@deepseek-ai/dsh-client-ui-agent-preset": "workspace:^",
"@deepseek-ai/dsh-headless": "workspace:^",
"@deepseek-ai/dsh-mcp-client": "workspace:^",
"@deepseek-ai/dsh-paths": "workspace:^",
+1
View File
@@ -2482,6 +2482,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co
- `@deepseek-ai/dsh-client-locale` ([`packages/client/locale/src/index.ts`](../packages/client/locale/src/index.ts))
- `@deepseek-ai/dsh-client-modules` — requires `httpServer` · `loader` ([`packages/client/modules/src/index.ts`](../packages/client/modules/src/index.ts))
- `@deepseek-ai/dsh-client-runtime` ([`packages/client/runtime/src/index.ts`](../packages/client/runtime/src/index.ts))
- `@deepseek-ai/dsh-client-ui-agent-preset` ([`packages/client/ui-agent-preset/src/index.ts`](../packages/client/ui-agent-preset/src/index.ts))
- `@deepseek-ai/dsh-client-ui-command` ([`packages/client/ui-command/src/index.ts`](../packages/client/ui-command/src/index.ts))
- `@deepseek-ai/dsh-client-ui-conversation` ([`packages/client/ui-conversation/src/index.ts`](../packages/client/ui-conversation/src/index.ts))
- `@deepseek-ai/dsh-client-ui-goal` ([`packages/client/ui-goal/src/index.ts`](../packages/client/ui-goal/src/index.ts))
+5
View File
@@ -180,6 +180,11 @@
- id: ui-permission
name: '@deepseek-ai/dsh-client-ui-permission'
# The agent-preset row in General settings: the default preset for
# sessions created later. Absent a roster it renders nothing.
- id: ui-agent-preset
name: '@deepseek-ai/dsh-client-ui-agent-preset'
# Plan control: the composer plan seat over the plan projection + /plan channel.
- id: ui-plan
name: '@deepseek-ai/dsh-client-ui-plan'
@@ -167,6 +167,10 @@ export class FakeApiClient implements IApiClient {
execute: (payload: unknown) => this.record('command.execute', payload, this.onCommandExecute(payload)),
}
readonly agentPresets: IApiClient['agentPresets'] = {
list: (payload: unknown) => this.record('agentPreset.list', payload, Promise.resolve(ok({ presets: [] }))),
}
readonly skills: IApiClient['skills'] = {
list: (payload: unknown) => this.record('skill.list', payload, this.onSkillList(payload)),
}
@@ -202,6 +202,10 @@ export class FakeApiClient implements IApiClient {
execute: (payload: unknown) => this.record('command.execute', payload, this.onCommandExecute(payload)),
}
readonly agentPresets: IApiClient['agentPresets'] = {
list: (payload: unknown) => this.record('agentPreset.list', payload, Promise.resolve(ok({ presets: [] }))),
}
readonly skills: IApiClient['skills'] = {
list: (payload: unknown) => this.record('skill.list', payload, this.onSkillList(payload)),
}
@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/client/ui-agent-preset/README.md
README.md: c22d5a40659bf6300f133ff902fbc3bf8bc75276
README.zh.md: 6b7e43adcdfe7d6de781511edb5d91a4c45cf11e
+35
View File
@@ -0,0 +1,35 @@
# dsh-client-ui-agent-preset
English | [中文](README.zh.md)
The agent-preset surface: one General-settings row choosing which [preset](../../preset/agent-presets/README.md) new sessions are composed from.
## Why it is a new-session preference
A session's preset is fixed when the session is created — the host refuses to adopt an existing session under a different one, because that session's history was produced under the first preset's tools. So this row cannot be a live switch, and it says so: changing it applies to sessions started afterwards while running sessions keep the composition they began with.
## What it reads and writes
Options and the current default both come from one `agentPreset.list` call. The roster already reports which id a session with no explicit choice gets, so the row needs no settings-schema introspection; the write targets the `agent-presets` settings namespace's `default` field, which is what the host resolves at creation.
A locally authored preset is exactly as privileged as the plugins it names, so the list marks `user` rows rather than presenting every preset as shipped and vetted.
The row re-reads on `settings/changed` for its own namespace and on `connection/reset`: the roster is a live directory and the default is a settings field, so an external edit or a reconnect can both move it.
## When the row is absent
A deployment that composes no presets answers with an empty roster, and the row renders nothing — every session then shares the host composition, and there is nothing to choose between.
## Model Experience
Indirectly, through the preset a later session is composed from; [`dsh-agent-presets`](../../preset/agent-presets/README.md) owns what that composition puts in front of the model.
#### KV Cache effect
No direct invalidation. Changing the default never touches a running session's prefix; a session created afterwards establishes its own prefix from its own composition.
## Known Limitations and Deferred Work
- **No per-session choice at creation** — this row sets the default only. The wire already carries `agentPreset` on `session.create`, so a session-start surface can offer the choice; that surface does not exist yet.
- **Presets are listed by id** — a preset carries no display metadata, so the menu shows directory names.
- **No authoring** — creating, editing, or deleting a preset is a filesystem act; this surface only chooses among what the roster supplies.
@@ -0,0 +1,35 @@
# dsh-client-ui-agent-preset
[English](README.md) | 中文
agent preset 表层:General 设置中的一行,用于选择新建会话据以组装的 [preset](../../preset/agent-presets/README.md)。
## 为什么它是"新建会话"的偏好设置
会话的 preset 在创建时即固定——宿主拒绝以不同 preset 接管已存在的会话,因为该会话的历史是在最初那份 preset 的工具下产生的。因此本行不可能是实时切换,它也如实说明了这一点:更改只对此后开启的会话生效,而运行中的会话保持它们开始时的组装。
## 它读什么、写什么
选项与当前默认值都来自同一次 `agentPreset.list` 调用。名单本身已经报告了"未显式选择的会话会得到哪个 id",因此本行无需对 settings schema 做内省;写入目标是 `agent-presets` settings 命名空间的 `default` 字段,也正是宿主在创建时解析的那个字段。
本地创作的 preset 的权限恰好等于它所引用的插件,因此列表会标注 `user` 行,而不是把每个 preset 都呈现为随附且已审核的。
本行在自身命名空间的 `settings/changed` 以及 `connection/reset` 时重新读取:名单是一个活动目录,默认值是一项设置,外部编辑与重新连接都可能改变它。
## 何时不显示本行
未组装任何 preset 的部署返回空名单,本行不渲染任何内容——此时每个会话共用宿主组装,也就无从选择。
## Model Experience
Indirectly, through the preset a later session is composed from; [`dsh-agent-presets`](../../preset/agent-presets/README.md) owns what that composition puts in front of the model.
#### KV Cache effect
没有直接的失效影响。更改默认值绝不触及运行中会话的前缀;此后创建的会话依据它自己的组装建立自己的前缀。
## Known Limitations and Deferred Work
- **创建时无法逐会话选择** —— 本行只设置默认值。wire 上 `session.create` 已经携带 `agentPreset`,因此会话开启表层可以提供该选择;该表层尚不存在。
- **preset 按 id 列出** —— preset 不携带展示用元数据,因此菜单显示的是目录名。
- **不提供创作能力** —— 创建、编辑或删除 preset 是文件系统行为;本表层只在名单提供的范围内做选择。
@@ -0,0 +1,67 @@
{
"name": "@deepseek-ai/dsh-client-ui-agent-preset",
"description": "Agent-preset surface: the default preset for later sessions, in General settings",
"version": "0.0.1",
"private": true,
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
"exports": {
".": {
"types": "./lib/types/index.d.ts",
"default": "./lib/index.js"
},
"./invariant": {
"types": "./lib/types/invariant.d.ts",
"default": "./lib/invariant.js"
},
"./client": {
"types": "./lib/types/client/index.d.ts",
"default": "./lib/client.js"
},
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"dshClient": {
"inject": [
"@deepseek-ai/dsh-client-connection",
"@deepseek-ai/dsh-client-locale",
"@deepseek-ai/dsh-client-runtime"
],
"platform": "web"
},
"scripts": {
"bundle": "tsdown",
"watch": "tsdown --watch"
},
"license": "BSD-3-Clause",
"peerDependencies": {
"@deepseek-ai/dsh-client-connection": "^0.0.1",
"@deepseek-ai/dsh-client-locale": "^0.0.1",
"@deepseek-ai/dsh-client-runtime": "^0.0.1",
"@deepseek-ai/dsh-client-ui-primitives": "^0.0.1",
"@deepseek-ai/dsh-client-ui-slots": "^0.0.1",
"@deepseek-ai/dsh-invariants": "^0.0.1",
"cordis": "^4.0.0-rc.7",
"react": "^18.2.0"
},
"devDependencies": {
"@deepseek-ai/dsh-client-connection": "workspace:^",
"@deepseek-ai/dsh-client-locale": "workspace:^",
"@deepseek-ai/dsh-client-runtime": "workspace:^",
"@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@types/react": "~18.3.1",
"cordis": "^4.0.0-rc.7",
"react": "^18.2.0"
},
"files": [
"lib/index.js",
"lib/invariant.js",
"lib/client.js",
"lib/types/**/*.d.ts",
"lib/types/**/*.d.ts.map",
"src"
]
}
@@ -0,0 +1,60 @@
/* Agent-preset row: title/description plus the preset selector pill. */
.row {
display: flex;
align-items: center;
gap: 8px;
padding: 16px 0;
border-bottom: 1px solid var(--dsw-alias-border-l2);
}
.rowText {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 4px;
padding-right: 48px;
}
.title {
font-size: 14px;
font-weight: 400;
line-height: 22px;
color: var(--dsw-alias-label-primary);
}
.desc {
font-size: 12px;
font-weight: 400;
line-height: 18px;
color: var(--dsw-alias-label-tertiary);
}
.selector {
display: inline-flex;
align-items: center;
gap: 12px;
height: 36px;
padding: 0 14px;
border: none;
border-radius: 18px;
background: var(--dsw-alias-bg-module-platform);
font: inherit;
font-size: 14px;
line-height: 22px;
color: var(--dsw-alias-label-primary);
cursor: pointer;
}
.selector:hover:not(:disabled) {
background: var(--dsw-alias-interactive-bg-hover);
}
.selector:disabled {
cursor: default;
}
.chevron {
flex: none;
}
@@ -0,0 +1,104 @@
/**
* Agent-preset preference row: the preset new sessions are composed from.
* A running session keeps the composition it began with, so this row never
* disturbs work in progress.
*/
import { useEffect, useState } from 'react'
import type { SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
import { IconChevronDownOutline14, Menu } from '@deepseek-ai/dsh-client-ui-primitives'
import type { AgentPresetSettingsState } from './settings-store.ts'
import type { AgentPresetSettingsKey } from './locales.ts'
import css from './AgentPresetRow.module.css'
/** Registration-side business face for the host-backed preference. */
export interface AgentPresetRowInjected {
hooks: {
/** Agent-preset settings snapshot bound by the renderer as useAgentPreset. */
agentPreset: SnapshotStore<AgentPresetSettingsState>
}
/** Load the roster when the row first renders. */
load: () => Promise<void>
/** Persist one preset as the default for later sessions. */
select: (id: string) => Promise<void>
}
/** Full component props. */
export type AgentPresetRowProps =
PropsRuntime<'settings.general.item'>
& PropsLocale<'settings.agentPreset'>
& InjectFace<AgentPresetRowInjected>
/**
* Render the new-session agent-preset selector.
* @param props - composed slot props.
* @returns the row, or null when the deployment composes no presets.
*/
export function AgentPresetRow({ load, select, useAgentPreset, t }: AgentPresetRowProps) {
const state = useAgentPreset(snapshot => snapshot)
const [open, setOpen] = useState(false)
useEffect(() => {
void load()
}, [load])
useEffect(() => {
if (state.writable && state.status !== 'unavailable') return
setOpen(false)
}, [state.status, state.writable])
// A deployment that composes no presets has nothing to choose between, and
// every session shares the host composition — the row simply does not exist.
if (state.status === 'unavailable') return null
const busy = state.status === 'loading' || state.status === 'saving'
const label = state.currentValue === '' ? t('loading') : state.currentValue
const description: string = state.error ?? t('description')
return (
<div className={css.row}>
<div className={css.rowText}>
<div className={css.title}>{t('title')}</div>
<div className={css.desc} role={state.error === null ? undefined : 'alert'}>{description}</div>
</div>
<Menu
open={open}
onClose={() => { setOpen(false) }}
// A locally authored preset is exactly as privileged as the plugins it
// names, so the list says which rows are local rather than presenting
// every preset as shipped and vetted.
items={state.options.map(option => ({
id: option.id,
label: option.trust === 'user' ? `${option.id} · ${t('userTrust')}` : option.id,
}))}
selectedId={state.currentValue}
onSelect={(id) => {
setOpen(false)
void select(id)
}}
align="end"
portal
anchor={(
<button
type="button"
className={css.selector}
aria-haspopup="menu"
aria-expanded={open}
disabled={busy || !state.writable || state.options.length === 0}
onClick={() => { setOpen(value => !value) }}
>
{label}
<IconChevronDownOutline14 className={css.chevron} />
</button>
)}
/>
</div>
)
}
declare module '@deepseek-ai/dsh-client-ui-slots' {
interface LocaleNamespaceMap {
/** Agent-preset row copy. */
'settings.agentPreset': AgentPresetSettingsKey
}
}
@@ -0,0 +1,64 @@
/**
* Agent-preset surface plugin, browser half — one General-settings row that
* writes the default preset for sessions created later.
*
* A running session keeps the composition it began with (the host refuses to
* adopt an existing session under a different preset), so this row is a
* new-session preference rather than a live switch. Per-session choice at
* creation time belongs to the session-start surface, which reads the same
* roster.
*/
import type { ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client'
// Type-only: pulls the locale plugin's Context merge (ctx.locale).
import type {} from '@deepseek-ai/dsh-client-locale/client'
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
import { AgentPresetRow } from './AgentPresetRow.tsx'
import type { AgentPresetRowInjected } from './AgentPresetRow.tsx'
import { en, zh } from './locales.ts'
import { AGENT_PRESET_SETTINGS_NS, AgentPresetSettingsController } from './settings-store.ts'
export type { AgentPresetRowInjected, AgentPresetRowProps } from './AgentPresetRow.tsx'
export type { AgentPresetOption, AgentPresetSettingsState } from './settings-store.ts'
export { AGENT_PRESET_SETTINGS_NS } from './settings-store.ts'
/** Required services (cordis fiber inject). */
export const inject = ['slots', 'locale', 'connection']
/**
* Mount the General-settings row.
* @param ctx - the browser plugin context.
*/
export function apply(ctx: ClientContext): void {
const controller = new AgentPresetSettingsController((ctx.get('connection') as ConnectionHandle).api)
ctx.effect(() => ctx.locale.register('settings.agentPreset', { zh, en }), 'ui-agent-preset: settings row dictionaries')
const injected = (): AgentPresetRowInjected => ({
hooks: { agentPreset: controller.store },
load: () => controller.load(),
select: (id: string) => controller.select(id),
})
ctx.effect(() => {
// The roster is a live directory and the default is a settings field, so
// both an external settings edit and a reconnect can move this row.
const refresh = (ns?: string): void => {
if (ns !== undefined && ns !== AGENT_PRESET_SETTINGS_NS) return
void controller.load()
}
const disposers = [
ctx.on('settings/changed', refresh),
ctx.on('connection/reset', () => { refresh() }),
]
return () => { for (const dispose of disposers) dispose() }
}, 'ui-agent-preset: settings refresh')
ctx.slots.inject('settings.general.item', () => ctx.slots.register({
name: 'settings.general.item',
id: 'agent-preset',
order: -25,
locale: 'settings.agentPreset',
inject: injected,
}, AgentPresetRow))
}
@@ -0,0 +1,23 @@
/** Locale bundles for the agent-preset General-settings row. */
/** Locale keys this row renders. */
export type AgentPresetSettingsKey =
| 'title' | 'description' | 'loading' | 'error' | 'userTrust'
/** English copy. */
export const en: Record<AgentPresetSettingsKey, string> = {
title: 'Agent preset',
description: 'Applies to sessions you start from now on. Running sessions keep the preset they began with.',
loading: 'Loading presets…',
error: 'Could not load agent presets.',
userTrust: 'Local',
}
/** Simplified Chinese copy. */
export const zh: Record<AgentPresetSettingsKey, string> = {
title: 'Agent preset',
description: '对此后新建的会话生效。运行中的会话保持它开始时的 preset。',
loading: '正在加载 preset…',
error: '无法加载 agent preset。',
userTrust: '本地',
}
@@ -0,0 +1,114 @@
/**
* Agent-preset default-settings controller.
*
* Options and the current default both come from one `agentPreset.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-client-connection/client'
import { createSnapshotStore, type SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
/** The agent-preset settings namespace on the host wire. */
export const AGENT_PRESET_SETTINGS_NS = 'agent-presets'
/** One selectable preset. */
export interface AgentPresetOption {
/** Preset id, written to Settings and shown as the label. */
id: string
/** Whether the preset ships with the deployment or was authored locally. */
trust: 'system' | 'user'
}
/** Agent-preset settings-row snapshot. */
export interface AgentPresetSettingsState {
status: 'idle' | 'loading' | 'ready' | 'saving' | 'unavailable' | 'error'
error: string | null
writable: boolean
currentValue: string
options: readonly AgentPresetOption[]
}
const INITIAL: AgentPresetSettingsState = {
status: 'idle',
error: null,
writable: true,
currentValue: '',
options: [],
}
/** Reads the roster and persists the chosen default. */
export class AgentPresetSettingsController {
/** Row snapshot the renderer subscribes to. */
readonly store: SnapshotStore<AgentPresetSettingsState> = createSnapshotStore(INITIAL)
constructor(private readonly api: IApiClient) {}
private set(patch: Partial<AgentPresetSettingsState>): void {
this.store.set({ ...this.store.getSnapshot(), ...patch })
}
/**
* Load the roster. An empty roster means the deployment composes no
* presets, which is a valid deployment rather than a failure — the row
* reports `unavailable` and renders nothing.
* @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 = response.result.value.presets
if (presets.length === 0) {
this.set({ status: 'unavailable', options: [], currentValue: '' })
return
}
this.set({
status: 'ready',
error: null,
options: presets.map(preset => ({ id: preset.id, trust: preset.trust })),
currentValue: presets.find(preset => preset.isDefault)?.id ?? presets[0]?.id ?? '',
})
} catch (error) {
this.set({ status: 'error', error: error instanceof Error ? error.message : String(error) })
}
}
/**
* Persist one preset as the default for sessions created later. Running
* sessions keep the composition they were created with, so this never
* disturbs work in progress.
* @param id - the preset to make default.
* @returns once the write settled and the roster was re-read.
*/
async select(id: string): Promise<void> {
const before = this.store.getSnapshot()
if (before.status === 'saving' || id === before.currentValue) return
this.set({ status: 'saving', error: null, currentValue: id })
try {
const response = await this.api.settings.update({
ns: AGENT_PRESET_SETTINGS_NS,
patch: { default: id },
})
if (!response.result.ok) {
this.set({ status: 'ready', currentValue: before.currentValue, error: response.result.error.message })
return
}
// Re-read rather than trust the patch: the host resolves the default
// through the same roster the row displays.
await this.load()
} catch (error) {
this.set({
status: 'ready',
currentValue: before.currentValue,
error: error instanceof Error ? error.message : String(error),
})
}
}
}
+4
View File
@@ -0,0 +1,4 @@
declare module '*.module.css' {
const classes: Record<string, string>
export default classes
}
@@ -0,0 +1,9 @@
/**
* Agent-preset surface plugin, node half. The empty apply exists so the plugin
* appears in the host cordis.yml / Loader; the browser half ships the
* General-settings row through exports["./client"], discovered from the
* package.json dshClient declaration.
*/
/** Host plugin body — no host-side behavior for this surface plugin. */
export function apply(): void {}
@@ -0,0 +1,30 @@
/**
* Package-owned invariant companion for `@deepseek-ai/dsh-client-ui-agent-preset`.
* @module @deepseek-ai/dsh-client-ui-agent-preset/invariant
*/
/* jscpd:ignore-start */
import type { Context } from 'cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-client-ui-agent-preset'
/** Cordis companion plugin name. */
export const name = 'client-ui-agent-preset-invariant'
/** Service required before the companion can reserve package ownership. */
export const inject = ['invariants']
/**
* No runtime invariant: this is a browser-side surface plugin whose node half owns no event stream
* or mutable runtime data; the roster and the settings write are host contracts covered there.
*/
const install: InvariantInstaller = () => {}
/**
* Register this package's invariant companion.
* @param ctx - Cordis context carrying the invariant service.
* @returns the installed registration's disposer after setup succeeds.
*/
export const apply = (ctx: Context): Promise<() => void> =>
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
/* jscpd:ignore-end */
@@ -0,0 +1,122 @@
/**
* The agent-preset settings controller: it derives both the options and the
* current default from one roster call, writes only the `default` field, and
* treats an empty roster as "this deployment composes no presets" rather than
* as a failure.
*/
import { describe, expect, it } from 'vitest'
import type { IApiClient } from '@deepseek-ai/dsh-client-connection/client'
import {
AGENT_PRESET_SETTINGS_NS, AgentPresetSettingsController,
} from '../src/client/settings-store.ts'
interface Recorded { ns: string; patch: unknown }
/** A client whose roster and write outcome the test controls. */
function fakeApi(
presets: { id: string; trust: 'system' | 'user'; isDefault: boolean }[],
options: { writes?: Recorded[]; failWrite?: string; failList?: string } = {},
): IApiClient {
return {
agentPresets: {
list: () => Promise.resolve(options.failList === undefined
? { rpcId: 'r', result: { ok: true as const, value: { presets } } }
: { rpcId: 'r', result: { ok: false as const, error: { code: 'internal', message: options.failList, details: {} } } }),
},
settings: {
update: (payload: { ns: string; patch: unknown }) => {
options.writes?.push({ ns: payload.ns, patch: payload.patch })
if (options.failWrite !== undefined) {
return Promise.resolve({ rpcId: 'r', result: { ok: false as const, error: { code: 'internal', message: options.failWrite, details: {} } } })
}
// A committed write moves the roster's default, exactly as the host does.
for (const preset of presets) {
preset.isDefault = preset.id === (payload.patch as { default?: string }).default
}
return Promise.resolve({ rpcId: 'r', result: { ok: true as const, value: {} } })
},
},
} as unknown as IApiClient
}
describe('the agent-preset settings controller', () => {
it('derives options and the current default from one roster call', async () => {
const controller = new AgentPresetSettingsController(fakeApi([
{ id: 'standard', trust: 'system', isDefault: true },
{ id: 'mine', trust: 'user', isDefault: false },
]))
await controller.load()
const state = controller.store.getSnapshot()
expect(state.status).toBe('ready')
expect(state.currentValue).toBe('standard')
expect(state.options).toEqual([
{ id: 'standard', trust: 'system' },
{ id: 'mine', trust: 'user' },
])
})
it('reports an empty roster as unavailable, not as an error', async () => {
const controller = new AgentPresetSettingsController(fakeApi([]))
await controller.load()
// A deployment composing no presets is valid: every session shares the
// host composition and the row renders nothing.
expect(controller.store.getSnapshot().status).toBe('unavailable')
expect(controller.store.getSnapshot().error).toBeNull()
})
it('writes only the default field, into the agent-presets namespace', async () => {
const writes: Recorded[] = []
const controller = new AgentPresetSettingsController(fakeApi([
{ id: 'standard', trust: 'system', isDefault: true },
{ id: 'core-web', trust: 'system', isDefault: false },
], { writes }))
await controller.load()
await controller.select('core-web')
expect(writes).toEqual([{ ns: AGENT_PRESET_SETTINGS_NS, patch: { default: 'core-web' } }])
expect(controller.store.getSnapshot().currentValue).toBe('core-web')
})
it('restores the previous value and surfaces the message when the write fails', async () => {
const controller = new AgentPresetSettingsController(fakeApi([
{ id: 'standard', trust: 'system', isDefault: true },
{ id: 'core-web', trust: 'system', isDefault: false },
], { failWrite: 'read-only settings' }))
await controller.load()
await controller.select('core-web')
const state = controller.store.getSnapshot()
expect(state.currentValue).toBe('standard')
expect(state.error).toBe('read-only settings')
expect(state.status).toBe('ready')
})
it('ignores a pick that is already the default', async () => {
const writes: Recorded[] = []
const controller = new AgentPresetSettingsController(fakeApi([
{ id: 'standard', trust: 'system', isDefault: true },
], { writes }))
await controller.load()
await controller.select('standard')
expect(writes).toEqual([])
})
it('surfaces a roster failure without claiming the deployment has no presets', async () => {
const controller = new AgentPresetSettingsController(fakeApi([], { failList: 'host down' }))
await controller.load()
const state = controller.store.getSnapshot()
expect(state.status).toBe('error')
expect(state.error).toBe('host down')
})
})
@@ -0,0 +1,33 @@
{
"extends": "../../../tsconfig.base.client.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib/types"
},
"include": [
"src"
],
"references": [
{
"path": "../connection"
},
{
"path": "../locale"
},
{
"path": "../../../vendor/cordis"
},
{
"path": "../runtime"
},
{
"path": "../ui-primitives"
},
{
"path": "../ui-slots"
},
{
"path": "../../support/invariants"
}
]
}
@@ -0,0 +1,3 @@
import { clientBundle } from '../tsdown.client.ts'
export default clientBundle('@deepseek-ai/dsh-client-ui-agent-preset', ['lib/types/index.js', 'lib/types/invariant.js'])
+33
View File
@@ -146,6 +146,9 @@ importers:
'@deepseek-ai/dsh-base':
specifier: workspace:^
version: link:../../packages/bundle/base
'@deepseek-ai/dsh-client-ui-agent-preset':
specifier: workspace:^
version: link:../../packages/client/ui-agent-preset
'@deepseek-ai/dsh-headless':
specifier: workspace:^
version: link:../../packages/bundle/headless
@@ -1432,6 +1435,36 @@ importers:
specifier: ^18.2.0
version: 18.3.1(react@18.3.1)
packages/client/ui-agent-preset:
devDependencies:
'@deepseek-ai/dsh-client-connection':
specifier: workspace:^
version: link:../connection
'@deepseek-ai/dsh-client-locale':
specifier: workspace:^
version: link:../locale
'@deepseek-ai/dsh-client-runtime':
specifier: workspace:^
version: link:../runtime
'@deepseek-ai/dsh-client-ui-primitives':
specifier: workspace:^
version: link:../ui-primitives
'@deepseek-ai/dsh-client-ui-slots':
specifier: workspace:^
version: link:../ui-slots
'@deepseek-ai/dsh-invariants':
specifier: workspace:^
version: link:../../support/invariants
'@types/react':
specifier: ~18.3.1
version: 18.3.31
cordis:
specifier: ^4.0.0-rc.7
version: link:../../../vendor/cordis
react:
specifier: ^18.2.0
version: 18.3.1
packages/client/ui-command:
dependencies:
clsx:
@@ -47,6 +47,7 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
'packages/bash/pwsh-local': { kind: 'indirect', reason: 'The executor backend delegates model rendering to dsh-tool-pwsh.' },
'packages/code-runtime/code-runtime': { kind: 'indirect', reason: 'The service interface delegates model rendering to Code Mode in dsh-tools.' },
'packages/code-runtime/code-runtime-worker': { kind: 'indirect', reason: 'The worker backend delegates model rendering to Code Mode in dsh-tools.' },
'packages/client/ui-agent-preset': { kind: 'indirect', reason: 'Browser-side settings row; the preset it selects owns every model-facing effect.' },
'packages/preset/agent-presets': { kind: 'indirect', reason: 'The mount installs a preset\'s own plugins, which own every model-facing registration it makes visible.' },
'packages/typert/registry': { kind: 'none', reason: 'Runtime type registry; consumers (cordis_inspect, wire faces, gates) own any model-visible projection of registry contents.' },
'packages/typert/loader': { kind: 'none', reason: 'Loader integration only registers generated artifacts; consumers own any model-visible projection.' },
+1
View File
@@ -157,6 +157,7 @@
"@deepseek-ai/dsh-client-ui-command": ["./packages/client/ui-command/src"],
"@deepseek-ai/dsh-client-ui-model": ["./packages/client/ui-model/src"],
"@deepseek-ai/dsh-client-ui-goal": ["./packages/client/ui-goal/src"],
"@deepseek-ai/dsh-client-ui-agent-preset": ["./packages/client/ui-agent-preset/src"],
"@deepseek-ai/dsh-client-ui-permission": ["./packages/client/ui-permission/src"],
"@deepseek-ai/dsh-client-ui-skill": ["./packages/client/ui-skill/src"],
"@deepseek-ai/dsh-client-ui-subagent": ["./packages/client/ui-subagent/src"],
+1
View File
@@ -63,6 +63,7 @@
{ "path": "./packages/client/ui-subagent" },
{ "path": "./packages/client/ui-goal" },
{ "path": "./packages/client/ui-model" },
{ "path": "./packages/client/ui-agent-preset" },
{ "path": "./packages/client/ui-permission" },
{ "path": "./packages/client/ui-plan" },
{ "path": "./packages/client/ui-question" },