Преглед изворни кода

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.
Yichen Jiang пре 1 месец
родитељ
комит
e9648bdeee

+ 11 - 30
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
         <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>
   )

+ 12 - 28
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 (
-    <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) }}
     />
   )
 }

+ 81 - 0
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 (
+    <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>
+      )}
+    />
+  )
+}