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={(
- { setOpen(value => !value) }}
- >
- {label}
-
-
- )}
+ 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={(
- { setOpen(value => !value) }}
- >
- {state.current}
-
-
- )}
+ 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={(
+ { onOpenChange(!open) }}
+ >
+ {label}
+
+
+ )}
+ />
+ )
+}