fix(ui-agent-preset): restore the read-only configuration viewer

The declarative-preset refactor (#4569) removed the registry's read Remote
together with the YAML editor, and with it the 查看配置 card action that
opened a preset's composition read-only. #4850 brought the Creator entry
back and had to leave 查看配置 out because `agentPresets/read` was gone.

Give the registry a read-only `read` Remote that renders a declaration's
child plugin list from the in-memory definition in the Loader's own
entry-list dialect, so `!!js` conditions read as declared, and put the card
action back at the right end of each card footer: an icon that opens the
YAML in a read-only dialog titled after the preset. Nothing accepts YAML
back, and a broken preset stays readable because its diagnostic points into
that YAML.

Fixes #4895
This commit is contained in:
ZiyaZhang committed 2026-09-21 20:38:30 -07:00
1 parent b13bbc027c
commit 2426d398b5
25 files changed
+322 -43

No files matched your search

+24 -3
View File
@@ -1,4 +1,4 @@
/** The preset settings page only selects a preset; creating one starts a Creator-mode task. */
/** The preset settings page selects a preset and shows what it declares; creating one starts a Creator-mode task. */
import { fileURLToPath } from 'node:url'
import { join } from 'node:path'
import type { Browser, Page } from 'playwright'
@@ -27,13 +27,13 @@ describe('web e2e: preset roster guidance', () => {
}, 120_000)
afterAll(async () => { await browser?.close(); await scaffold?.close() })
it('shows the shipped roster with mode help and no editing actions', async () => {
it('shows the shipped roster with mode help and a read-only view, and no editing actions', async () => {
onTestFailed(() => saveFailureShot(page, 'preset-roster-section'))
await expect.poll(() => page.locator('[data-agent-preset-id]').count()).toBe(4)
const snapshot = await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd)
await compareOrRefreshGolden(join(EXPECTED, 'section.expected.md'), snapshot, mode)
expect(snapshot).toContain('让 Agent 帮我创建预设模式')
expect(snapshot).not.toContain('查看配置')
expect(snapshot).toContain('查看配置: 标准模式')
expect(snapshot).not.toContain('复制预设')
expect(snapshot).not.toContain('编辑插件')
expect(snapshot).not.toContain('打开目录')
@@ -55,6 +55,27 @@ describe('web e2e: preset roster guidance', () => {
expect(await settings.getByRole('button', { name: '新任务默认: 标准模式', exact: true }).getAttribute('aria-pressed')).toBe('true')
})
it('views a shipped composition read-only', async () => {
onTestFailed(() => saveFailureShot(page, 'preset-roster-view'))
const settings = page.getByRole('dialog', { name: '设置' })
await settings.getByRole('button', { name: '查看配置: PTC 模式', exact: true }).click()
const viewer = page.getByRole('dialog', { name: '查看配置 · PTC 模式', exact: true })
await viewer.waitFor({ timeout: 10_000 })
// The real shipped declaration, not a golden: the viewer shows whatever
// the deployment ships, and this lane only asserts it is shown read-only
// in the Loader's own dialect.
const shown = await viewer.locator('pre').textContent()
expect(shown).toContain("- id: persona\n name: '@deepseek-ai/dsh-persona'\n")
expect(shown).toContain('- id: workflow-ptc\n')
expect(shown).toContain("disabled: !!js process.platform === 'win32'\n")
expect(shown).not.toContain('__jsExpr')
expect(await viewer.getByRole('textbox').count()).toBe(0)
// The header X and the footer button share the 关闭 name; the footer one is last.
await viewer.getByRole('button', { name: '关闭', exact: true }).last().click()
await viewer.waitFor({ state: 'detached', timeout: 10_000 })
expect(await settings.getByRole('button', { name: '新任务默认: 标准模式', exact: true }).getAttribute('aria-pressed')).toBe('true')
})
it('starts a Creator-mode task from the section entry', async () => {
onTestFailed(() => saveFailureShot(page, 'preset-roster-creator'))
// The entry stages the self-referential preset and lands a new task on it,
@@ -21,6 +21,7 @@
- text: 处理代码、文件和资料,适合大多数任务。Agent 会按需使用检索、编辑和终端等工具。
- 'button "模式说明: 标准模式"': 模式说明
- 'button "如何使用: 标准模式"': 如何使用
- 'button "查看配置: 标准模式"': 查看配置
- listitem:
- 'button "设为新任务默认: PTC 模式"':
- text: PTC 模式 内置
@@ -28,6 +29,7 @@
- text: 包含标准模式的所有能力,更适合批量调用工具,并对结果进行筛选、整理、去重、统计或汇总的任务。
- 'button "模式说明: PTC 模式"': 模式说明
- 'button "如何使用: PTC 模式"': 如何使用
- 'button "查看配置: PTC 模式"': 查看配置
- listitem:
- 'button "设为新任务默认: 极简模式"':
- text: 极简模式 内置
@@ -35,6 +37,7 @@
- text: Agent 仅使用终端工具完成任务,适合测试和对比其基础表现。
- 'button "模式说明: 极简模式"': 模式说明
- 'button "如何使用: 极简模式"': 如何使用
- 'button "查看配置: 极简模式"': 查看配置
- listitem:
- 'button "设为新任务默认: 创造模式"':
- text: 创造模式 内置
@@ -42,5 +45,6 @@
- text: 用对话定制 DSH:让 Agent 编写插件,添加新功能或界面;也能组合工具和提示词,创建自己的模式。
- 'button "模式说明: 创造模式"': 模式说明
- 'button "如何使用: 创造模式"': 如何使用
- 'button "查看配置: 创造模式"': 查看配置
- heading "自定义" [level=3]
- button "让 Agent 帮我创建预设模式"
+2 -2
View File
@@ -2,5 +2,5 @@
# 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 docs/subsystems/core.md
core.md: aa9af309c03ebedae353139c4017d19f2607dd7d
core.zh.md: b18ff5bba1c0309e9b9dcdf1f3e97adb5895caf6
core.md: 64344148708b29fc186a2e33ef187025c4bc5300
core.zh.md: 0b85519b916490d6692a31f59ed89527ce72fa43
+6
View File
@@ -510,6 +510,12 @@ async list(): Promise<AgentPreset[]>
*/
async resolve(id?: string): Promise<AgentPreset>
/** Read one declaration's child plugin list as YAML, for viewing only.
* @param agentPreset Preset identity.
* @returns The declared composition beside its published metadata.
*/
@Remote('read') readDocument(agentPreset: string): Promise<AgentPresetDocument>
/** Bind an unpublished Agent to the current preset revision.
* @param ctx Agent context from its setup callback.
* @param id Requested preset, or the default.
+6
View File
@@ -520,6 +520,12 @@ async list(): Promise<AgentPreset[]>
*/
async resolve(id?: string): Promise<AgentPreset>
/** Read one declaration's child plugin list as YAML, for viewing only.
* @param agentPreset Preset identity.
* @returns The declared composition beside its published metadata.
*/
@Remote('read') readDocument(agentPreset: string): Promise<AgentPresetDocument>
/** Bind an unpublished Agent to the current preset revision.
* @param ctx Agent context from its setup callback.
* @param id Requested preset, or the default.
@@ -2,5 +2,5 @@
# 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: 176957e32695debf6a8aedd98368ec0d985d9182
README.zh.md: a9496780ce5ea48ae95420d11d9f182134518b93
README.md: 3f50449078f66643025fe235ee45ce63689d65b7
README.zh.md: 8753520ecc97f06eeea0d52ea4e782a7e76c38e8
+5 -5
View File
@@ -1,5 +1,5 @@
---
description: "Choose Agent presets and the new-task default in Web, and read what each mode does. Authoring is guided to Creator mode."
description: "Choose Agent presets and the new-task default in Web, read what each mode does and what it declares. Authoring is guided to Creator mode."
kind: "package-reference"
---
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
## Summary
Choose Agent presets and the new-task default in Web, and read what each mode does. Authoring is guided to Creator mode.
Choose Agent presets and the new-task default in Web, read what each mode does and what it declares. Authoring is guided to Creator mode.
## Table of Contents
@@ -25,7 +25,7 @@ Choose Agent presets and the new-task default in Web, and read what each mode do
<a id="use-this-package"></a>
## Use this package
Settings shows the built-in and custom card groups with default highlighting and card-body selection; a group without presets is omitted, except the custom group, which keeps its Creator entry on screen. The page edits nothing: that entry starts a Creator-mode task that authors or overrides a preset as a bundle, offered while the `cordis` preset is on the roster and a conversation flow exists.
Settings shows the built-in and custom card groups with default highlighting and card-body selection; a group without presets is omitted, except the custom group, which keeps its Creator entry on screen. Every card offers “View configuration”, which opens the preset's declared plugin list as read-only YAML in the Loader's own dialect (`!!js` conditions included); a failed preset stays readable because its diagnostic points into that YAML. The page edits nothing: the Creator entry starts a Creator-mode task that authors or overrides a preset as a bundle, offered while the `cordis` preset is on the roster and a conversation flow exists.
The “Choose a mode for new tasks” switch controls whether the saved user default is active. Hiding selection uses the deployment default; showing it restores the user preference. Choosing a healthy default also synchronizes the blank session on the current new-task surface. Creator starts a new task using the `cordis` preset. The new-session picker additionally requires Developer tools in General Settings.
@@ -37,7 +37,7 @@ Known shipped presets offer mode details and usage examples in a read-only dialo
<details>
<summary>Implementation internals — click to expand</summary>
`agentPresets/list` supplies the roster and the chooser policy; default and visibility changes write the `agent-presets` settings namespace. The picker, blank-session synchronization and read-only session label use recorded preset identities. Connection resets and settings updates refresh the roster.
`agentPresets/list` supplies the roster and the chooser policy, and `agentPresets/read` one declaration's YAML for the viewer; default and visibility changes write the `agent-presets` settings namespace. The picker, blank-session synchronization and read-only session label use recorded preset identities. Connection resets and settings updates refresh the roster.
</details>
@@ -61,7 +61,7 @@ Selection changes affect only later tasks; existing plugins and prompts remain u
<a id="known-limitations-and-deferred-work"></a>
- Web creates and edits no preset: a bundle installed through Creator mode declares a new preset or overrides a shipped one by row id, replacing its complete child list.
- Web creates and edits no preset: the configuration viewer is read-only, and a bundle installed through Creator mode declares a new preset or overrides a shipped one by row id, replacing its complete child list.
<a id="dev-note"></a>
### Dev Note
+5 -5
View File
@@ -1,5 +1,5 @@
---
description: "在 Web 中选择 Agent preset 和新任务默认值,并查看各模式的说明。创建与修改引导至创造模式。"
description: "在 Web 中选择 Agent preset 和新任务默认值,查看各模式的说明与声明的配置。创建与修改引导至创造模式。"
kind: "package-reference"
---
@@ -9,7 +9,7 @@ kind: "package-reference"
## 概述
在 Web 中选择 Agent preset 和新任务默认值,并查看各模式的说明。创建与修改引导至创造模式。
在 Web 中选择 Agent preset 和新任务默认值,查看各模式的说明与声明的配置。创建与修改引导至创造模式。
## 目录
@@ -25,7 +25,7 @@ kind: "package-reference"
<a id="use-this-package"></a>
## 使用此包
设置页显示内置与自定义卡片分组、默认项高亮和点击卡片选择;没有 preset 的分组不显示,但自定义分组会保留其创造入口。本页不编辑任何内容:该入口启动一个创造模式任务,以 bundle 形式创建或覆盖 preset;当 `cordis` preset 在列表中且存在会话流程时提供。
设置页显示内置与自定义卡片分组、默认项高亮和点击卡片选择;没有 preset 的分组不显示,但自定义分组会保留其创造入口。每张卡片都提供「查看配置」,以只读 YAML 打开该 preset 声明的插件列表,使用 Loader 自己的方言(含 `!!js` 条件);加载失败的 preset 同样可读,因为其诊断信息正指向这份 YAML。本页不编辑任何内容:创造入口启动一个创造模式任务,以 bundle 形式创建或覆盖 preset;当 `cordis` preset 在列表中且存在会话流程时提供。
「新任务可选择模式」开关控制保存的用户默认值是否生效。关闭时使用部署默认值,重新开启则恢复用户偏好。选择健康定义作为默认值也会同步当前新任务页面的空白会话。Creator 入口开启一个使用 `cordis` preset 的新任务。新会话选择器还要求通用设置开启开发者工具。
@@ -37,7 +37,7 @@ kind: "package-reference"
<details>
<summary>实现细节 — 点击展开</summary>
`agentPresets/list` 提供列表和选择器策略;默认值与可见性的修改写入 `agent-presets` settings 命名空间。选择器、空白会话同步和只读会话标签使用记录的 preset 标识。连接重置和设置更新会刷新列表。
`agentPresets/list` 提供列表和选择器策略,`agentPresets/read` 为查看器提供一条声明的 YAML;默认值与可见性的修改写入 `agent-presets` settings 命名空间。选择器、空白会话同步和只读会话标签使用记录的 preset 标识。连接重置和设置更新会刷新列表。
</details>
@@ -61,7 +61,7 @@ kind: "package-reference"
<a id="known-limitations-and-deferred-work"></a>
- Web 不创建也不编辑 preset:通过创造模式安装的 bundle 声明新 preset,或按行 id 覆盖内置 preset,覆盖会替换整个子插件列表。
- Web 不创建也不编辑 preset:配置查看器是只读的;通过创造模式安装的 bundle 声明新 preset,或按行 id 覆盖内置 preset,覆盖会替换整个子插件列表。
<a id="dev-note"></a>
### 开发备注
@@ -317,6 +317,77 @@
color: var(--dsw-alias-label-primary);
}
/* Icon-only actions: the label rides `data-tip` so the row stays quiet until
someone reaches for it. */
.iconButton {
position: relative;
appearance: none;
border: 0;
border-radius: 7px;
padding: 6px;
background: none;
color: var(--dsw-alias-label-tertiary);
cursor: pointer;
display: inline-flex;
align-items: center;
}
.iconButton:hover {
background: var(--dsw-alias-bg-layer-1);
color: var(--dsw-alias-label-primary);
}
.iconButton:focus-visible {
outline: 2px solid var(--dsw-alias-brand-primary);
outline-offset: -1px;
}
.iconButton::after {
content: attr(data-tip);
position: absolute;
bottom: calc(100% + 6px);
left: 50%;
transform: translateX(-50%);
padding: 3px 8px;
border-radius: 6px;
background: var(--dsw-alias-label-primary);
color: var(--dsw-alias-bg-layer-3);
font-size: 11px;
line-height: 17px;
white-space: nowrap;
opacity: 0;
pointer-events: none;
transition: opacity .12s;
}
.iconButton:hover::after,
.iconButton:focus-visible::after {
opacity: 1;
}
.dialog {
width: min(720px, 100%);
}
/* A declared composition can be long; the dialog scrolls it rather than grow. */
.viewerCode {
margin: 0;
padding: 12px;
max-height: min(60vh, 560px);
overflow: auto;
border: 0.5px solid var(--dsw-alias-border-l4);
border-radius: 10px;
background: var(--dsw-alias-bg-layer-2);
color: var(--dsw-alias-label-secondary);
font-family: var(--dsw-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
font-size: 12.5px;
line-height: 1.5;
white-space: pre;
tab-size: 2;
--dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);
--dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);
}
.error {
margin: 0;
font-size: 12px;
@@ -1,7 +1,9 @@
/** Preset selection settings: the roster, its default, mode help and the Creator-mode entry. */
/** Preset selection settings: the roster, its default, mode help, a read-only view of each composition, and the Creator-mode entry. */
import type { ReactNode } from 'react'
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
import { Button, IconPlusOutlineRegular, Switch, Tag, Tooltip } from '@deepseek-ai/dsh-client-ui-primitives'
import {
Button, IconBrowseOutlineRegular, IconPlusOutlineRegular, Modal, Switch, Tag, Tooltip,
} from '@deepseek-ai/dsh-client-ui-primitives'
import type { ObservableSnapshot, SnapshotStore } from '@deepseek-ai/dsh-client-store'
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
import type { AgentPresetSectionState } from './section-store.ts'
@@ -19,6 +21,10 @@ export interface AgentPresetSectionInjected {
/** Stage the `cordis` preset and start a Creator-mode task; absent without a conversation flow. */
startCreatorDraft?: () => void
load: () => Promise<void>
/** Open one preset's declared composition in the read-only viewer. */
view: (id: string) => Promise<void>
/** Close the read-only viewer. */
closeView: () => void
makeDefault: (id: string) => Promise<void>
setPickerVisible: (visible: boolean) => Promise<void>
}
@@ -51,12 +57,13 @@ function CardDescription({ text }: { text: string }): ReactNode {
)
}
/** Render the roster with its default, mode help, and the guidance to Creator mode.
/** Render the roster with its default, mode help, composition viewer, and the guidance to Creator mode.
* @param props Settings actions, snapshot hooks and localized text.
* @returns The preset settings section.
*/
export function AgentPresetSection({
useAgentPresetSection, load, makeDefault, setPickerVisible, startCreatorDraft, close: closeSettings, useDeveloperTools, t,
useAgentPresetSection, load, view, closeView, makeDefault, setPickerVisible, startCreatorDraft,
close: closeSettings, useDeveloperTools, t,
}: AgentPresetSectionProps) {
const state = useAgentPresetSection(value => value)
const developerTools = useDeveloperTools(enabled => enabled)
@@ -65,6 +72,9 @@ export function AgentPresetSection({
page: PresetGuidePage
} | null>(null)
useEffect(() => { void load() }, [load])
const viewed = state.view
const viewedRow = viewed === null ? undefined : state.rows.find(row => row.id === viewed.id)
const viewedTitle = viewed === null ? '' : viewedRow === undefined ? viewed.title : presetDisplayText(viewedRow, t).name
// Creator mode authors presets in conversation; it needs the flow to land a
// session in and the self-referential preset on the roster.
const creator = startCreatorDraft !== undefined && state.rows.some(row => row.id === 'cordis') ? startCreatorDraft : undefined
@@ -138,14 +148,21 @@ export function AgentPresetSection({
<CardDescription text={display.description ?? t('noDescription')} />
{row.broken === undefined ? null : <span className={css.cardBrokenReason} role="alert">{row.broken}</span>}
</button>
{help === undefined ? null : <div className={css.cardFoot}>
<div className={css.cardHelp}>
<div className={css.cardFoot}>
{help === undefined ? null : <div className={css.cardHelp}>
<Button variant="ghost" className={css.helpButton} aria-label={`${t('modeExplanation')}: ${display.name}`}
onClick={() => { setGuide({ content: help, page: 'explanation' }) }}>{t('modeExplanation')}</Button>
<Button variant="ghost" className={css.helpButton} aria-label={`${t('howToUse')}: ${display.name}`}
onClick={() => { setGuide({ content: help, page: 'usage' }) }}>{t('howToUse')}</Button>
</div>
</div>}
</div>}
{/* Reading the declaration is the one thing this page offers
beyond choosing: a broken preset's YAML is also where its
diagnostic points, so the viewer stays available for it. */}
<button type="button" className={css.iconButton} data-tip={t('view')} aria-label={`${t('view')}: ${display.name}`}
onClick={() => { void view(row.id) }}>
<IconBrowseOutlineRegular />
</button>
</div>
</li>
})}
</ul>}
@@ -153,5 +170,10 @@ export function AgentPresetSection({
</section>
})}
{guide === null ? null : <PresetGuideDialog guide={guide.content} initialPage={guide.page} t={t} onClose={() => { setGuide(null) }} />}
<Modal open={viewed !== null} onClose={closeView} closeLabel={t('close')}
title={viewed === null ? '' : `${t('view')} · ${viewedTitle}`} className={css.dialog as string}
footer={<Button variant="outline" autoFocus onClick={closeView}>{t('close')}</Button>}>
{viewed === null ? null : <pre className={css.viewerCode}>{viewed.content}</pre>}
</Modal>
</section>
}
@@ -2,7 +2,8 @@
* Agent-preset surface plugin, browser half — three surfaces over one roster:
* a chip on the new-session screen for the session about to start, a
* read-only label in the session header, and a settings section that lists
* the roster (selection, the new-task default, and the way into Creator mode).
* the roster (selection, the new-task default, a read-only view of each
* declared composition, and the way into Creator mode).
*
* A running session keeps the composition it began with (the host refuses to
* adopt an existing session under a different preset). That is what splits
@@ -49,7 +50,7 @@ export type { AgentPresetLabelInjected, AgentPresetLabelProps } from './AgentPre
export type { AgentPresetSeatInjected, AgentPresetSeatProps } from './AgentPresetSeat.tsx'
export type { AgentPresetSectionInjected, AgentPresetSectionProps } from './AgentPresetSection.tsx'
export type { AgentPresetSeatState } from './seat-store.ts'
export type { AgentPresetSectionState } from './section-store.ts'
export type { AgentPresetSectionState, PresetView } from './section-store.ts'
export type { AgentPresetOption, AgentPresetSettingsState } from './settings-store.ts'
export { AGENT_PRESET_SETTINGS_NS, writeDefaultPreset } from './settings-store.ts'
@@ -199,6 +200,8 @@ export function apply(ctx: ClientContext): void {
const sectionInjected = (): AgentPresetSectionInjected => ({
hooks: { agentPresetSection: section.store, developerTools: ctx.configForms.developerTools.enabled },
load: () => section.load(),
view: (id: string) => section.view(id),
closeView: () => { section.closeView() },
...creatorDraft === undefined ? {} : { startCreatorDraft: creatorDraft },
makeDefault: (id: string) => section.makeDefault(id, captureBlankSessionSync()),
setPickerVisible: (showPicker: boolean) => section.setPickerVisible(showPicker, captureBlankSessionSync()),
@@ -12,6 +12,7 @@ export type AgentPresetSettingsKey =
| 'nav'
| 'sectionIntro'
| 'setDefault'
| 'view'
| 'presetStandardName'
| 'presetStandardDescription'
| 'presetPtcName'
@@ -44,6 +45,7 @@ export const en: Record<AgentPresetSettingsKey, string> = {
nav: 'Agent presets',
setDefault: 'Set as new task default',
view: 'View configuration',
presetStandardName: 'Standard mode',
presetStandardDescription:
@@ -89,6 +91,7 @@ export const zh: Record<AgentPresetSettingsKey, string> = {
nav: 'Agent 预设',
setDefault: '设为新任务默认',
view: '查看配置',
presetStandardName: '标准模式',
presetStandardDescription: '处理代码、文件和资料,适合大多数任务。Agent 会按需使用检索、编辑和终端等工具。',
@@ -1,10 +1,19 @@
/** Preset roster and selection policy for the settings section. */
/** Preset roster, selection policy and the read-only composition viewer for the settings section. */
import type { Context } from '@deepseek-ai/cordis'
import type {} from '@deepseek-ai/dsh-api-remotes/client'
import { createSnapshotStore, type SnapshotStore } from '@deepseek-ai/dsh-client-store'
import type { AgentPresetRow } from '@deepseek-ai/dsh-agent-preset-registry/types'
import { writeDefaultPreset, writeModeSelectionEnabled } from './settings-store.ts'
/** The read-only composition viewer over one preset. */
export interface PresetView {
/** The preset being read. */
id: string
/** Display name the preset published, or its id. */
title: string
/** The declared child plugin list as YAML. */
content: string
}
/** Settings page state. */
export interface AgentPresetSectionState {
status: 'idle' | 'loading' | 'ready' | 'error'
@@ -12,13 +21,15 @@ export interface AgentPresetSectionState {
showPicker: boolean
policySaving: boolean
rows: readonly AgentPresetRow[]
/** The open viewer, or null. */
view: PresetView | null
}
const INITIAL: AgentPresetSectionState = { status: 'idle', error: null, showPicker: true, policySaving: false, rows: [] }
const INITIAL: AgentPresetSectionState = { status: 'idle', error: null, showPicker: true, policySaving: false, rows: [], view: null }
const message = (error: unknown): string => error instanceof Error ? error.message : String(error)
/** Loads the roster and writes the default and chooser policy. */
/** Loads the roster, writes the default and chooser policy, and reads one composition at a time. */
export class AgentPresetSectionController {
/** Observable roster and selection state. */
/** Observable roster, selection and viewer state. */
readonly store: SnapshotStore<AgentPresetSectionState> = createSnapshotStore(INITIAL)
private loading: Promise<void> | undefined
constructor(private readonly ctx: Context) {}
@@ -39,6 +50,22 @@ export class AgentPresetSectionController {
} catch (error) { this.set({ status: 'error', error: message(error) }) }
}
/** Open one preset's declared composition in the viewer.
* @param id Preset to read.
* @returns Once the read settles; a failure lands in `error` and leaves the viewer closed.
*/
async view(id: string): Promise<void> {
this.set({ error: null })
try {
const result = await this.ctx.remote.agentPresets.read(id)
if (!result.ok) throw new Error(result.error.message)
const { name, content } = result.value
this.set({ view: { id, title: name ?? id, content } })
} catch (error) { this.set({ error: message(error) }) }
}
/** Close the viewer. */
closeView(): void { this.set({ view: null }) }
/** Set the default and synchronize the current blank task when supplied.
* @param id Selected default.
* @param sync Blank-session synchronization callback.
@@ -131,9 +131,9 @@ async function bench(options: {
}
return Promise.resolve(ROSTER)
},
read: () => Promise.resolve({
read: (id: string) => Promise.resolve({
ok: true as const,
value: { agentPreset: 'standard', content: '', revision: 'rev' },
value: { agentPreset: id, content: `# ${id}\n[]\n` },
}),
save: (id: string) => {
calls.push(`save:${id}`)
@@ -350,6 +350,11 @@ describe('ui-agent-preset apply', () => {
await section.makeDefault('standard')
expect(section.hooks.agentPresetSection.getSnapshot().rows)
.toEqual([{ id: 'standard', isDefault: true }])
await section.view('standard')
expect(section.hooks.agentPresetSection.getSnapshot().view)
.toEqual({ id: 'standard', title: 'standard', content: '# standard\n[]\n' })
section.closeView()
expect(section.hooks.agentPresetSection.getSnapshot().view).toBeNull()
})
it('refreshes a showing surface when its namespace changes, and ignores others', async () => {
@@ -4,6 +4,7 @@ import { AgentPresetSectionController } from '../src/client/section-store.ts'
function fixture() {
const remote = { agentPresets: {
list: vi.fn(async () => ({ ok: true as const, value: { presets: [{ id: 'standard', isDefault: true }], modeSelectionEnabled: true } })),
read: vi.fn(async (id: string) => ({ ok: true as const, value: { agentPreset: id, name: 'Standard', content: '- name: fs\n' } })),
}, settings: { update: vi.fn(async () => ({ ok: true as const, value: {} })) } }
const controller = new AgentPresetSectionController({ remote } as never)
return { remote, controller, state: () => controller.store.getSnapshot() }
@@ -14,7 +15,7 @@ describe('the preset roster', () => {
const { controller, remote, state } = fixture()
await Promise.all([controller.load(), controller.load()])
expect(remote.agentPresets.list).toHaveBeenCalledOnce()
expect(state()).toMatchObject({ status: 'ready', rows: [{ id: 'standard', isDefault: true }], showPicker: true })
expect(state()).toMatchObject({ status: 'ready', rows: [{ id: 'standard', isDefault: true }], showPicker: true, view: null })
remote.agentPresets.list.mockRejectedValueOnce(new Error('offline'))
await controller.load()
expect(state()).toMatchObject({ status: 'error', error: 'offline' })
@@ -26,6 +27,25 @@ describe('the preset roster', () => {
expect(state().error).toBe('gone')
})
it('opens one declared composition for reading and keeps a failed read out of the viewer', async () => {
const { controller, remote, state } = fixture()
await controller.view('standard')
expect(remote.agentPresets.read).toHaveBeenCalledWith('standard')
expect(state().view).toEqual({ id: 'standard', title: 'Standard', content: '- name: fs\n' })
controller.closeView()
expect(state().view).toBeNull()
remote.agentPresets.read.mockResolvedValueOnce({ ok: true, value: { agentPreset: 'mine', content: '[]\n' } } as never)
await controller.view('mine')
expect(state().view).toEqual({ id: 'mine', title: 'mine', content: '[]\n' })
controller.closeView()
remote.agentPresets.read.mockResolvedValueOnce({ ok: false, error: { message: 'Unknown agent preset: gone' } } as never)
await controller.view('gone')
expect(state()).toMatchObject({ view: null, error: 'Unknown agent preset: gone' })
remote.agentPresets.read.mockRejectedValueOnce(new Error('offline'))
await controller.view('standard')
expect(state()).toMatchObject({ view: null, error: 'offline' })
})
it('keeps default selection and blank-session synchronization on their existing settings path', async () => {
const { controller, remote, state } = fixture()
const sync = vi.fn(async () => undefined)
@@ -13,8 +13,9 @@ function unusedHook(): never {
}
function view(partial: Partial<AgentPresetSectionState> = {}, startCreatorDraft?: () => void, developerTools = true) {
const store = createSnapshotStore<AgentPresetSectionState>({ status: 'ready', error: null,
showPicker: true, policySaving: false, rows: [{ id: 'standard', isDefault: true }, { id: 'mine', name: 'Mine', isDefault: false }], ...partial })
const actions = { load: vi.fn(async () => {}), makeDefault: vi.fn(async () => {}),
showPicker: true, policySaving: false, rows: [{ id: 'standard', isDefault: true }, { id: 'mine', name: 'Mine', isDefault: false }],
view: null, ...partial })
const actions = { load: vi.fn(async () => {}), view: vi.fn(async () => {}), closeView: vi.fn(), makeDefault: vi.fn(async () => {}),
setPickerVisible: vi.fn(async () => {}), close: vi.fn() }
const props: AgentPresetSectionProps = { ...actions,
...(startCreatorDraft === undefined ? {} : { startCreatorDraft }),
@@ -87,6 +88,27 @@ it('disables the Creator entry when mode selection is hidden', () => {
fireEvent.click(button)
expect(launch).not.toHaveBeenCalled()
})
it('reads a declared composition read-only from every card, broken ones included', () => {
const actions = view({ rows: [{ id: 'standard', isDefault: true }, { id: 'broken', isDefault: false, broken: 'Missing plugin' }] })
expect(screen.queryByRole('dialog')).toBeNull()
fireEvent.click(within(rowFor('standard')).getByRole('button', { name: `${en.view}: ${en.presetStandardName}` }))
expect(actions.view).toHaveBeenCalledWith('standard')
fireEvent.click(within(rowFor('broken')).getByRole('button', { name: `${en.view}: broken` }))
expect(actions.view).toHaveBeenLastCalledWith('broken')
expect(actions.makeDefault).not.toHaveBeenCalled()
})
it('shows the open composition under the preset display name, without copy, and closes it from the footer', () => {
const actions = view({ view: { id: 'standard', title: 'standard', content: '- id: tool-fs\n name: fs\n' } })
const dialog = screen.getByRole('dialog', { name: `${en.view} · ${en.presetStandardName}` })
expect(dialog.querySelector('pre')?.textContent).toBe('- id: tool-fs\n name: fs\n')
expect(within(dialog).queryByRole('textbox')).toBeNull()
expect(dialog.querySelectorAll('p')).toHaveLength(0)
fireEvent.click(within(dialog).getAllByRole('button', { name: en.close }).at(-1)!)
expect(actions.closeView).toHaveBeenCalledOnce()
cleanup()
view({ view: { id: 'gone', title: 'Gone', content: '[]\n' } })
expect(screen.getByRole('dialog', { name: `${en.view} · Gone` })).toBeTruthy()
})
it('shows roster errors while the policy switch stays usable', () => {
const actions = view({ error: 'Roster stale', rows: [{ id: 'broken', isDefault: false, broken: 'Missing plugin' }] })
expect(screen.getAllByRole('alert').map(node => node.textContent)).toEqual(['Roster stale', 'Missing plugin'])
@@ -159,6 +159,12 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
parameters: [{ name: 'id', description: 'Explicit preset or the current default.' }],
returns: 'Current metadata, including failure when activation failed.',
},
{
signature: '@Remote(\'read\') readDocument(agentPreset: string): Promise<AgentPresetDocument>',
description: 'Read one declaration\'s child plugin list as YAML, for viewing only.',
parameters: [{ name: 'agentPreset', description: 'Preset identity.' }],
returns: 'The declared composition beside its published metadata.',
},
{
signature: 'async mount(ctx: Context, id?: string): Promise<AgentPreset>',
description: 'Bind an unpublished Agent to the current preset revision.',
@@ -4182,6 +4188,10 @@ export const TYPE_API: readonly TypeApiEntry[] = [
name: 'AgentPresetCompositionRow',
declaration: 'export interface AgentPresetCompositionRow {\n readonly entryId: string | null;\n readonly moduleName: string;\n readonly enabled: CompositionRowEnablement;\n readonly condition?: string;\n readonly fiberState?: FiberState;\n}',
},
{
name: 'AgentPresetDocument',
declaration: 'export interface AgentPresetDocument {\n readonly agentPreset: string;\n readonly content: string;\n readonly name?: string;\n readonly description?: string;\n}',
},
{
name: 'AgentPresetRoster',
declaration: 'export interface AgentPresetRoster {\n readonly presets: readonly AgentPresetRow[];\n readonly modeSelectionEnabled: boolean;\n}',
@@ -2,5 +2,5 @@
# 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/preset/agent-preset-registry/README.md
README.md: 7a1ee3e733fc54194515dbf093e53af0c9698e40
README.zh.md: 11206a3f9237cbca237220a81fd98ec5eea1083e
README.md: 44b01be1ceea5936668f091fb410137f3a139a88
README.zh.md: a0c75fa50ba781ffc394e04502c922684beda387
@@ -45,7 +45,7 @@ Choose an Agent’s tools, prompt sections and skills through declarative preset
The Web definitions come from the `dsh-web-app` bundle. Definitions are ordinary plugin rows; the registry neither scans directories nor accepts preset paths. The `selectedDefault` and `modeSelectionEnabled` volatile fields of the `agent-preset-registry` entry retain the user default and the chooser visibility; hiding the chooser uses the deployment `default`.
The registry writes no declarations. A new preset or an override of a shipped one is a bundle patch: an `insert` of a `@deepseek-ai/dsh-agent-preset` row, or a patch keyed by that row’s id, installed into the profile with `plugin_manager`; Creator mode authors such bundles in conversation.
The registry writes no declarations. The `read` Remote renders one declaration’s child list back as entry-list YAML (`!!js` conditions included) so a client can show what a preset composes; nothing accepts YAML back. A new preset or an override of a shipped one is a bundle patch: an `insert` of a `@deepseek-ai/dsh-agent-preset` row, or a patch keyed by that row’s id, installed into the profile with `plugin_manager`; Creator mode authors such bundles in conversation.
<a id="understand-the-implementation"></a>
## Understand the implementation
@@ -45,7 +45,7 @@ kind: "package-reference"
Web 内置定义来自 `dsh-web-app` bundle。定义使用普通插件行;注册表不扫描目录,也不接受 preset 路径。`agent-preset-registry` 条目的 volatile 字段 `selectedDefault` 与 `modeSelectionEnabled` 保留用户默认值和选择器可见性;隐藏选择器时使用部署 `default`。
注册表不写入任何声明。新建 preset 或覆盖内置 preset 都是 bundle 补丁:插入一行 `@deepseek-ai/dsh-agent-preset`,或按该行 id 写覆盖补丁,再用 `plugin_manager` 安装到 profile;创造模式在对话中编写这类 bundle。
注册表不写入任何声明。`read` Remote 把一条声明的子插件列表按 entry-list YAML 方言(含 `!!js` 条件)渲染回来,供客户端展示 preset 的组成;没有任何接口接受 YAML 写回。新建 preset 或覆盖内置 preset 都是 bundle 补丁:插入一行 `@deepseek-ai/dsh-agent-preset`,或按该行 id 写覆盖补丁,再用 `plugin_manager` 安装到 profile;创造模式在对话中编写这类 bundle。
<a id="understand-the-implementation"></a>
## 理解实现
@@ -53,6 +53,7 @@
],
"license": "MIT",
"peerDependencies": {
"@deepseek-ai/cordis-plugin-include": "workspace:^",
"@deepseek-ai/cordis-plugin-loader": "workspace:^",
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
@@ -67,6 +68,7 @@
},
"dependencies": {
"@deepseek-ai/schemastery": "workspace:^",
"js-yaml": "^4.2.0",
"zod": "^4.4.3"
},
"devDependencies": {
@@ -91,6 +93,7 @@
"@deepseek-ai/dsh-sandbox": "workspace:^",
"@deepseek-ai/dsh-sandbox-policy": "workspace:^",
"@deepseek-ai/dsh-user-approval": "workspace:^",
"@deepseek-ai/dsh-agent-preset": "workspace:^"
"@deepseek-ai/dsh-agent-preset": "workspace:^",
"@types/js-yaml": "^4.0.9"
}
}
@@ -3,11 +3,13 @@ import { Context } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
import { Remote, RemoteError, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'
import { bindScopeParent, createScope, scopeOf, type Scope, type ScopeKey, type ScopeParentBinding } from '@deepseek-ai/dsh-scope'
import { entryListSchema } from '@deepseek-ai/cordis-plugin-include'
import { dump } from 'js-yaml'
import type { Agent } from '@deepseek-ai/dsh-agent'
// Type-only: the optional `settings` service this registry keeps off the generated pages.
import type {} from '@deepseek-ai/dsh-settings'
import type {} from '@deepseek-ai/dsh-tools'
import type { AgentPresetRoster } from './types.ts'
import type { AgentPresetDocument, AgentPresetRoster } from './types.ts'
import { entryListProblem, type PresetDefinition } from './definition.ts'
import type { AgentPreset, Config } from './preset.ts'
import { agentPresetProjectionDefinition } from './session.ts'
@@ -191,6 +193,25 @@ export class AgentPresetRegistry extends TypertRemoteService {
return { id: wanted, ...(broken === undefined ? {} : { broken }) }
}
/** Read one declaration's child plugin list as YAML, for viewing only.
* @param agentPreset Preset identity.
* @returns The declared composition beside its published metadata.
*/
@Remote('read')
readDocument(agentPreset: string): Promise<AgentPresetDocument> {
const record = this.definitions.get(agentPreset)
if (record === undefined) {
return Promise.reject(new RemoteError('agent-preset/not-found', `Unknown agent preset: ${agentPreset}`,
{ agentPreset, available: [...this.definitions.keys()] }))
}
const { id, name, description, plugins } = record.config
// The Loader's own dialect, so `!!js` conditions read as declared rather than as expression objects.
const content = dump(plugins, { schema: entryListSchema, noRefs: true, lineWidth: -1 })
return Promise.resolve({
agentPreset: id, content, ...(name === undefined ? {} : { name }), ...(description === undefined ? {} : { description }),
})
}
private async retain(id?: string): Promise<Generation> {
const wanted = id ?? this.defaultId
while (true) {
@@ -26,6 +26,18 @@ export interface AgentPresetRoster {
readonly modeSelectionEnabled: boolean
}
/** One preset's declared composition, rendered for reading. */
export interface AgentPresetDocument {
/** The preset the composition belongs to. */
readonly agentPreset: string
/** The declared child plugin list as entry-list YAML, `!!js` expressions included. */
readonly content: string
/** Display name the preset published. */
readonly name?: string
/** One sentence on what this preset is for. */
readonly description?: string
}
declare module '@deepseek-ai/dsh-typert-protocol' {
interface RemoteErrorDetailsMap {
/** No declaration supplies the requested identity. */
@@ -2,7 +2,7 @@ import { afterEach, describe, expect, it } from 'vitest'
import { createScope } from '@deepseek-ai/dsh-scope'
import { assembleContextFor } from '@deepseek-ai/dsh-agent'
import { entryListProblem, livePresetMounts } from '../src/index.ts'
import { currentKey, harness, declare, contribution, agentOn, liveRegistries } from './harness.ts'
import { currentKey, harness, declare, contribution, agentOn, liveRegistries, plugin } from './harness.ts'
import { omitsGeneratedPage } from '../../../settings/settings/tests/live-config.ts'
import Loader from '@deepseek-ai/cordis-plugin-loader'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
@@ -121,6 +121,23 @@ describe('declarative preset revisions', () => {
expect.objectContaining({ id: 'empty', rows: [expect.objectContaining({ moduleName: 'missing', enabled: false })] }),
]))
})
it('renders a declaration as entry-list YAML for reading, conditions included', async () => {
const ctx = await setup()
await declare(ctx, {
...contribution('standard'), name: 'Standard', description: 'General',
plugins: [
{ id: 'contribute', name: plugin('contribute'), config: { tool: 'standard' } },
{ id: 'windows-only', name: 'missing', disabled: { __jsExpr: "process.platform !== 'win32'" } },
],
})
const document = await ctx.agentPresets.readDocument('standard')
expect(document).toMatchObject({ agentPreset: 'standard', name: 'Standard', description: 'General' })
expect(document.content.startsWith('- id: contribute\n name: ')).toBe(true)
expect(document.content).toContain("\n config:\n tool: standard\n- id: windows-only\n name: missing\n disabled: !!js process.platform !== 'win32'\n")
expect(document.content).not.toContain('__jsExpr')
await expect(ctx.agentPresets.readDocument('absent')).rejects.toThrow('Unknown agent preset: absent')
})
})
it('retains a retired revision for an in-flight cold read', async () => {
+6
View File
@@ -9409,6 +9409,9 @@ importers:
'@deepseek-ai/schemastery':
specifier: link:../../../vendor/schemastery
version: link:../../../vendor/schemastery
js-yaml:
specifier: ^4.2.0
version: 4.3.1
zod:
specifier: ^4.4.3
version: 4.4.3
@@ -9479,6 +9482,9 @@ importers:
'@deepseek-ai/dsh-user-approval':
specifier: workspace:^
version: link:../../interaction/user-approval
'@types/js-yaml':
specifier: ^4.0.9
version: 4.0.9
packages/preset/persona:
dependencies: