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 { useEffect, useState } from 'react'
|
||||||
import type { SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
|
import type { SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
|
||||||
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
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 { AgentPresetSettingsState } from './settings-store.ts'
|
||||||
import type { AgentPresetSettingsKey } from './locales.ts'
|
import type { AgentPresetSettingsKey } from './locales.ts'
|
||||||
|
import { PresetMenu } from './PresetMenu.tsx'
|
||||||
import css from './AgentPresetRow.module.css'
|
import css from './AgentPresetRow.module.css'
|
||||||
|
|
||||||
/** Registration-side business face for the host-backed preference. */
|
/** 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.
|
// every session shares the host composition — the row simply does not exist.
|
||||||
if (state.status === 'unavailable') return null
|
if (state.status === 'unavailable') return null
|
||||||
const busy = state.status === 'loading' || state.status === 'saving'
|
const busy = state.status === 'loading' || state.status === 'saving'
|
||||||
// The metadata name is what every other surface shows — the new-session chip,
|
const label = state.currentValue === '' ? t('loading') : state.currentValue
|
||||||
// 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 description: string = state.error ?? t('description')
|
const description: string = state.error ?? t('description')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -66,38 +61,17 @@ export function AgentPresetRow({ load, select, useAgentPreset, t }: AgentPresetR
|
|||||||
<div className={css.title}>{t('title')}</div>
|
<div className={css.title}>{t('title')}</div>
|
||||||
<div className={css.desc} role={state.error === null ? undefined : 'alert'}>{description}</div>
|
<div className={css.desc} role={state.error === null ? undefined : 'alert'}>{description}</div>
|
||||||
</div>
|
</div>
|
||||||
<Menu
|
<PresetMenu
|
||||||
open={open}
|
options={state.options}
|
||||||
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,
|
|
||||||
}))}
|
|
||||||
selectedId={state.currentValue}
|
selectedId={state.currentValue}
|
||||||
onSelect={(id) => {
|
label={label}
|
||||||
setOpen(false)
|
userTrustLabel={t('userTrust')}
|
||||||
void select(id)
|
buttonClassName={css.selector}
|
||||||
}}
|
chevronClassName={css.chevron}
|
||||||
align="end"
|
disabled={busy || !state.writable || state.options.length === 0}
|
||||||
portal
|
open={open}
|
||||||
anchor={(
|
onOpenChange={setOpen}
|
||||||
<button
|
onSelect={(id) => { void select(id) }}
|
||||||
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>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,50 +1,44 @@
|
|||||||
/**
|
/**
|
||||||
* The agent-preset chip on the new-session screen, beside the workspace
|
* Composer seat for the session's agent preset.
|
||||||
* picker.
|
|
||||||
*
|
*
|
||||||
* It lives here rather than in the composer because the choice is only
|
* The switch exists only while the conversation has not started: after the
|
||||||
* available before a conversation starts: once a turn has run, the session's
|
* first turn the session's history was produced under this preset's tools, so
|
||||||
* history was produced under that preset's tools and the host refuses to swap
|
* the seat becomes a plain label rather than offering a choice it cannot honor.
|
||||||
* 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 { useEffect, useState } from 'react'
|
||||||
import type { SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
|
import type { SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
|
||||||
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
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 agentPreset seat).
|
||||||
// Type-only: pulls the ui-conversation SlotMap merge (the hero seat).
|
|
||||||
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
|
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||||
import type { AgentPresetSeatState } from './seat-store.ts'
|
import type { AgentPresetSeatState } from './seat-store.ts'
|
||||||
|
import { PresetMenu } from './PresetMenu.tsx'
|
||||||
import css from './AgentPresetSeat.module.css'
|
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 {
|
export interface AgentPresetSeatInjected {
|
||||||
hooks: {
|
hooks: {
|
||||||
/** Seat snapshot bound by the renderer as useAgentPresetSeat. */
|
/** Seat snapshot bound by the renderer as useAgentPresetSeat. */
|
||||||
agentPresetSeat: SnapshotStore<AgentPresetSeatState>
|
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>
|
load: () => Promise<void>
|
||||||
/** Stage one preset for the next session. */
|
/** Switch this session to another preset. */
|
||||||
select: (id: string) => Promise<void>
|
select: (id: string) => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Full component props. */
|
/** Full component props. */
|
||||||
export type AgentPresetSeatProps =
|
export type AgentPresetSeatProps =
|
||||||
PropsRuntime<'conversation.hero.agentPreset'>
|
PropsRuntime<'conversation.input.agentPreset'>
|
||||||
& PropsLocale<'settings.agentPreset'>
|
& PropsLocale<'settings.agentPreset'>
|
||||||
& InjectFace<AgentPresetSeatInjected>
|
& InjectFace<AgentPresetSeatInjected>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render the new-session agent-preset chip.
|
* Render the session's agent-preset seat.
|
||||||
* @param props - composed slot props.
|
* @param props - composed slot props; `locked` is the composer's own busy state.
|
||||||
* @returns the chip, or null when the deployment composes no presets.
|
* @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 state = useAgentPresetSeat(snapshot => snapshot)
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
@@ -52,49 +46,34 @@ export function AgentPresetSeat({ load, select, useAgentPresetSeat, t }: AgentPr
|
|||||||
void load()
|
void load()
|
||||||
}, [load])
|
}, [load])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (state.switchable) return
|
||||||
|
setOpen(false)
|
||||||
|
}, [state.switchable])
|
||||||
|
|
||||||
// Nothing to choose between: the deployment composes no presets and every
|
// Nothing to choose between: the deployment composes no presets and every
|
||||||
// session shares the host composition.
|
// session shares the host composition.
|
||||||
if (state.options.length === 0 || state.current === '') return null
|
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 (
|
return (
|
||||||
<Menu
|
<PresetMenu
|
||||||
open={open}
|
options={state.options}
|
||||||
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}
|
selectedId={state.current}
|
||||||
onSelect={(id) => {
|
label={state.current}
|
||||||
setOpen(false)
|
userTrustLabel={t('userTrust')}
|
||||||
void select(id)
|
buttonClassName={css.seat}
|
||||||
}}
|
chevronClassName={css.chevron}
|
||||||
align="start"
|
disabled={locked || state.busy}
|
||||||
portal
|
title={state.error ?? t('seatHint')}
|
||||||
anchor={(
|
open={open}
|
||||||
<button
|
onOpenChange={setOpen}
|
||||||
type="button"
|
onSelect={(id) => { void select(id) }}
|
||||||
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>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
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