refactor(ui-agent-preset): one preset picker behind both surfaces

The settings row and the composer seat differ in where they sit, what they
call the current value, and when they refuse a pick — not in how the picker
behaves. Extracting it also stops the row from reading like the permission
row it has nothing to do with.
This commit is contained in:
Yichen Jiang
2026-08-07 15:59:23 +08:00
parent d612103135
commit e9648bdeee
3 changed files with 104 additions and 58 deletions

View File

@@ -7,9 +7,9 @@
import { useEffect, useState } from 'react'
import type { SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
import { IconChevronDownOutline14, Menu } from '@deepseek-ai/dsh-client-ui-primitives'
import type { AgentPresetSettingsState } from './settings-store.ts'
import type { AgentPresetSettingsKey } from './locales.ts'
import { PresetMenu } from './PresetMenu.tsx'
import css from './AgentPresetRow.module.css'
/** Registration-side business face for the host-backed preference. */
@@ -61,36 +61,17 @@ export function AgentPresetRow({ load, select, useAgentPreset, t }: AgentPresetR
<div className={css.title}>{t('title')}</div>
<div className={css.desc} role={state.error === null ? undefined : 'alert'}>{description}</div>
</div>
<Menu
open={open}
onClose={() => { setOpen(false) }}
// A locally authored preset is exactly as privileged as the plugins it
// names, so the list says which rows are local rather than presenting
// every preset as shipped and vetted.
items={state.options.map(option => ({
id: option.id,
label: option.trust === 'user' ? `${option.id} · ${t('userTrust')}` : option.id,
}))}
<PresetMenu
options={state.options}
selectedId={state.currentValue}
onSelect={(id) => {
setOpen(false)
void select(id)
}}
align="end"
portal
anchor={(
<button
type="button"
className={css.selector}
aria-haspopup="menu"
aria-expanded={open}
disabled={busy || !state.writable || state.options.length === 0}
onClick={() => { setOpen(value => !value) }}
>
{label}
<IconChevronDownOutline14 className={css.chevron} />
</button>
)}
label={label}
userTrustLabel={t('userTrust')}
buttonClassName={css.selector}
chevronClassName={css.chevron}
disabled={busy || !state.writable || state.options.length === 0}
open={open}
onOpenChange={setOpen}
onSelect={(id) => { void select(id) }}
/>
</div>
)

View File

@@ -9,10 +9,10 @@
import { useEffect, useState } from 'react'
import type { SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
import { IconChevronDownOutline14, Menu } from '@deepseek-ai/dsh-client-ui-primitives'
// Type-only: pulls the ui-conversation SlotMap merge (the agentPreset seat).
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
import type { AgentPresetSeatState } from './seat-store.ts'
import { PresetMenu } from './PresetMenu.tsx'
import css from './AgentPresetSeat.module.css'
/** Registration-side business face for the composer seat. */
@@ -62,34 +62,18 @@ export function AgentPresetSeat({ load, select, useAgentPresetSeat, locked, t }:
}
return (
<Menu
open={open}
onClose={() => { setOpen(false) }}
items={state.options.map(option => ({
id: option.id,
label: option.trust === 'user' ? `${option.id} · ${t('userTrust')}` : option.id,
}))}
<PresetMenu
options={state.options}
selectedId={state.current}
onSelect={(id) => {
setOpen(false)
void select(id)
}}
align="end"
portal
anchor={(
<button
type="button"
className={css.seat}
aria-haspopup="menu"
aria-expanded={open}
title={state.error ?? t('seatHint')}
disabled={locked || state.busy}
onClick={() => { setOpen(value => !value) }}
>
{state.current}
<IconChevronDownOutline14 className={css.chevron} />
</button>
)}
label={state.current}
userTrustLabel={t('userTrust')}
buttonClassName={css.seat}
chevronClassName={css.chevron}
disabled={locked || state.busy}
title={state.error ?? t('seatHint')}
open={open}
onOpenChange={setOpen}
onSelect={(id) => { void select(id) }}
/>
)
}

View File

@@ -0,0 +1,81 @@
/**
* The preset picker both surfaces render: a menu of presets over a button
* naming the current one.
*
* The settings row and the composer seat differ in where they sit, what they
* call the current value, and when they refuse a pick — not in how the picker
* itself behaves. Trust is the one thing the list always says: a locally
* authored preset is exactly as privileged as the plugins it names, so the
* label marks it rather than presenting every preset as shipped and vetted.
*/
import { IconChevronDownOutline14, Menu } from '@deepseek-ai/dsh-client-ui-primitives'
import type { AgentPresetOption } from './settings-store.ts'
/** What one surface passes to the shared picker. */
export interface PresetMenuProps {
/** Presets to offer, in roster order. */
options: readonly AgentPresetOption[]
/** The preset the button names and the menu marks selected. */
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
/** Class for the trigger button, owned by the calling surface. */
buttonClassName: string | undefined
/** Class for the chevron, owned by the calling surface. */
chevronClassName: string | undefined
/** Whether the trigger refuses interaction. */
disabled: boolean
/** Native tooltip, absent where the surface offers none. */
title?: string
/** Whether the menu is open — the surface owns this so it can force it shut. */
open: boolean
/** Report the menu's next open state. */
onOpenChange: (open: boolean) => void
/** Called with the picked preset once the menu has closed. */
onSelect: (id: string) => void
}
/**
* Render the preset picker.
* @param props - the calling surface's copy, styling, and handlers.
* @returns the menu and its trigger.
*/
export function PresetMenu({
options, selectedId, label, userTrustLabel, buttonClassName, chevronClassName,
disabled, title, open, onOpenChange, onSelect,
}: PresetMenuProps) {
return (
<Menu
open={open}
onClose={() => { onOpenChange(false) }}
items={options.map(option => ({
id: option.id,
label: option.trust === 'user' ? `${option.id} · ${userTrustLabel}` : option.id,
}))}
selectedId={selectedId}
onSelect={(id) => {
onOpenChange(false)
onSelect(id)
}}
align="end"
portal
anchor={(
<button
type="button"
className={buttonClassName}
aria-haspopup="menu"
aria-expanded={open}
{...title === undefined ? {} : { title }}
disabled={disabled}
onClick={() => { onOpenChange(!open) }}
>
{label}
<IconChevronDownOutline14 className={chevronClassName} />
</button>
)}
/>
)
}