feat(web): localize shipped agent presets
This commit is contained in:
@@ -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: 0f1daeaa6014d4c3c88e6a69ff90cf1ecacdbaf7
|
||||
README.zh.md: 08e25d9e98b83a94a434248bb3dff60da1cc31ba
|
||||
README.md: c4c7df4e6fbe0479cac4767247c1b10fd65aad77
|
||||
README.zh.md: 84c02977ec18f89c06311b570428f92c2b459fb3
|
||||
@@ -26,6 +26,8 @@ Options and the current default both come from one `agentPreset.list` call. The
|
||||
|
||||
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.
|
||||
|
||||
Preset files publish one unlocalized `name` and `description`, which Web uses for every `user` row and unknown `system` row. For the four shipped ids (`standard`, `code`, `minimal`, and `cordis`), Web resolves both fields from its active locale only when the roster marks the row `system`; an identically named `user` preset keeps its file metadata.
|
||||
|
||||
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.
|
||||
|
||||
## The management section
|
||||
|
||||
@@ -26,6 +26,8 @@ chip 以部署默认值打开,其选择是**暂存**的——该界面先于
|
||||
|
||||
本地创作的 preset 的权限恰好等于它所引用的插件,因此列表会标注 `user` 行,而不是把每个 preset 都呈现为随附且已审核的。
|
||||
|
||||
preset 文件提供一套未国际化的 `name` 与 `description`,Web 将其用于所有 `user` 行和未知的 `system` 行。对于四个随附 id(`standard`、`code`、`minimal` 与 `cordis`),只有名单将该行标记为 `system` 时,Web 才会从当前 locale 解析这两个字段;同名的 `user` preset 仍使用其文件元数据。
|
||||
|
||||
本行在自身命名空间的 `settings/changed` 以及 `connection/reset` 时重新读取:名单是一个活动目录,默认值是一项设置,外部编辑与重新连接都可能改变它。
|
||||
|
||||
## 管理分区
|
||||
|
||||
@@ -15,6 +15,7 @@ import { IconThinkOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
// Type-only: pulls the ui-conversation SlotMap merge (the header actions).
|
||||
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
import type { AgentPresetSettingsState } from './settings-store.ts'
|
||||
import { presetDisplayText } from './locales.ts'
|
||||
import css from './AgentPresetLabel.module.css'
|
||||
|
||||
/** Registration-side business face for the header label. */
|
||||
@@ -53,10 +54,11 @@ export function AgentPresetLabel({
|
||||
if (preset === undefined) return null
|
||||
|
||||
const option = options.find(entry => entry.id === preset)
|
||||
const text = option === undefined ? undefined : presetDisplayText(option, t)
|
||||
return (
|
||||
<span className={css.label} title={option?.description ?? t('headerHint')}>
|
||||
<span className={css.label} title={text?.description ?? t('headerHint')}>
|
||||
<IconThinkOutline16 className={css.icon} />
|
||||
{option?.name ?? preset}
|
||||
{text?.name ?? preset}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -8,7 +8,7 @@ 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 type { AgentPresetSettingsState } from './settings-store.ts'
|
||||
import type { AgentPresetSettingsKey } from './locales.ts'
|
||||
import { presetDisplayText, type AgentPresetSettingsKey } from './locales.ts'
|
||||
import { PresetMenu } from './PresetMenu.tsx'
|
||||
import css from './AgentPresetRow.module.css'
|
||||
|
||||
@@ -52,11 +52,11 @@ export function AgentPresetRow({ load, select, useAgentPreset, t }: AgentPresetR
|
||||
// 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'
|
||||
// The metadata name is what every other surface shows — the id is the
|
||||
// addressing, not the label. A preset that names itself nothing falls back
|
||||
// to its id, which is then all there is to say about it.
|
||||
// Every preset surface applies the same display-copy rule. The id remains
|
||||
// addressing rather than a label, except where no display name exists.
|
||||
const chosen = state.options.find(option => option.id === state.currentValue)
|
||||
const label = state.currentValue === '' ? t('loading') : (chosen?.name ?? state.currentValue)
|
||||
const chosenText = chosen === undefined ? undefined : presetDisplayText(chosen, t)
|
||||
const label = state.currentValue === '' ? t('loading') : (chosenText?.name ?? state.currentValue)
|
||||
const description: string = state.error ?? t('description')
|
||||
|
||||
return (
|
||||
@@ -69,7 +69,7 @@ export function AgentPresetRow({ load, select, useAgentPreset, t }: AgentPresetR
|
||||
options={state.options}
|
||||
selectedId={state.currentValue}
|
||||
label={label}
|
||||
userTrustLabel={t('userTrust')}
|
||||
t={t}
|
||||
buttonClassName={css.selector}
|
||||
chevronClassName={css.chevron}
|
||||
disabled={busy || !state.writable || state.options.length === 0}
|
||||
|
||||
@@ -19,6 +19,7 @@ import { IconChevronDownOutline14, IconThinkOutline16, Menu } from '@deepseek-ai
|
||||
// Type-only: pulls the ui-conversation SlotMap merge (the hero seat).
|
||||
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
import type { AgentPresetSeatState } from './seat-store.ts'
|
||||
import { presetDisplayText } from './locales.ts'
|
||||
import css from './AgentPresetSeat.module.css'
|
||||
|
||||
/** Registration-side business face for the hero chip. */
|
||||
@@ -57,22 +58,26 @@ export function AgentPresetSeat({ load, select, useAgentPresetSeat, t }: AgentPr
|
||||
if (state.options.length === 0 || state.current === '') return null
|
||||
|
||||
const chosen = state.options.find(option => option.id === state.current)
|
||||
const chosenText = chosen === undefined ? undefined : presetDisplayText(chosen, t)
|
||||
|
||||
return (
|
||||
<Menu
|
||||
open={open}
|
||||
onClose={() => { setOpen(false) }}
|
||||
items={state.options.map(option => ({
|
||||
id: option.id,
|
||||
// Name and description together: the id alone never said what a
|
||||
// preset does, which is the whole reason the metadata exists.
|
||||
label: (
|
||||
<span className={css.item}>
|
||||
<span className={css.itemName}>{option.name ?? option.id}</span>
|
||||
<span className={css.itemDesc}>{option.description ?? t('noDescription')}</span>
|
||||
</span>
|
||||
),
|
||||
}))}
|
||||
items={state.options.map((option) => {
|
||||
const text = presetDisplayText(option, t)
|
||||
return {
|
||||
id: option.id,
|
||||
// Name and description together: the id alone never says what a
|
||||
// preset does, which is why the roster carries display copy.
|
||||
label: (
|
||||
<span className={css.item}>
|
||||
<span className={css.itemName}>{text.name}</span>
|
||||
<span className={css.itemDesc}>{text.description ?? t('noDescription')}</span>
|
||||
</span>
|
||||
),
|
||||
}
|
||||
})}
|
||||
selectedId={state.current}
|
||||
onSelect={(id) => {
|
||||
setOpen(false)
|
||||
@@ -91,7 +96,7 @@ export function AgentPresetSeat({ load, select, useAgentPresetSeat, t }: AgentPr
|
||||
onClick={() => { setOpen(value => !value) }}
|
||||
>
|
||||
<IconThinkOutline16 className={css.seatIcon} />
|
||||
{chosen?.name ?? state.current}
|
||||
{chosenText?.name ?? state.current}
|
||||
<IconChevronDownOutline14 className={css.chevron} />
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
import type { SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import { draftBlocker, type AgentPresetSectionState } from './section-store.ts'
|
||||
import type { AgentPresetSettingsKey } from './locales.ts'
|
||||
import { presetDisplayText, type AgentPresetSettingsKey } from './locales.ts'
|
||||
import css from './AgentPresetSection.module.css'
|
||||
|
||||
/** Registration-side business face for the management section. */
|
||||
@@ -77,11 +77,13 @@ function CopyDialog({ state, t, actions }: CopyDialogProps): ReactNode {
|
||||
const draft = state.copy
|
||||
const blocker = draft === null ? undefined : draftBlocker(draft, state.rows)
|
||||
const message = draft === null ? null : draft.error ?? (blocker === undefined ? null : t(blocker))
|
||||
const source = draft === null ? undefined : state.rows.find(row => row.id === draft.from)
|
||||
const sourceTitle = source === undefined ? draft?.fromTitle : presetDisplayText(source, t).name
|
||||
return (
|
||||
<Modal
|
||||
open={draft !== null}
|
||||
onClose={() => { actions.cancelCopy() }}
|
||||
title={draft === null ? t('copyTitle') : `${t('copyTitle')} · ${t('copyOf')} ${draft.fromTitle}`}
|
||||
title={draft === null ? t('copyTitle') : `${t('copyTitle')} · ${t('copyOf')} ${sourceTitle}`}
|
||||
closeLabel={t('close')}
|
||||
description={t('copyIntro')}
|
||||
className={css.dialog as string}
|
||||
@@ -143,6 +145,11 @@ function CopyDialog({ state, t, actions }: CopyDialogProps): ReactNode {
|
||||
export function AgentPresetSection(props: AgentPresetSectionProps): ReactNode {
|
||||
const { useAgentPresetSection, t, load } = props
|
||||
const state = useAgentPresetSection(snapshot => snapshot)
|
||||
const viewedId = state.view?.id
|
||||
const viewedRow = viewedId === undefined ? undefined : state.rows.find(row => row.id === viewedId)
|
||||
const viewedTitle = state.view === null
|
||||
? ''
|
||||
: viewedRow === undefined ? state.view.title : presetDisplayText(viewedRow, t).name
|
||||
|
||||
useEffect(() => {
|
||||
void load()
|
||||
@@ -170,13 +177,15 @@ export function AgentPresetSection(props: AgentPresetSectionProps): ReactNode {
|
||||
<p className={css.intro}>{t('sectionIntro')}</p>
|
||||
{state.error === null ? null : <p className={css.error} role="alert">{state.error}</p>}
|
||||
{([['system', t('builtInGroup')], ['user', t('customGroup')]] as const).map(([trust, heading]) => {
|
||||
const group = state.rows.filter(row => row.trust === trust)
|
||||
const group = state.rows
|
||||
.filter(row => row.trust === trust)
|
||||
.map(row => ({ row, text: presetDisplayText(row, t) }))
|
||||
if (group.length === 0) return null
|
||||
return (
|
||||
<section key={trust} className={css.group}>
|
||||
<h3 className={css.groupHead}>{heading}</h3>
|
||||
<ul className={css.cards}>
|
||||
{group.map(row => (
|
||||
{group.map(({ row, text }) => (
|
||||
<li
|
||||
key={row.id}
|
||||
className={row.broken !== undefined
|
||||
@@ -196,12 +205,12 @@ export function AgentPresetSection(props: AgentPresetSectionProps): ReactNode {
|
||||
disabled={row.isDefault || row.broken !== undefined}
|
||||
// Without this the name is the whole card read aloud —
|
||||
// title, badge, description, id.
|
||||
aria-label={`${row.broken !== undefined ? t('brokenBadge') : row.isDefault ? t('inUse') : t('setDefault')}: ${row.name ?? row.id}`}
|
||||
aria-label={`${row.broken !== undefined ? t('brokenBadge') : row.isDefault ? t('inUse') : t('setDefault')}: ${text.name}`}
|
||||
title={row.broken ?? (row.isDefault ? t('inUse') : t('setDefault'))}
|
||||
onClick={() => { void props.makeDefault(row.id) }}
|
||||
>
|
||||
<span className={css.cardHead}>
|
||||
<span className={css.cardName}>{row.name ?? row.id}</span>
|
||||
<span className={css.cardName}>{text.name}</span>
|
||||
{row.broken !== undefined
|
||||
? <span className={css.brokenBadge}>{t('brokenBadge')}</span>
|
||||
: null}
|
||||
@@ -210,7 +219,7 @@ export function AgentPresetSection(props: AgentPresetSectionProps): ReactNode {
|
||||
</span>
|
||||
{row.isDefault ? <span className={css.inUse}>{t('inUse')}</span> : null}
|
||||
</span>
|
||||
<span className={css.cardDesc}>{row.description ?? t('noDescription')}</span>
|
||||
<span className={css.cardDesc}>{text.description ?? t('noDescription')}</span>
|
||||
{row.broken === undefined
|
||||
? null
|
||||
: <span className={css.cardBrokenReason} role="alert">{row.broken}</span>}
|
||||
@@ -231,7 +240,7 @@ export function AgentPresetSection(props: AgentPresetSectionProps): ReactNode {
|
||||
type="button"
|
||||
className={css.iconButton}
|
||||
data-tip={t('view')}
|
||||
aria-label={`${t('view')}: ${row.name ?? row.id}`}
|
||||
aria-label={`${t('view')}: ${text.name}`}
|
||||
onClick={() => { void props.view(row.id) }}
|
||||
>
|
||||
<IconBrowseOutline16 />
|
||||
@@ -243,7 +252,7 @@ export function AgentPresetSection(props: AgentPresetSectionProps): ReactNode {
|
||||
type="button"
|
||||
className={css.iconButton}
|
||||
data-tip={state.hasDocument ? t('openLocation') : t('showLocation')}
|
||||
aria-label={`${state.hasDocument ? t('openLocation') : t('showLocation')}: ${row.name ?? row.id}`}
|
||||
aria-label={`${state.hasDocument ? t('openLocation') : t('showLocation')}: ${text.name}`}
|
||||
onClick={() => { void props.openLocation(row.id) }}
|
||||
>
|
||||
<IconFolderOpenOutline16 />
|
||||
@@ -256,7 +265,7 @@ export function AgentPresetSection(props: AgentPresetSectionProps): ReactNode {
|
||||
data-tip={row.broken !== undefined
|
||||
? t('brokenNoCopy')
|
||||
: state.authorable ? t('duplicate') : t('duplicateUnavailable')}
|
||||
aria-label={`${t('duplicate')}: ${row.name ?? row.id}`}
|
||||
aria-label={`${t('duplicate')}: ${text.name}`}
|
||||
onClick={() => { props.beginCopy(row.id) }}
|
||||
>
|
||||
<IconCopyOutline16 />
|
||||
@@ -267,7 +276,7 @@ export function AgentPresetSection(props: AgentPresetSectionProps): ReactNode {
|
||||
type="button"
|
||||
className={`${css.iconButton} ${css.iconDanger}`}
|
||||
data-tip={t('delete')}
|
||||
aria-label={`${t('delete')}: ${row.name ?? row.id}`}
|
||||
aria-label={`${t('delete')}: ${text.name}`}
|
||||
onClick={() => { props.confirmDelete(row.id) }}
|
||||
>
|
||||
<IconTrashOutline16 />
|
||||
@@ -325,7 +334,7 @@ export function AgentPresetSection(props: AgentPresetSectionProps): ReactNode {
|
||||
<Modal
|
||||
open={state.view !== null}
|
||||
onClose={() => { props.closeView() }}
|
||||
title={state.view === null ? '' : `${t('view')} · ${state.view.title}`}
|
||||
title={state.view === null ? '' : `${t('view')} · ${viewedTitle}`}
|
||||
closeLabel={t('close')}
|
||||
description={t('composition')}
|
||||
className={css.dialog as string}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
import { IconChevronDownOutline14, Menu } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type { AgentPresetOption } from './settings-store.ts'
|
||||
import { presetDisplayText, type AgentPresetSettingsKey } from './locales.ts'
|
||||
|
||||
/** What one surface passes to the shared picker. */
|
||||
export interface PresetMenuProps {
|
||||
@@ -20,8 +21,8 @@ export interface PresetMenuProps {
|
||||
selectedId: string
|
||||
/** Text on the button; the surfaces word a pending roster differently. */
|
||||
label: string
|
||||
/** Suffix marking a locally authored preset in the menu. */
|
||||
userTrustLabel: string
|
||||
/** Active Web locale lookup. */
|
||||
t: (key: AgentPresetSettingsKey) => string
|
||||
/** Class for the trigger button, owned by the calling surface. */
|
||||
buttonClassName: string | undefined
|
||||
/** Class for the chevron, owned by the calling surface. */
|
||||
@@ -42,22 +43,22 @@ export interface PresetMenuProps {
|
||||
* @returns the menu and its trigger.
|
||||
*/
|
||||
export function PresetMenu({
|
||||
options, selectedId, label, userTrustLabel, buttonClassName, chevronClassName,
|
||||
options, selectedId, label, t, buttonClassName, chevronClassName,
|
||||
disabled, open, onOpenChange, onSelect,
|
||||
}: PresetMenuProps) {
|
||||
return (
|
||||
<Menu
|
||||
open={open}
|
||||
onClose={() => { onOpenChange(false) }}
|
||||
items={options.map(option => ({
|
||||
id: option.id,
|
||||
// The metadata name is what every surface shows; the id is addressing,
|
||||
// not a label. A preset that names itself nothing falls back to its id,
|
||||
// which is then all there is to say about it.
|
||||
label: option.trust === 'user'
|
||||
? `${option.name ?? option.id} · ${userTrustLabel}`
|
||||
: option.name ?? option.id,
|
||||
}))}
|
||||
items={options.map((option) => {
|
||||
const name = presetDisplayText(option, t).name
|
||||
return {
|
||||
id: option.id,
|
||||
// All preset surfaces resolve copy the same way; the id is addressing,
|
||||
// not a label, except where no display name exists.
|
||||
label: option.trust === 'user' ? `${name} · ${t('userTrust')}` : name,
|
||||
}
|
||||
})}
|
||||
selectedId={selectedId}
|
||||
onSelect={(id) => {
|
||||
onOpenChange(false)
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
export type AgentPresetSettingsKey =
|
||||
| 'title' | 'description' | 'loading' | 'error' | 'userTrust' | 'seatHint' | 'headerHint'
|
||||
| 'nav' | 'sectionIntro' | 'builtIn' | 'setDefault' | 'view'
|
||||
| 'presetStandardName' | 'presetStandardDescription'
|
||||
| 'presetCodeName' | 'presetCodeDescription'
|
||||
| 'presetMinimalName' | 'presetMinimalDescription'
|
||||
| 'presetCordisName' | 'presetCordisDescription'
|
||||
| 'duplicate' | 'duplicateUnavailable' | 'delete' | 'presetId' | 'presetIdPlaceholder' | 'copyOf'
|
||||
| 'displayName' | 'displayNamePlaceholder'
|
||||
| 'inUse' | 'noDescription' | 'builtInGroup' | 'customGroup'
|
||||
@@ -30,6 +34,17 @@ export const en: Record<AgentPresetSettingsKey, string> = {
|
||||
builtIn: 'Built-in',
|
||||
setDefault: 'Set as default',
|
||||
view: 'View',
|
||||
presetStandardName: 'Standard mode',
|
||||
presetStandardDescription: 'Full coding agent with file editing, shell, search, planning, delegation, and workflows.',
|
||||
presetCodeName: 'Code mode',
|
||||
presetCodeDescription:
|
||||
'Presents Standard mode\'s tools through Code Mode: the model writes TypeScript against an SDK and runs it once instead of making multiple tool calls.',
|
||||
presetMinimalName: 'Minimal mode',
|
||||
presetMinimalDescription:
|
||||
'Exposes only bash and str_replace_editor to the model, for benchmarks and minimal reproductions.',
|
||||
presetCordisName: 'Creator mode',
|
||||
presetCordisDescription:
|
||||
'Adds self-inspection tools to Standard mode, so it can read and modify its own running composition and create new presets from it.',
|
||||
duplicate: 'Duplicate',
|
||||
duplicateUnavailable: 'This deployment has no writable preset directory',
|
||||
delete: 'Delete',
|
||||
@@ -82,6 +97,14 @@ export const zh: Record<AgentPresetSettingsKey, string> = {
|
||||
builtIn: '内置',
|
||||
setDefault: '设为默认',
|
||||
view: '查看',
|
||||
presetStandardName: '标准模式',
|
||||
presetStandardDescription: '完整的编码 agent:文件读写、shell、检索、计划、委派与工作流。',
|
||||
presetCodeName: '代码模式',
|
||||
presetCodeDescription: '标准模式的工具改为 Code Mode 呈现:模型写一段 TypeScript 调用 SDK,一次执行代替多轮工具调用。',
|
||||
presetMinimalName: '极简模式',
|
||||
presetMinimalDescription: '只向模型呈现 bash 与 str_replace_editor,适合 benchmark 与最小复现。',
|
||||
presetCordisName: '创造模式',
|
||||
presetCordisDescription: '标准模式加上自指工具集,可以读改自己运行的这套组装,并据此创作新的预设。',
|
||||
duplicate: '复制',
|
||||
duplicateUnavailable: '此部署未配置可写的预设目录',
|
||||
delete: '删除',
|
||||
@@ -116,3 +139,53 @@ export const zh: Record<AgentPresetSettingsKey, string> = {
|
||||
deleteConfirm: '删除',
|
||||
deleting: '正在删除…',
|
||||
}
|
||||
|
||||
/** Preset roster fields needed to resolve Web display copy. */
|
||||
export interface PresetDisplaySource {
|
||||
/** Stable preset id. */
|
||||
readonly id: string
|
||||
/** Whether the deployment ships the preset or the user owns it. */
|
||||
readonly trust: 'system' | 'user'
|
||||
/** Unlocalized name published by the preset. */
|
||||
readonly name?: string
|
||||
/** Unlocalized description published by the preset. */
|
||||
readonly description?: string
|
||||
}
|
||||
|
||||
/** Display copy resolved for the active Web locale. */
|
||||
export interface PresetDisplayText {
|
||||
/** Localized built-in name or the preset's own fallback name. */
|
||||
readonly name: string
|
||||
/** Localized built-in description or the preset's own description. */
|
||||
readonly description?: string
|
||||
}
|
||||
|
||||
interface PresetLocaleKeys {
|
||||
readonly name: AgentPresetSettingsKey
|
||||
readonly description: AgentPresetSettingsKey
|
||||
}
|
||||
|
||||
const BUILT_IN_PRESET_KEYS: Readonly<Partial<Record<string, PresetLocaleKeys>>> = {
|
||||
standard: { name: 'presetStandardName', description: 'presetStandardDescription' },
|
||||
code: { name: 'presetCodeName', description: 'presetCodeDescription' },
|
||||
minimal: { name: 'presetMinimalName', description: 'presetMinimalDescription' },
|
||||
cordis: { name: 'presetCordisName', description: 'presetCordisDescription' },
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve preset display copy without making user-authored metadata translatable.
|
||||
* @param preset - roster row whose copy is being rendered.
|
||||
* @param t - active Web locale lookup.
|
||||
* @returns localized copy for a known shipped preset, otherwise file metadata.
|
||||
*/
|
||||
export function presetDisplayText(
|
||||
preset: PresetDisplaySource,
|
||||
t: (key: AgentPresetSettingsKey) => string,
|
||||
): PresetDisplayText {
|
||||
const keys = preset.trust === 'system' ? BUILT_IN_PRESET_KEYS[preset.id] : undefined
|
||||
if (keys !== undefined) return { name: t(keys.name), description: t(keys.description) }
|
||||
return {
|
||||
name: preset.name ?? preset.id,
|
||||
...preset.description === undefined ? {} : { description: preset.description },
|
||||
}
|
||||
}
|
||||
@@ -90,7 +90,7 @@ describe('the General-settings row', () => {
|
||||
const actions = renderRow()
|
||||
|
||||
await waitFor(() => { expect(actions.load).toHaveBeenCalledTimes(1) })
|
||||
expect(screen.getByRole('button').textContent).toContain('标准模式')
|
||||
expect(screen.getByRole('button').textContent).toContain(en.presetStandardName)
|
||||
})
|
||||
|
||||
it('marks a locally authored option as local', () => {
|
||||
@@ -102,7 +102,7 @@ describe('the General-settings row', () => {
|
||||
// list says which rows are local rather than presenting all as vetted.
|
||||
expect(screen.getByText(`mine · ${en.userTrust}`)).toBeTruthy()
|
||||
// The shipped one carries no marker; only local rows are called out.
|
||||
expect(screen.getAllByText('标准模式')).toHaveLength(2)
|
||||
expect(screen.getAllByText(en.presetStandardName)).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('falls back to the id for a preset that published no name', () => {
|
||||
@@ -128,6 +128,12 @@ describe('the General-settings row', () => {
|
||||
expect(screen.getByText('bare')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('shows the selected id until a stale roster contains it', () => {
|
||||
renderRow({ currentValue: 'arriving', options: [] })
|
||||
|
||||
expect(screen.getByRole('button').textContent).toContain('arriving')
|
||||
})
|
||||
|
||||
it('writes the picked preset and closes the menu', () => {
|
||||
const actions = renderRow()
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
@@ -194,7 +200,7 @@ describe('the new-session chip', () => {
|
||||
const actions = renderSeat()
|
||||
|
||||
await waitFor(() => { expect(actions.load).toHaveBeenCalledTimes(1) })
|
||||
expect(screen.getByRole('button').textContent).toContain('标准模式')
|
||||
expect(screen.getByRole('button').textContent).toContain(en.presetStandardName)
|
||||
expect(screen.getByRole('button').getAttribute('title')).toBe(en.seatHint)
|
||||
})
|
||||
|
||||
@@ -205,7 +211,7 @@ describe('the new-session chip', () => {
|
||||
|
||||
// The id alone never said what a preset does; the description is the
|
||||
// whole reason a preset can publish metadata at all.
|
||||
expect(screen.getByText('完整的编码 agent。')).toBeTruthy()
|
||||
expect(screen.getByText(en.presetStandardDescription)).toBeTruthy()
|
||||
// A preset that published none still reads as a row, with its id standing
|
||||
// in for the name.
|
||||
expect(screen.getByText(en.noDescription)).toBeTruthy()
|
||||
@@ -218,6 +224,12 @@ describe('the new-session chip', () => {
|
||||
expect(screen.getByRole('button').textContent).toContain('mine')
|
||||
})
|
||||
|
||||
it('shows the staged id until a stale roster contains it', () => {
|
||||
renderSeat({ current: 'arriving' })
|
||||
|
||||
expect(screen.getByRole('button').textContent).toContain('arriving')
|
||||
})
|
||||
|
||||
it('stages the picked preset and closes the menu', () => {
|
||||
const actions = renderSeat()
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
@@ -267,7 +279,7 @@ describe('the session-header label', () => {
|
||||
await waitFor(() => { expect(load).toHaveBeenCalledTimes(1) })
|
||||
// A control here would promise a switch the host refuses outright.
|
||||
expect(screen.queryByRole('button')).toBeNull()
|
||||
expect(screen.getByTitle('完整的编码 agent。').textContent).toBe('标准模式')
|
||||
expect(screen.getByTitle(en.presetStandardDescription).textContent).toBe(en.presetStandardName)
|
||||
})
|
||||
|
||||
it('falls back to the id, and to the generic hint, when metadata is absent', () => {
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/** Web-localized copy for the four shipped presets and file copy for every other row. */
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { en, presetDisplayText, zh } from '../src/client/locales.ts'
|
||||
|
||||
const translate = (bundle: typeof en) => (key: keyof typeof en): string => bundle[key]
|
||||
|
||||
describe('preset display copy', () => {
|
||||
it.each([
|
||||
['standard', 'presetStandardName', 'presetStandardDescription'],
|
||||
['code', 'presetCodeName', 'presetCodeDescription'],
|
||||
['minimal', 'presetMinimalName', 'presetMinimalDescription'],
|
||||
['cordis', 'presetCordisName', 'presetCordisDescription'],
|
||||
] as const)('localizes the shipped %s preset in English and Chinese', (id, nameKey, descriptionKey) => {
|
||||
const preset = { id, trust: 'system' as const, name: 'file name', description: 'file description' }
|
||||
|
||||
expect(presetDisplayText(preset, translate(en)))
|
||||
.toEqual({ name: en[nameKey], description: en[descriptionKey] })
|
||||
expect(presetDisplayText(preset, translate(zh)))
|
||||
.toEqual({ name: zh[nameKey], description: zh[descriptionKey] })
|
||||
})
|
||||
|
||||
it('keeps file metadata for user and unknown system presets', () => {
|
||||
const fileCopy = { name: '我的标准', description: '团队自己的 preset。' }
|
||||
|
||||
expect(presetDisplayText({ id: 'standard', trust: 'user', ...fileCopy }, translate(en)))
|
||||
.toEqual(fileCopy)
|
||||
expect(presetDisplayText({ id: 'deployment-extra', trust: 'system', ...fileCopy }, translate(en)))
|
||||
.toEqual(fileCopy)
|
||||
expect(presetDisplayText({ id: 'bare', trust: 'user' }, translate(en)))
|
||||
.toEqual({ name: 'bare' })
|
||||
})
|
||||
})
|
||||
@@ -85,13 +85,13 @@ describe('the preset list', () => {
|
||||
await waitFor(() => { expect(actions.load).toHaveBeenCalledTimes(1) })
|
||||
})
|
||||
|
||||
it('shows the published name and description, falling back to the id', () => {
|
||||
it('shows resolved copy for built-ins and falls back to custom ids', () => {
|
||||
renderSection()
|
||||
|
||||
// The name is what a picker reads; the id stays visible as the key the
|
||||
// Display copy is what a picker reads; the id stays visible as the key the
|
||||
// composition and the session header actually carry.
|
||||
expect(screen.getByText('标准模式')).toBeTruthy()
|
||||
expect(screen.getByText('完整的编码 agent。')).toBeTruthy()
|
||||
expect(screen.getByText(en.presetStandardName)).toBeTruthy()
|
||||
expect(screen.getByText(en.presetStandardDescription)).toBeTruthy()
|
||||
const mine = rowFor('mine')
|
||||
expect(within(mine).getAllByText('mine').length).toBeGreaterThan(0)
|
||||
expect(within(mine).getByText(en.noDescription)).toBeTruthy()
|
||||
@@ -134,7 +134,7 @@ describe('the preset list', () => {
|
||||
it('picks a preset by clicking its card, and the one in use is inert', () => {
|
||||
const actions = renderSection()
|
||||
|
||||
const inUse = within(rowFor('standard')).getByRole('button', { name: `${en.inUse}: 标准模式` })
|
||||
const inUse = within(rowFor('standard')).getByRole('button', { name: `${en.inUse}: ${en.presetStandardName}` })
|
||||
expect(inUse).toHaveProperty('disabled', true)
|
||||
fireEvent.click(inUse)
|
||||
|
||||
@@ -150,8 +150,8 @@ describe('the preset list', () => {
|
||||
// the point. A custom preset is edited in its files, so its row leads
|
||||
// there instead; there is no editor for either.
|
||||
const standard = rowFor('standard')
|
||||
expect(within(standard).getByRole('button', { name: `${en.view}: 标准模式` })).toBeTruthy()
|
||||
expect(within(standard).queryByRole('button', { name: `${en.openLocation}: 标准模式` })).toBeNull()
|
||||
expect(within(standard).getByRole('button', { name: `${en.view}: ${en.presetStandardName}` })).toBeTruthy()
|
||||
expect(within(standard).queryByRole('button', { name: `${en.openLocation}: ${en.presetStandardName}` })).toBeNull()
|
||||
const mine = rowFor('mine')
|
||||
expect(within(mine).getByRole('button', { name: `${en.openLocation}: mine` })).toBeTruthy()
|
||||
expect(within(mine).queryByRole('button', { name: `${en.view}: mine` })).toBeNull()
|
||||
@@ -161,13 +161,13 @@ describe('the preset list', () => {
|
||||
renderSection()
|
||||
|
||||
expect(within(rowFor('mine')).getByRole('button', { name: `${en.delete}: mine` })).toBeTruthy()
|
||||
expect(within(rowFor('standard')).queryByRole('button', { name: `${en.delete}: 标准模式` })).toBeNull()
|
||||
expect(within(rowFor('standard')).queryByRole('button', { name: `${en.delete}: ${en.presetStandardName}` })).toBeNull()
|
||||
})
|
||||
|
||||
it('disables duplication when nothing is writable, and says why', () => {
|
||||
renderSection({ authorable: false })
|
||||
|
||||
const duplicate = within(rowFor('standard')).getByRole('button', { name: `${en.duplicate}: 标准模式` })
|
||||
const duplicate = within(rowFor('standard')).getByRole('button', { name: `${en.duplicate}: ${en.presetStandardName}` })
|
||||
expect(duplicate).toHaveProperty('disabled', true)
|
||||
expect(duplicate.getAttribute('data-tip')).toBe(en.duplicateUnavailable)
|
||||
})
|
||||
@@ -205,7 +205,7 @@ describe('the preset list', () => {
|
||||
// There is no readable composition to offer; the reason on the card is
|
||||
// the whole story a shipped row can tell.
|
||||
const standard = rowFor('standard')
|
||||
expect(within(standard).queryByRole('button', { name: `${en.view}: 标准模式` })).toBeNull()
|
||||
expect(within(standard).queryByRole('button', { name: `${en.view}: ${en.presetStandardName}` })).toBeNull()
|
||||
expect(within(standard).getByRole('alert').textContent).toContain('not valid YAML')
|
||||
})
|
||||
|
||||
@@ -232,7 +232,7 @@ describe('the preset list', () => {
|
||||
fireEvent.click(within(rowFor('mine')).getByRole('button', { name: `${en.setDefault}: mine` }))
|
||||
fireEvent.click(within(rowFor('mine')).getByRole('button', { name: `${en.openLocation}: mine` }))
|
||||
fireEvent.click(within(rowFor('mine')).getByRole('button', { name: `${en.duplicate}: mine` }))
|
||||
fireEvent.click(within(rowFor('standard')).getByRole('button', { name: `${en.view}: 标准模式` }))
|
||||
fireEvent.click(within(rowFor('standard')).getByRole('button', { name: `${en.view}: ${en.presetStandardName}` }))
|
||||
|
||||
expect(actions.makeDefault).toHaveBeenCalledWith('mine')
|
||||
expect(actions.openLocation).toHaveBeenCalledWith('mine')
|
||||
@@ -311,7 +311,7 @@ describe('the copy dialog', () => {
|
||||
const actions = renderSection({ copy: draft })
|
||||
|
||||
const dialog = screen.getByRole('dialog')
|
||||
expect(dialog.getAttribute('aria-label')).toBe(`${en.copyTitle} · ${en.copyOf} 标准模式`)
|
||||
expect(dialog.getAttribute('aria-label')).toBe(`${en.copyTitle} · ${en.copyOf} ${en.presetStandardName}`)
|
||||
expect(within(dialog).getByText(en.copyIntro)).toBeTruthy()
|
||||
fireEvent.change(within(dialog).getByPlaceholderText(en.presetIdPlaceholder), { target: { value: 'my-agent' } })
|
||||
fireEvent.change(within(dialog).getByPlaceholderText(en.displayNamePlaceholder), { target: { value: '我的模式' } })
|
||||
@@ -374,11 +374,17 @@ describe('the read-only viewer', () => {
|
||||
renderSection({ view: { id: 'standard', title: '标准模式', content: '- id: tool-bash\n' } })
|
||||
|
||||
const dialog = screen.getByRole('dialog')
|
||||
expect(dialog.getAttribute('aria-label')).toBe(`${en.view} · 标准模式`)
|
||||
expect(dialog.getAttribute('aria-label')).toBe(`${en.view} · ${en.presetStandardName}`)
|
||||
expect(within(dialog).getByText(en.composition)).toBeTruthy()
|
||||
expect(within(dialog).getByText(/tool-bash/).textContent).toBe('- id: tool-bash\n')
|
||||
})
|
||||
|
||||
it('keeps the loaded title when the viewed row leaves the roster', () => {
|
||||
renderSection({ view: { id: 'retired', title: 'Retired mode', content: '- id: tool-bash\n' } })
|
||||
|
||||
expect(screen.getByRole('dialog').getAttribute('aria-label')).toBe(`${en.view} · Retired mode`)
|
||||
})
|
||||
|
||||
it('closes through the controller', () => {
|
||||
const actions = renderSection({ view: { id: 'standard', title: '标准模式', content: '- id: x\n' } })
|
||||
|
||||
|
||||
Reference in New Issue
Block a user