diff --git a/packages/client/ui-agent-preset/src/client/AgentPresetRow.tsx b/packages/client/ui-agent-preset/src/client/AgentPresetRow.tsx index 492f593134..9056f32266 100644 --- a/packages/client/ui-agent-preset/src/client/AgentPresetRow.tsx +++ b/packages/client/ui-agent-preset/src/client/AgentPresetRow.tsx @@ -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
{t('title')}
{description}
- { 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, - }))} + { - setOpen(false) - void select(id) - }} - align="end" - portal - anchor={( - - )} + 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) }} /> ) diff --git a/packages/client/ui-agent-preset/src/client/AgentPresetSeat.tsx b/packages/client/ui-agent-preset/src/client/AgentPresetSeat.tsx index 3e8d08479a..352ca66274 100644 --- a/packages/client/ui-agent-preset/src/client/AgentPresetSeat.tsx +++ b/packages/client/ui-agent-preset/src/client/AgentPresetSeat.tsx @@ -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 ( - { setOpen(false) }} - items={state.options.map(option => ({ - id: option.id, - label: option.trust === 'user' ? `${option.id} · ${t('userTrust')}` : option.id, - }))} + { - setOpen(false) - void select(id) - }} - align="end" - portal - anchor={( - - )} + 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) }} /> ) } diff --git a/packages/client/ui-agent-preset/src/client/PresetMenu.tsx b/packages/client/ui-agent-preset/src/client/PresetMenu.tsx new file mode 100644 index 0000000000..bdf22ee56f --- /dev/null +++ b/packages/client/ui-agent-preset/src/client/PresetMenu.tsx @@ -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 ( + { 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={( + + )} + /> + ) +}