From 2426d398b5cf23d0e13598580bb6e5fe2cfd292d Mon Sep 17 00:00:00 2001 From: ZiyaZhang <199893125+ZiyaZhang@users.noreply.github.com> Date: Mon, 21 Sep 2026 20:38:30 -0700 Subject: [PATCH] fix(ui-agent-preset): restore the read-only configuration viewer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- apps/web/tests/agent-preset-authoring.e2e.ts | 27 ++++++- .../section.expected.md | 4 ++ docs/subsystems/core.i18n.yaml | 4 +- docs/subsystems/core.md | 6 ++ docs/subsystems/core.zh.md | 6 ++ .../client/ui-agent-preset/README.i18n.yaml | 4 +- packages/client/ui-agent-preset/README.md | 10 +-- packages/client/ui-agent-preset/README.zh.md | 10 +-- .../src/client/AgentPresetSection.module.css | 71 +++++++++++++++++++ .../src/client/AgentPresetSection.tsx | 38 +++++++--- .../ui-agent-preset/src/client/index.ts | 7 +- .../ui-agent-preset/src/client/locales.ts | 3 + .../src/client/section-store.ts | 35 +++++++-- .../tests/apply.client.spec.ts | 9 ++- .../tests/section-store.client.spec.ts | 22 +++++- .../tests/section.client.spec.tsx | 26 ++++++- .../extensions/tool-cordis/src/api-catalog.ts | 10 +++ .../agent-preset-registry/README.i18n.yaml | 4 +- .../preset/agent-preset-registry/README.md | 2 +- .../preset/agent-preset-registry/README.zh.md | 2 +- .../preset/agent-preset-registry/package.json | 5 +- .../preset/agent-preset-registry/src/index.ts | 23 +++++- .../preset/agent-preset-registry/src/types.ts | 12 ++++ .../tests/registry.spec.ts | 19 ++++- pnpm-lock.yaml | 6 ++ 25 files changed, 322 insertions(+), 43 deletions(-) diff --git a/apps/web/tests/agent-preset-authoring.e2e.ts b/apps/web/tests/agent-preset-authoring.e2e.ts index 4986424158..3a6058efa6 100644 --- a/apps/web/tests/agent-preset-authoring.e2e.ts +++ b/apps/web/tests/agent-preset-authoring.e2e.ts @@ -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, diff --git a/apps/web/tests/expected/agent-preset-authoring/section.expected.md b/apps/web/tests/expected/agent-preset-authoring/section.expected.md index 7bccb7e4c2..11f454119f 100644 --- a/apps/web/tests/expected/agent-preset-authoring/section.expected.md +++ b/apps/web/tests/expected/agent-preset-authoring/section.expected.md @@ -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 帮我创建预设模式" diff --git a/docs/subsystems/core.i18n.yaml b/docs/subsystems/core.i18n.yaml index 29fbf9d0ae..51b124f552 100644 --- a/docs/subsystems/core.i18n.yaml +++ b/docs/subsystems/core.i18n.yaml @@ -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 diff --git a/docs/subsystems/core.md b/docs/subsystems/core.md index aa9af309c0..6434414870 100644 --- a/docs/subsystems/core.md +++ b/docs/subsystems/core.md @@ -510,6 +510,12 @@ async list(): Promise */ async resolve(id?: string): Promise +/** 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 + /** Bind an unpublished Agent to the current preset revision. * @param ctx Agent context from its setup callback. * @param id Requested preset, or the default. diff --git a/docs/subsystems/core.zh.md b/docs/subsystems/core.zh.md index b18ff5bba1..0b85519b91 100644 --- a/docs/subsystems/core.zh.md +++ b/docs/subsystems/core.zh.md @@ -520,6 +520,12 @@ async list(): Promise */ async resolve(id?: string): Promise +/** 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 + /** Bind an unpublished Agent to the current preset revision. * @param ctx Agent context from its setup callback. * @param id Requested preset, or the default. diff --git a/packages/client/ui-agent-preset/README.i18n.yaml b/packages/client/ui-agent-preset/README.i18n.yaml index 961b1712a0..fde3410d83 100644 --- a/packages/client/ui-agent-preset/README.i18n.yaml +++ b/packages/client/ui-agent-preset/README.i18n.yaml @@ -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 diff --git a/packages/client/ui-agent-preset/README.md b/packages/client/ui-agent-preset/README.md index 176957e326..3f50449078 100644 --- a/packages/client/ui-agent-preset/README.md +++ b/packages/client/ui-agent-preset/README.md @@ -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 ## 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
Implementation internals — click to expand -`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.
@@ -61,7 +61,7 @@ Selection changes affect only later tasks; existing plugins and prompts remain u -- 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. ### Dev Note diff --git a/packages/client/ui-agent-preset/README.zh.md b/packages/client/ui-agent-preset/README.zh.md index a9496780ce..8753520ecc 100644 --- a/packages/client/ui-agent-preset/README.zh.md +++ b/packages/client/ui-agent-preset/README.zh.md @@ -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" ## 使用此包 -设置页显示内置与自定义卡片分组、默认项高亮和点击卡片选择;没有 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"
实现细节 — 点击展开 -`agentPresets/list` 提供列表和选择器策略;默认值与可见性的修改写入 `agent-presets` settings 命名空间。选择器、空白会话同步和只读会话标签使用记录的 preset 标识。连接重置和设置更新会刷新列表。 +`agentPresets/list` 提供列表和选择器策略,`agentPresets/read` 为查看器提供一条声明的 YAML;默认值与可见性的修改写入 `agent-presets` settings 命名空间。选择器、空白会话同步和只读会话标签使用记录的 preset 标识。连接重置和设置更新会刷新列表。
@@ -61,7 +61,7 @@ kind: "package-reference" -- Web 不创建也不编辑 preset:通过创造模式安装的 bundle 声明新 preset,或按行 id 覆盖内置 preset,覆盖会替换整个子插件列表。 +- Web 不创建也不编辑 preset:配置查看器是只读的;通过创造模式安装的 bundle 声明新 preset,或按行 id 覆盖内置 preset,覆盖会替换整个子插件列表。 ### 开发备注 diff --git a/packages/client/ui-agent-preset/src/client/AgentPresetSection.module.css b/packages/client/ui-agent-preset/src/client/AgentPresetSection.module.css index 33827b02c4..f801fe90cd 100644 --- a/packages/client/ui-agent-preset/src/client/AgentPresetSection.module.css +++ b/packages/client/ui-agent-preset/src/client/AgentPresetSection.module.css @@ -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; diff --git a/packages/client/ui-agent-preset/src/client/AgentPresetSection.tsx b/packages/client/ui-agent-preset/src/client/AgentPresetSection.tsx index 6650be7d9e..c7e6ee3dc4 100644 --- a/packages/client/ui-agent-preset/src/client/AgentPresetSection.tsx +++ b/packages/client/ui-agent-preset/src/client/AgentPresetSection.tsx @@ -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 + /** Open one preset's declared composition in the read-only viewer. */ + view: (id: string) => Promise + /** Close the read-only viewer. */ + closeView: () => void makeDefault: (id: string) => Promise setPickerVisible: (visible: boolean) => Promise } @@ -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({ {row.broken === undefined ? null : {row.broken}} - {help === undefined ? null :
-
+
+ {help === undefined ? null :
-
-
} +
} + {/* 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. */} + +
})} } @@ -153,5 +170,10 @@ export function AgentPresetSection({ })} {guide === null ? null : { setGuide(null) }} />} + {t('close')}}> + {viewed === null ? null :
{viewed.content}
} +
} diff --git a/packages/client/ui-agent-preset/src/client/index.ts b/packages/client/ui-agent-preset/src/client/index.ts index 9121d5b07d..a253ac3888 100644 --- a/packages/client/ui-agent-preset/src/client/index.ts +++ b/packages/client/ui-agent-preset/src/client/index.ts @@ -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()), diff --git a/packages/client/ui-agent-preset/src/client/locales.ts b/packages/client/ui-agent-preset/src/client/locales.ts index 4137600704..0577e07443 100644 --- a/packages/client/ui-agent-preset/src/client/locales.ts +++ b/packages/client/ui-agent-preset/src/client/locales.ts @@ -12,6 +12,7 @@ export type AgentPresetSettingsKey = | 'nav' | 'sectionIntro' | 'setDefault' + | 'view' | 'presetStandardName' | 'presetStandardDescription' | 'presetPtcName' @@ -44,6 +45,7 @@ export const en: Record = { nav: 'Agent presets', setDefault: 'Set as new task default', + view: 'View configuration', presetStandardName: 'Standard mode', presetStandardDescription: @@ -89,6 +91,7 @@ export const zh: Record = { nav: 'Agent 预设', setDefault: '设为新任务默认', + view: '查看配置', presetStandardName: '标准模式', presetStandardDescription: '处理代码、文件和资料,适合大多数任务。Agent 会按需使用检索、编辑和终端等工具。', 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 334a134d87..9f4a8ecc97 100644 --- a/packages/client/ui-agent-preset/src/client/section-store.ts +++ b/packages/client/ui-agent-preset/src/client/section-store.ts @@ -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 = createSnapshotStore(INITIAL) private loading: Promise | 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 { + 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. diff --git a/packages/client/ui-agent-preset/tests/apply.client.spec.ts b/packages/client/ui-agent-preset/tests/apply.client.spec.ts index 23e75612d0..ed6e9698a5 100644 --- a/packages/client/ui-agent-preset/tests/apply.client.spec.ts +++ b/packages/client/ui-agent-preset/tests/apply.client.spec.ts @@ -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 () => { diff --git a/packages/client/ui-agent-preset/tests/section-store.client.spec.ts b/packages/client/ui-agent-preset/tests/section-store.client.spec.ts index 1a708fd4ec..2441ea606a 100644 --- a/packages/client/ui-agent-preset/tests/section-store.client.spec.ts +++ b/packages/client/ui-agent-preset/tests/section-store.client.spec.ts @@ -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) diff --git a/packages/client/ui-agent-preset/tests/section.client.spec.tsx b/packages/client/ui-agent-preset/tests/section.client.spec.tsx index 9add87de92..44461a0cbe 100644 --- a/packages/client/ui-agent-preset/tests/section.client.spec.tsx +++ b/packages/client/ui-agent-preset/tests/section.client.spec.tsx @@ -13,8 +13,9 @@ function unusedHook(): never { } function view(partial: Partial = {}, startCreatorDraft?: () => void, developerTools = true) { const store = createSnapshotStore({ 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']) diff --git a/packages/extensions/tool-cordis/src/api-catalog.ts b/packages/extensions/tool-cordis/src/api-catalog.ts index f8edccaf67..9abb48eb32 100644 --- a/packages/extensions/tool-cordis/src/api-catalog.ts +++ b/packages/extensions/tool-cordis/src/api-catalog.ts @@ -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', + 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', 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}', diff --git a/packages/preset/agent-preset-registry/README.i18n.yaml b/packages/preset/agent-preset-registry/README.i18n.yaml index 93f84a50f9..a7792a0e26 100644 --- a/packages/preset/agent-preset-registry/README.i18n.yaml +++ b/packages/preset/agent-preset-registry/README.i18n.yaml @@ -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 diff --git a/packages/preset/agent-preset-registry/README.md b/packages/preset/agent-preset-registry/README.md index 7a1ee3e733..44b01be1ce 100644 --- a/packages/preset/agent-preset-registry/README.md +++ b/packages/preset/agent-preset-registry/README.md @@ -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. ## Understand the implementation diff --git a/packages/preset/agent-preset-registry/README.zh.md b/packages/preset/agent-preset-registry/README.zh.md index 11206a3f92..a0c75fa50b 100644 --- a/packages/preset/agent-preset-registry/README.zh.md +++ b/packages/preset/agent-preset-registry/README.zh.md @@ -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。 ## 理解实现 diff --git a/packages/preset/agent-preset-registry/package.json b/packages/preset/agent-preset-registry/package.json index 71b0d8865a..f442537386 100644 --- a/packages/preset/agent-preset-registry/package.json +++ b/packages/preset/agent-preset-registry/package.json @@ -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" } } diff --git a/packages/preset/agent-preset-registry/src/index.ts b/packages/preset/agent-preset-registry/src/index.ts index d8f54dddc3..f1366ff0d9 100644 --- a/packages/preset/agent-preset-registry/src/index.ts +++ b/packages/preset/agent-preset-registry/src/index.ts @@ -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 { + 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 { const wanted = id ?? this.defaultId while (true) { diff --git a/packages/preset/agent-preset-registry/src/types.ts b/packages/preset/agent-preset-registry/src/types.ts index 4a73dc09b9..eba2d3dce3 100644 --- a/packages/preset/agent-preset-registry/src/types.ts +++ b/packages/preset/agent-preset-registry/src/types.ts @@ -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. */ diff --git a/packages/preset/agent-preset-registry/tests/registry.spec.ts b/packages/preset/agent-preset-registry/tests/registry.spec.ts index 778fa2f834..a5481db4be 100644 --- a/packages/preset/agent-preset-registry/tests/registry.spec.ts +++ b/packages/preset/agent-preset-registry/tests/registry.spec.ts @@ -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 () => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4bb1a3a0da..475d32fe58 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: