From 93ad74cab5d2c29e384ca3f5a35dc2e363244173 Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Fri, 7 Aug 2026 16:01:43 +0800 Subject: [PATCH] refactor(ui-agent-preset): keep the row's picker shared, the hero chip its own MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The chip renders name and description on two lines with its own icon and alignment, so it is not the same control as the settings row — the shared picker carries the row, and the chip keeps its shape. --- .../src/client/AgentPresetRow.tsx | 6 +- .../src/client/AgentPresetSeat.tsx | 91 ++++++++++++------- .../ui-agent-preset/src/client/PresetMenu.tsx | 7 +- 3 files changed, 67 insertions(+), 37 deletions(-) diff --git a/packages/client/ui-agent-preset/src/client/AgentPresetRow.tsx b/packages/client/ui-agent-preset/src/client/AgentPresetRow.tsx index 9056f32266..ba875b0b95 100644 --- a/packages/client/ui-agent-preset/src/client/AgentPresetRow.tsx +++ b/packages/client/ui-agent-preset/src/client/AgentPresetRow.tsx @@ -52,7 +52,11 @@ export function AgentPresetRow({ load, select, useAgentPreset, t }: AgentPresetR // every session shares the host composition — the row simply does not exist. if (state.status === 'unavailable') return null const busy = state.status === 'loading' || state.status === 'saving' - const label = state.currentValue === '' ? t('loading') : state.currentValue + // The metadata name is what every other surface shows — the id is the + // addressing, not the label. A preset that names itself nothing falls back + // to its id, which is then all there is to say about it. + const chosen = state.options.find(option => option.id === state.currentValue) + const label = state.currentValue === '' ? t('loading') : (chosen?.name ?? state.currentValue) const description: string = state.error ?? t('description') return ( diff --git a/packages/client/ui-agent-preset/src/client/AgentPresetSeat.tsx b/packages/client/ui-agent-preset/src/client/AgentPresetSeat.tsx index 352ca66274..8e18471fbc 100644 --- a/packages/client/ui-agent-preset/src/client/AgentPresetSeat.tsx +++ b/packages/client/ui-agent-preset/src/client/AgentPresetSeat.tsx @@ -1,44 +1,50 @@ /** - * Composer seat for the session's agent preset. + * The agent-preset chip on the new-session screen, beside the workspace + * picker. * - * The switch exists only while the conversation has not started: after the - * first turn the session's history was produced under this preset's tools, so - * the seat becomes a plain label rather than offering a choice it cannot honor. + * It lives here rather than in the composer because the choice is only + * available before a conversation starts: once a turn has run, the session's + * history was produced under that preset's tools and the host refuses to swap + * them. A control that spends most of its life disabled belongs on the screen + * where it still works. + * + * The menu opens on the staged choice, which starts as the deployment default. + * Picking stages; the choice reaches a session when one becomes current. */ 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' -// Type-only: pulls the ui-conversation SlotMap merge (the agentPreset seat). +import { IconChevronDownOutline14, IconThinkOutline16, Menu } from '@deepseek-ai/dsh-client-ui-primitives' +// Type-only: pulls the ui-conversation SlotMap merge (the hero 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. */ +/** Registration-side business face for the hero chip. */ export interface AgentPresetSeatInjected { hooks: { /** Seat snapshot bound by the renderer as useAgentPresetSeat. */ agentPresetSeat: SnapshotStore } - /** Load the roster and this session's state when the seat first renders. */ + /** Read the roster when the chip first renders. */ load: () => Promise - /** Switch this session to another preset. */ + /** Stage one preset for the next session. */ select: (id: string) => Promise } /** Full component props. */ export type AgentPresetSeatProps = - PropsRuntime<'conversation.input.agentPreset'> + PropsRuntime<'conversation.hero.agentPreset'> & PropsLocale<'settings.agentPreset'> & InjectFace /** - * Render the session's agent-preset seat. - * @param props - composed slot props; `locked` is the composer's own busy state. - * @returns the seat, or null when the deployment composes no presets. + * Render the new-session agent-preset chip. + * @param props - composed slot props. + * @returns the chip, or null when the deployment composes no presets. */ -export function AgentPresetSeat({ load, select, useAgentPresetSeat, locked, t }: AgentPresetSeatProps) { +export function AgentPresetSeat({ load, select, useAgentPresetSeat, t }: AgentPresetSeatProps) { const state = useAgentPresetSeat(snapshot => snapshot) const [open, setOpen] = useState(false) @@ -46,34 +52,49 @@ export function AgentPresetSeat({ load, select, useAgentPresetSeat, locked, t }: void load() }, [load]) - useEffect(() => { - if (state.switchable) return - setOpen(false) - }, [state.switchable]) - // Nothing to choose between: the deployment composes no presets and every // session shares the host composition. if (state.options.length === 0 || state.current === '') return null - // Past the first turn the preset is a fact about this session, not a - // control — showing a disabled menu would suggest it could still be changed. - if (!state.switchable) { - return {state.current} - } + const chosen = state.options.find(option => option.id === state.current) return ( - { void select(id) }} + onClose={() => { setOpen(false) }} + items={state.options.map(option => ({ + id: option.id, + // Name and description together: the id alone never said what a + // preset does, which is the whole reason the metadata exists. + label: ( + + {option.name ?? option.id} + {option.description ?? t('noDescription')} + + ), + }))} + selectedId={state.current} + onSelect={(id) => { + setOpen(false) + void select(id) + }} + align="start" + portal + anchor={( + + )} /> ) } diff --git a/packages/client/ui-agent-preset/src/client/PresetMenu.tsx b/packages/client/ui-agent-preset/src/client/PresetMenu.tsx index bdf22ee56f..1049ee6bc2 100644 --- a/packages/client/ui-agent-preset/src/client/PresetMenu.tsx +++ b/packages/client/ui-agent-preset/src/client/PresetMenu.tsx @@ -53,7 +53,12 @@ export function PresetMenu({ onClose={() => { onOpenChange(false) }} items={options.map(option => ({ id: option.id, - label: option.trust === 'user' ? `${option.id} · ${userTrustLabel}` : option.id, + // The metadata name is what every surface shows; the id is addressing, + // not a label. A preset that names itself nothing falls back to its id, + // which is then all there is to say about it. + label: option.trust === 'user' + ? `${option.name ?? option.id} · ${userTrustLabel}` + : option.name ?? option.id, }))} selectedId={selectedId} onSelect={(id) => {