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 8d1fcecf7e..ae296a7022 100644
--- a/packages/client/ui-agent-preset/src/client/AgentPresetSection.module.css
+++ b/packages/client/ui-agent-preset/src/client/AgentPresetSection.module.css
@@ -31,35 +31,77 @@
/* Cards, not rows: a preset is a thing you pick, and the description is the
part that tells them apart — a row would bury it beside the actions. */
+.group {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.groupHead {
+ margin: 0;
+ font-size: 12px;
+ font-weight: 600;
+ letter-spacing: .06em;
+ text-transform: uppercase;
+ color: var(--dsw-alias-label-tertiary);
+}
+
.cards {
list-style: none;
margin: 0;
padding: 0;
display: grid;
- grid-template-columns: repeat(auto-fill, minmax(232px, 1fr));
+ grid-template-columns: repeat(auto-fill, minmax(268px, 1fr));
gap: 12px;
}
.card {
border: 1px solid var(--dsw-alias-border-l2);
border-radius: 12px;
- padding: 14px 16px;
+ display: flex;
+ flex-direction: column;
+ background: var(--dsw-alias-bg-layer-3);
+ transition: border-color .16s, background .16s;
+}
+
+.card:hover:not(.cardActive) {
+ border-color: var(--dsw-alias-label-dimmed);
+}
+
+/* The default preset reads as selected, not merely badged. */
+.cardActive {
+ background: var(--dsw-alias-bg-layer-2);
+ border-color: var(--dsw-alias-label-primary);
+}
+
+/* The card body is the control that picks the preset. */
+.cardMain {
+ appearance: none;
+ border: 0;
+ background: none;
+ font: inherit;
+ color: inherit;
+ text-align: left;
+ cursor: pointer;
display: flex;
flex-direction: column;
gap: 8px;
- background: var(--dsw-alias-bg-layer-3);
+ padding: 14px 16px 12px;
+ border-radius: 12px 12px 0 0;
}
-/* The default preset is the one in use; it reads as selected rather than
- merely badged. */
-.cardActive {
- background: var(--dsw-alias-bg-layer-2);
- border-color: var(--dsw-alias-label-tertiary);
+.cardMain:disabled {
+ cursor: default;
+}
+
+.cardMain:focus-visible {
+ outline: 2px solid var(--dsw-alias-brand-primary);
+ outline-offset: -2px;
}
.cardHead {
display: flex;
- align-items: baseline;
+ align-items: center;
gap: 8px;
}
@@ -77,19 +119,12 @@
}
.cardDesc {
- margin: 0;
font-size: 13px;
line-height: 1.55;
color: var(--dsw-alias-label-secondary);
flex: 1;
}
-.cardMeta {
- display: flex;
- align-items: center;
- gap: 8px;
-}
-
.cardId {
font-family: var(--dsw-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
font-size: 11px;
@@ -98,35 +133,41 @@
.cardFoot {
display: flex;
- padding-top: 4px;
+ gap: 2px;
+ padding: 6px 10px;
border-top: 1px solid var(--dsw-alias-border-l2);
}
-.badge,
-.defaultBadge {
- border-radius: 999px;
- padding: 2px 8px;
- font-size: 11px;
- line-height: 16px;
-}
-
-.badge {
- border: 1px solid var(--dsw-alias-border-l2);
+/* Icon-only actions: the label rides `title` so the row stays quiet until
+ someone reaches for it. */
+.iconButton {
+ appearance: none;
+ border: 0;
+ border-radius: 7px;
+ padding: 6px;
+ background: none;
color: var(--dsw-alias-label-tertiary);
-}
-
-.defaultBadge {
- background: var(--dsw-alias-brand-primary);
- color: var(--dsw-alias-label-primary-foreground);
-}
-
-.rowActions {
+ cursor: pointer;
display: inline-flex;
- gap: 4px;
- flex-wrap: wrap;
align-items: center;
}
+.iconButton:hover:not(:disabled) {
+ 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;
+}
+
+.iconDanger:hover:not(:disabled) {
+ background: var(--dsw-alias-interactive-bg-hover-danger);
+ color: var(--dsw-alias-state-error-primary);
+}
+
+
.secondaryButton {
border: none;
border-radius: 7px;
@@ -138,24 +179,12 @@
cursor: pointer;
}
-.dangerButton {
- border: none;
- border-radius: 7px;
- padding: 5px 8px;
- background: none;
- color: var(--dsw-alias-state-error-primary);
- font: inherit;
- font-size: 12.5px;
- cursor: pointer;
-}
-.secondaryButton:hover:not(:disabled),
-.dangerButton:hover:not(:disabled) {
+.secondaryButton:hover:not(:disabled) {
background: var(--dsw-alias-bg-layer-1);
}
.secondaryButton:disabled,
-.dangerButton:disabled,
.addButton:disabled {
opacity: 0.5;
cursor: default;
diff --git a/packages/client/ui-agent-preset/src/client/AgentPresetSection.tsx b/packages/client/ui-agent-preset/src/client/AgentPresetSection.tsx
index b5fdf2aead..18fc0a646d 100644
--- a/packages/client/ui-agent-preset/src/client/AgentPresetSection.tsx
+++ b/packages/client/ui-agent-preset/src/client/AgentPresetSection.tsx
@@ -10,7 +10,9 @@
import { useEffect } from 'react'
import type { ReactNode } from 'react'
-import { Button, Modal } from '@deepseek-ai/dsh-client-ui-primitives'
+import {
+ Button, IconBrowseOutline16, IconCopyOutline16, IconEditOutline16, IconTrashOutline16, Modal,
+} from '@deepseek-ai/dsh-client-ui-primitives'
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, type PresetDraft } from './section-store.ts'
@@ -186,68 +188,86 @@ export function AgentPresetSection(props: AgentPresetSectionProps): ReactNode {
{t('nav')}
{t('sectionIntro')}
{state.error === null ? null : {state.error}
}
-
- {state.rows.map(row => (
-
-
- {row.name ?? row.id}
- {row.isDefault ? {t('inUse')} : null}
-
- {row.description ?? t('noDescription')}
-
- {row.trust === 'user' ? t('userTrust') : t('builtIn')}
- {row.id}
-
-
-
- {row.isDefault
- ? null
- : (
+ {([['system', t('builtInGroup')], ['user', t('customGroup')]] as const).map(([trust, heading]) => {
+ const group = state.rows.filter(row => row.trust === trust)
+ if (group.length === 0) return null
+ return (
+
+ {heading}
+
+ {state.authorable
+ ? (
+ { void props.createFrom(row.id) }}
+ >
+
+
+ )
+ : null}
+ {row.trust === 'user'
+ ? (
+ { props.confirmDelete(row.id) }}
+ >
+
+
+ )
+ : null}
+
+ {draft !== null && !draft.creating && draft.source === row.id
+ ?
+ : null}
+
+ ))}
+
+
+ )
+ })}
{draft !== null && draft.creating
? (
diff --git a/packages/client/ui-agent-preset/src/client/locales.ts b/packages/client/ui-agent-preset/src/client/locales.ts
index 613ff59b08..3d323636ea 100644
--- a/packages/client/ui-agent-preset/src/client/locales.ts
+++ b/packages/client/ui-agent-preset/src/client/locales.ts
@@ -6,7 +6,7 @@ export type AgentPresetSettingsKey =
| 'nav' | 'sectionIntro' | 'builtIn' | 'defaultBadge' | 'setDefault' | 'edit' | 'view'
| 'duplicate' | 'delete' | 'newPreset' | 'presetId' | 'presetIdPlaceholder' | 'copyOf'
| 'displayName' | 'displayNamePlaceholder' | 'displayDescription' | 'displayDescriptionPlaceholder'
- | 'inUse' | 'noDescription'
+ | 'inUse' | 'noDescription' | 'builtInGroup' | 'customGroup'
| 'composition' | 'readOnlyNotice' | 'save' | 'saving' | 'cancel' | 'close' | 'retry'
| 'idRequired' | 'idInvalid' | 'idTaken'
| 'deleteTitle' | 'deleteDescription' | 'deleteConfirm' | 'deleting'
@@ -17,7 +17,7 @@ export const en: Record
= {
description: 'Applies to sessions you start from now on. Running sessions keep the preset they began with.',
loading: 'Loading presets…',
error: 'Could not load agent presets.',
- userTrust: 'Local',
+ userTrust: 'Custom',
seatHint: 'Agent preset for this session — switchable until you send the first message',
lockedHint: 'This session\'s agent preset is fixed once the conversation starts',
nav: 'Agent presets',
@@ -39,6 +39,8 @@ export const en: Record = {
displayDescription: 'Description',
displayDescriptionPlaceholder: 'One sentence on what this preset is for',
inUse: 'In use',
+ builtInGroup: 'Built-in',
+ customGroup: 'Custom',
noDescription: 'No description.',
copyOf: 'Copied from',
composition: 'Composition (cordis.yml)',
@@ -64,7 +66,7 @@ export const zh: Record = {
description: '对此后新建的会话生效。运行中的会话保持它开始时的 preset。',
loading: '正在加载预设…',
error: '无法加载 Agent 预设。',
- userTrust: '本地',
+ userTrust: '自定义',
seatHint: '本会话的 Agent 预设 —— 发送第一条消息前可切换',
lockedHint: '会话开始后,其 Agent 预设即固定',
nav: 'Agent 预设',
@@ -84,6 +86,8 @@ export const zh: Record = {
displayDescription: '描述',
displayDescriptionPlaceholder: '一句话说明这个预设做什么',
inUse: '当前使用',
+ builtInGroup: '内置',
+ customGroup: '自定义',
noDescription: '暂无描述。',
copyOf: '复制自',
composition: '组装(cordis.yml)',
diff --git a/packages/client/ui-agent-preset/tests/section.spec.tsx b/packages/client/ui-agent-preset/tests/section.spec.tsx
index 0ae4eb88cf..34e441cb1c 100644
--- a/packages/client/ui-agent-preset/tests/section.spec.tsx
+++ b/packages/client/ui-agent-preset/tests/section.spec.tsx
@@ -98,35 +98,65 @@ describe('the preset list', () => {
expect(within(rowFor('mine')).getByText(en.userTrust)).toBeTruthy()
})
+ it('separates built-in presets from custom ones', () => {
+ renderSection()
+
+ // Two different things: one set ships with the deployment and is
+ // read-only, the other is the user's own.
+ expect(screen.getByRole('heading', { name: en.builtInGroup })).toBeTruthy()
+ expect(screen.getByRole('heading', { name: en.customGroup })).toBeTruthy()
+ expect(within(rowFor('standard')).getByText(en.builtIn)).toBeTruthy()
+ expect(within(rowFor('mine')).getByText(en.userTrust)).toBeTruthy()
+ })
+
+ it('shows no group heading for a set nobody has', () => {
+ renderSection({ rows: [{ id: 'standard', trust: 'system', isDefault: true }] })
+
+ expect(screen.queryByRole('heading', { name: en.customGroup })).toBeNull()
+ })
+
+ 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}: 标准模式` })
+ expect(inUse).toHaveProperty('disabled', true)
+ fireEvent.click(inUse)
+
+ // Clicking the card IS the choice; the preset already in use cannot be
+ // re-picked, so the click reaches nothing.
+ expect(actions.makeDefault).not.toHaveBeenCalled()
+ })
+
it('offers Edit for a local preset and View for a shipped one', () => {
renderSection()
- expect(within(rowFor('mine')).getByText(en.edit)).toBeTruthy()
+ expect(within(rowFor('mine')).getByRole('button', { name: en.edit })).toBeTruthy()
// A shipped composition is readable but not editable, and the label is
// what says so before the editor opens.
- expect(within(rowFor('standard')).getByText(en.view)).toBeTruthy()
+ expect(within(rowFor('standard')).getByRole('button', { name: en.view })).toBeTruthy()
})
it('offers Delete only for a locally authored preset', () => {
renderSection()
- expect(within(rowFor('mine')).getByText(en.delete)).toBeTruthy()
- expect(within(rowFor('standard')).queryByText(en.delete)).toBeNull()
+ expect(within(rowFor('mine')).getByRole('button', { name: en.delete })).toBeTruthy()
+ expect(within(rowFor('standard')).queryByRole('button', { name: en.delete })).toBeNull()
})
it('hides duplication and disables creation when nothing is writable', () => {
renderSection({ authorable: false })
- expect(screen.queryByText(en.duplicate)).toBeNull()
+ expect(screen.queryByRole('button', { name: en.duplicate })).toBeNull()
expect(screen.getByText(`+ ${en.newPreset}`)).toHaveProperty('disabled', true)
})
it('routes the row actions to the controller', () => {
const actions = renderSection()
- fireEvent.click(within(rowFor('mine')).getByText(en.setDefault))
- fireEvent.click(within(rowFor('mine')).getByText(en.edit))
- fireEvent.click(within(rowFor('mine')).getByText(en.duplicate))
+ // The card body is the control that picks a preset.
+ fireEvent.click(within(rowFor('mine')).getByRole('button', { name: `${en.setDefault}: mine` }))
+ fireEvent.click(within(rowFor('mine')).getByRole('button', { name: en.edit }))
+ fireEvent.click(within(rowFor('mine')).getByRole('button', { name: en.duplicate }))
fireEvent.click(screen.getByText(`+ ${en.newPreset}`))
expect(actions.makeDefault).toHaveBeenCalledWith('mine')
@@ -259,7 +289,7 @@ describe('deleting a preset', () => {
it('asks before deleting', () => {
const actions = renderSection()
- fireEvent.click(within(rowFor('mine')).getByText(en.delete))
+ fireEvent.click(within(rowFor('mine')).getByRole('button', { name: en.delete }))
expect(actions.confirmDelete).toHaveBeenCalledWith('mine')
})