mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
Merge branch 'stack/agent-profiles-5-web-ui' into stack/agent-profiles-8-authoring
# Conflicts: # packages/client/ui-agent-preset/src/client/AgentPresetRow.tsx # packages/client/ui-agent-preset/src/client/AgentPresetSeat.tsx
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. */
|
||||
@@ -52,12 +52,7 @@ 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'
|
||||
// The metadata name is what every other surface shows — the new-session chip,
|
||||
// the session header, the preset cards — so this row shows it too; 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 label = state.currentValue === '' ? t('loading') : state.currentValue
|
||||
const description: string = state.error ?? t('description')
|
||||
|
||||
return (
|
||||
@@ -66,38 +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.name ?? option.id} · ${t('userTrust')}`
|
||||
: option.name ?? 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>
|
||||
)
|
||||
|
||||
@@ -1,50 +1,44 @@
|
||||
/**
|
||||
* The agent-preset chip on the new-session screen, beside the workspace
|
||||
* picker.
|
||||
* Composer seat for the session's agent preset.
|
||||
*
|
||||
* 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.
|
||||
* 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.
|
||||
*/
|
||||
|
||||
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, IconThinkOutline16, Menu } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
// Type-only: pulls the ui-conversation SlotMap merge (the hero seat).
|
||||
// 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 hero chip. */
|
||||
/** Registration-side business face for the composer seat. */
|
||||
export interface AgentPresetSeatInjected {
|
||||
hooks: {
|
||||
/** Seat snapshot bound by the renderer as useAgentPresetSeat. */
|
||||
agentPresetSeat: SnapshotStore<AgentPresetSeatState>
|
||||
}
|
||||
/** Read the roster when the chip first renders. */
|
||||
/** Load the roster and this session's state when the seat first renders. */
|
||||
load: () => Promise<void>
|
||||
/** Stage one preset for the next session. */
|
||||
/** Switch this session to another preset. */
|
||||
select: (id: string) => Promise<void>
|
||||
}
|
||||
|
||||
/** Full component props. */
|
||||
export type AgentPresetSeatProps =
|
||||
PropsRuntime<'conversation.hero.agentPreset'>
|
||||
PropsRuntime<'conversation.input.agentPreset'>
|
||||
& PropsLocale<'settings.agentPreset'>
|
||||
& InjectFace<AgentPresetSeatInjected>
|
||||
|
||||
/**
|
||||
* Render the new-session agent-preset chip.
|
||||
* @param props - composed slot props.
|
||||
* @returns the chip, or null when the deployment composes no presets.
|
||||
* 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.
|
||||
*/
|
||||
export function AgentPresetSeat({ load, select, useAgentPresetSeat, t }: AgentPresetSeatProps) {
|
||||
export function AgentPresetSeat({ load, select, useAgentPresetSeat, locked, t }: AgentPresetSeatProps) {
|
||||
const state = useAgentPresetSeat(snapshot => snapshot)
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
@@ -52,49 +46,34 @@ export function AgentPresetSeat({ load, select, useAgentPresetSeat, t }: AgentPr
|
||||
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
|
||||
|
||||
const chosen = state.options.find(option => option.id === state.current)
|
||||
// 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>
|
||||
}
|
||||
|
||||
return (
|
||||
<Menu
|
||||
open={open}
|
||||
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>
|
||||
),
|
||||
}))}
|
||||
<PresetMenu
|
||||
options={state.options}
|
||||
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>
|
||||
)}
|
||||
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