mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
refactor(ui-agent-preset): keep the row's picker shared, the hero chip its own
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.
This commit is contained in:
@@ -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 (
|
||||
|
||||
@@ -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<AgentPresetSeatState>
|
||||
}
|
||||
/** Load the roster and this session's state when the seat first renders. */
|
||||
/** Read the roster when the chip first renders. */
|
||||
load: () => Promise<void>
|
||||
/** Switch this session to another preset. */
|
||||
/** Stage one preset for the next session. */
|
||||
select: (id: string) => Promise<void>
|
||||
}
|
||||
|
||||
/** Full component props. */
|
||||
export type AgentPresetSeatProps =
|
||||
PropsRuntime<'conversation.input.agentPreset'>
|
||||
PropsRuntime<'conversation.hero.agentPreset'>
|
||||
& PropsLocale<'settings.agentPreset'>
|
||||
& InjectFace<AgentPresetSeatInjected>
|
||||
|
||||
/**
|
||||
* 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 <span className={`${css.seat} ${css.locked}`} title={t('lockedHint')}>{state.current}</span>
|
||||
}
|
||||
const chosen = state.options.find(option => option.id === state.current)
|
||||
|
||||
return (
|
||||
<PresetMenu
|
||||
options={state.options}
|
||||
selectedId={state.current}
|
||||
label={state.current}
|
||||
userTrustLabel={t('userTrust')}
|
||||
buttonClassName={css.seat}
|
||||
chevronClassName={css.chevron}
|
||||
disabled={locked || state.busy}
|
||||
title={state.error ?? t('seatHint')}
|
||||
<Menu
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
onSelect={(id) => { 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: (
|
||||
<span className={css.item}>
|
||||
<span className={css.itemName}>{option.name ?? option.id}</span>
|
||||
<span className={css.itemDesc}>{option.description ?? t('noDescription')}</span>
|
||||
</span>
|
||||
),
|
||||
}))}
|
||||
selectedId={state.current}
|
||||
onSelect={(id) => {
|
||||
setOpen(false)
|
||||
void select(id)
|
||||
}}
|
||||
align="start"
|
||||
portal
|
||||
anchor={(
|
||||
<button
|
||||
type="button"
|
||||
className={css.seat}
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={open}
|
||||
title={state.error ?? t('seatHint')}
|
||||
disabled={state.busy}
|
||||
onClick={() => { setOpen(value => !value) }}
|
||||
>
|
||||
<IconThinkOutline16 className={css.seatIcon} />
|
||||
{chosen?.name ?? state.current}
|
||||
<IconChevronDownOutline14 className={css.chevron} />
|
||||
</button>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user