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:
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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
packages/client/ui-agent-preset/src/client/PresetMenu.tsx
Normal file
81
packages/client/ui-agent-preset/src/client/PresetMenu.tsx
Normal 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>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user