mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
Two-level model/effort selection per the MenuDropdown mock
The seat's dropdown follows figma 496:26454: the root pane is the Model / Effort cell pair (14/22 label, value in the tertiary tone, right chevron), each drilling into its own list — the provider-grouped model list over the shared directory, and the High/Max effort levels. The trigger (313:14108) shows both values: model name plus effort in the caption tone. Effort is a client-local display echo on the shared directory state for now — the design pairs the two as one selection, but no wire carries a per-session effort override yet (the deepseek adapter's reasoningEffort is deployment config); the directory state documents that boundary, so wiring it later is a submit-path change, not a UI one. Escape backs out of a drilled pane before closing.
This commit is contained in:
@@ -45,6 +45,12 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Effort value beside the model name (mock's 'High': same 13/20/500, caption tone). */
|
||||
.triggerEffort {
|
||||
flex: 0 0 auto;
|
||||
color: var(--dsw-alias-label-caption);
|
||||
}
|
||||
|
||||
.chevron {
|
||||
flex: 0 0 auto;
|
||||
color: var(--dsw-alias-label-caption);
|
||||
@@ -65,9 +71,9 @@
|
||||
width: min(320px, calc(100vw - 32px));
|
||||
max-height: min(360px, calc(100vh - 96px));
|
||||
overflow: hidden;
|
||||
padding: 6px;
|
||||
padding: 4px;
|
||||
border: 1px solid var(--dsw-alias-border-l2-darkmode-thin);
|
||||
border-radius: 14px;
|
||||
border-radius: 12px;
|
||||
background: var(--dsw-specific-input-major);
|
||||
box-shadow: var(--dsw-shadow-lv3);
|
||||
color: var(--dsw-alias-label-primary);
|
||||
@@ -197,3 +203,49 @@
|
||||
flex: 0 0 18px;
|
||||
color: var(--dsw-alias-state-business-primary);
|
||||
}
|
||||
|
||||
/* Two-level root cells (figma 496:26454 .Menu_cell): 40px row, 10px side
|
||||
padding, 8px gap, 10px radius; 14/22 label in primary, value in the
|
||||
#81858C tertiary tone, right chevron drilling into the sub-list. */
|
||||
.cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
padding: 0 10px;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
background: transparent;
|
||||
color: var(--dsw-alias-label-primary);
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.cell:hover {
|
||||
background: var(--dsw-alias-interactive-bg-hover);
|
||||
}
|
||||
|
||||
.cellLabel {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cellValue {
|
||||
flex: 0 1 auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
}
|
||||
|
||||
.cellChevron {
|
||||
flex: 0 0 auto;
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
/**
|
||||
* ModelSelect: the composer's named model seat (`conversation.input.model`).
|
||||
* Compact trigger + upward provider-grouped single-select menu, revived from
|
||||
* the original PR #600 ModelSelector form. Data and submission ride the SAME
|
||||
* per-session ModelDirectory as the /model popup — one shared current, one
|
||||
* catalog load path, one selectModel route: a switch in either entry is what
|
||||
* the other shows next.
|
||||
* Two-level selection per figma 496:26454's MenuDropdown: the root menu is
|
||||
* the Model / Effort row pair (label + current value + a right chevron),
|
||||
* each drilling into its own list — the provider-grouped model list over
|
||||
* the shared directory, and the effort levels. The trigger (313:14108's
|
||||
* ToggleButton) shows both: model name + effort in the caption tone.
|
||||
* Data and submission ride the SAME per-session ModelDirectory as the
|
||||
* /model popup; effort is a client-local display echo until a wire carries
|
||||
* a per-session override (see the directory's state contract).
|
||||
*/
|
||||
import {
|
||||
useEffect, useId, useMemo, useRef, useState, useSyncExternalStore,
|
||||
@@ -12,27 +15,37 @@ import {
|
||||
} from 'react'
|
||||
import clsx from 'clsx'
|
||||
import type { ModelTarget } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import { IconCheckOutline16, IconChevronDownOutline14 } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import {
|
||||
IconCheckOutline16, IconChevronDownOutline14, IconChevronRightOutline14,
|
||||
} from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type { ModelEffort } from './directory.ts'
|
||||
import type { ModelSelectInjected } from './slots.ts'
|
||||
import css from './ModelSelect.module.css'
|
||||
|
||||
type FocusPreference = 'current' | 'first' | 'last'
|
||||
/** The displayable effort levels (deepseek wire vocabulary, capitalized for the UI). */
|
||||
const EFFORT_LEVELS: readonly { id: ModelEffort; label: string }[] = [
|
||||
{ id: 'high', label: 'High' },
|
||||
{ id: 'max', label: 'Max' },
|
||||
]
|
||||
|
||||
/** Which pane the dropdown shows: the two-row root or one drilled-in list. */
|
||||
type Pane = 'root' | 'model' | 'effort'
|
||||
|
||||
/**
|
||||
* Render the composer model seat.
|
||||
* @param props - owner share (locked) + injected face (shared directory store/verbs).
|
||||
* @returns the trigger and, while open, the upward menu.
|
||||
* @returns the trigger and, while open, the two-level menu.
|
||||
*/
|
||||
export function ModelSelect({ locked, directory, load, select }: ModelSelectInjected & { locked: boolean }) {
|
||||
export function ModelSelect({ locked, directory, load, select, setEffort }: ModelSelectInjected & { locked: boolean }) {
|
||||
const state = useSyncExternalStore(
|
||||
fn => directory.subscribe(fn),
|
||||
() => directory.getSnapshot(),
|
||||
)
|
||||
const [open, setOpen] = useState(false)
|
||||
const [pane, setPane] = useState<Pane>('root')
|
||||
const rootRef = useRef<HTMLDivElement | null>(null)
|
||||
const triggerRef = useRef<HTMLButtonElement | null>(null)
|
||||
const itemRefs = useRef<(HTMLButtonElement | null)[]>([])
|
||||
const pendingFocus = useRef<FocusPreference | null>(null)
|
||||
const id = useId()
|
||||
|
||||
const choices = useMemo(() => state.groups.flatMap(group =>
|
||||
@@ -45,6 +58,7 @@ export function ModelSelect({ locked, directory, load, select }: ModelSelectInje
|
||||
? -1
|
||||
: choices.findIndex(c => c.target.provider === state.current?.provider && c.target.model === state.current.model)
|
||||
const busy = state.status === 'selecting'
|
||||
const effortLabel = EFFORT_LEVELS.find(l => l.id === state.effort)?.label ?? 'High'
|
||||
|
||||
// Mount-time load resolves the trigger label; every open refreshes.
|
||||
useEffect(() => { load() }, [load])
|
||||
@@ -58,66 +72,39 @@ export function ModelSelect({ locked, directory, load, select }: ModelSelectInje
|
||||
return () => { document.removeEventListener('mousedown', closeOutside) }
|
||||
}, [open])
|
||||
|
||||
useEffect(() => {
|
||||
const preference = pendingFocus.current
|
||||
if (!open || preference === null || choices.length === 0) return
|
||||
const index = preference === 'first'
|
||||
? 0
|
||||
: preference === 'last'
|
||||
? choices.length - 1
|
||||
: selectedIndex >= 0 ? selectedIndex : 0
|
||||
itemRefs.current[index]?.focus()
|
||||
pendingFocus.current = null
|
||||
}, [choices.length, open, selectedIndex])
|
||||
|
||||
const show = (preference: FocusPreference | null = null): void => {
|
||||
pendingFocus.current = preference
|
||||
const show = (): void => {
|
||||
setPane('root')
|
||||
setOpen(true)
|
||||
load()
|
||||
}
|
||||
|
||||
const close = (restoreFocus = false): void => {
|
||||
setOpen(false)
|
||||
pendingFocus.current = null
|
||||
setPane('root')
|
||||
if (restoreFocus) queueMicrotask(() => { triggerRef.current?.focus() })
|
||||
}
|
||||
|
||||
const moveFocus = (offset: number): void => {
|
||||
if (choices.length === 0) return
|
||||
const active = itemRefs.current.findIndex(item => item === document.activeElement)
|
||||
const origin = active >= 0 ? active : selectedIndex >= 0 ? selectedIndex : 0
|
||||
const next = (origin + offset + choices.length) % choices.length
|
||||
itemRefs.current[next]?.focus()
|
||||
const items = itemRefs.current.filter(item => item !== null)
|
||||
if (items.length === 0) return
|
||||
const active = items.findIndex(item => item === document.activeElement)
|
||||
const next = (Math.max(active, 0) + offset + items.length) % items.length
|
||||
items[next]?.focus()
|
||||
}
|
||||
|
||||
const onRootKeyDown = (event: KeyboardEvent<HTMLDivElement>): void => {
|
||||
if (event.key === 'Escape' && open) {
|
||||
event.preventDefault()
|
||||
close(true)
|
||||
// Escape backs out of a drilled pane first, then closes.
|
||||
if (pane !== 'root') setPane('root')
|
||||
else close(true)
|
||||
return
|
||||
}
|
||||
if (!open) return
|
||||
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
||||
event.preventDefault()
|
||||
moveFocus(event.key === 'ArrowDown' ? 1 : -1)
|
||||
return
|
||||
}
|
||||
if (event.key === 'Home' || event.key === 'End') {
|
||||
event.preventDefault()
|
||||
itemRefs.current[event.key === 'Home' ? 0 : choices.length - 1]?.focus()
|
||||
}
|
||||
}
|
||||
|
||||
const onTriggerKeyDown = (event: KeyboardEvent<HTMLButtonElement>): void => {
|
||||
if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return
|
||||
event.preventDefault()
|
||||
if (!open) {
|
||||
show(event.key === 'ArrowDown' ? 'first' : 'last')
|
||||
return
|
||||
}
|
||||
pendingFocus.current = 'current'
|
||||
const index = selectedIndex >= 0 ? selectedIndex : 0
|
||||
itemRefs.current[index]?.focus()
|
||||
}
|
||||
|
||||
const onBlur = (event: FocusEvent<HTMLDivElement>): void => {
|
||||
@@ -135,7 +122,13 @@ export function ModelSelect({ locked, directory, load, select }: ModelSelectInje
|
||||
})
|
||||
}
|
||||
|
||||
const label = choices[selectedIndex]?.model.name ?? state.current?.model ?? '选择模型'
|
||||
const modelLabel = choices[selectedIndex]?.model.name ?? state.current?.model ?? '选择模型'
|
||||
itemRefs.current = []
|
||||
let itemIndex = 0
|
||||
const itemRef = () => {
|
||||
const at = itemIndex++
|
||||
return (node: HTMLButtonElement | null) => { itemRefs.current[at] = node }
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={rootRef} className={css.root} onKeyDown={onRootKeyDown} onBlur={onBlur}>
|
||||
@@ -143,16 +136,16 @@ export function ModelSelect({ locked, directory, load, select }: ModelSelectInje
|
||||
ref={triggerRef}
|
||||
type="button"
|
||||
className={css.trigger}
|
||||
aria-label={`选择模型,当前 ${label}`}
|
||||
aria-label={`选择模型,当前 ${modelLabel},effort ${effortLabel}`}
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={open}
|
||||
aria-controls={open ? `${id}-menu` : undefined}
|
||||
title={label}
|
||||
title={`${modelLabel} · ${effortLabel}`}
|
||||
disabled={locked}
|
||||
onClick={() => { open ? close() : show() }}
|
||||
onKeyDown={onTriggerKeyDown}
|
||||
>
|
||||
<span className={css.triggerLabel}>{label}</span>
|
||||
<span className={css.triggerLabel}>{modelLabel}</span>
|
||||
<span className={css.triggerEffort}>{effortLabel}</span>
|
||||
<IconChevronDownOutline14 className={clsx(css.chevron, open && css.chevronOpen)} />
|
||||
</button>
|
||||
|
||||
@@ -161,69 +154,104 @@ export function ModelSelect({ locked, directory, load, select }: ModelSelectInje
|
||||
id={`${id}-menu`}
|
||||
className={css.menu}
|
||||
role="menu"
|
||||
aria-label="模型"
|
||||
aria-label="模型与 effort"
|
||||
aria-busy={state.status === 'loading' || busy}
|
||||
>
|
||||
{state.status === 'loading' && (
|
||||
<div className={css.status}>正在刷新模型列表…</div>
|
||||
{pane === 'root' && (
|
||||
<>
|
||||
<button ref={itemRef()} type="button" role="menuitem" className={css.cell} onClick={() => setPane('model')}>
|
||||
<span className={css.cellLabel}>Model</span>
|
||||
<span className={css.cellValue}>{modelLabel}</span>
|
||||
<IconChevronRightOutline14 className={css.cellChevron} />
|
||||
</button>
|
||||
<button ref={itemRef()} type="button" role="menuitem" className={css.cell} onClick={() => setPane('effort')}>
|
||||
<span className={css.cellLabel}>Effort</span>
|
||||
<span className={css.cellValue}>{effortLabel}</span>
|
||||
<IconChevronRightOutline14 className={css.cellChevron} />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{state.error !== null && (
|
||||
<div className={css.error}>
|
||||
<span>模型操作失败:{state.error}</span>
|
||||
<button type="button" className={css.retry} onClick={() => { load() }}>重试</button>
|
||||
</div>
|
||||
|
||||
{pane === 'model' && (
|
||||
<>
|
||||
{state.status === 'loading' && (
|
||||
<div className={css.status}>正在刷新模型列表…</div>
|
||||
)}
|
||||
{state.error !== null && (
|
||||
<div className={css.error}>
|
||||
<span>模型操作失败:{state.error}</span>
|
||||
<button type="button" className={css.retry} onClick={() => { load() }}>重试</button>
|
||||
</div>
|
||||
)}
|
||||
{state.failures.map(failure => (
|
||||
<div className={css.warning} key={failure.id}>
|
||||
<span>{failure.name} 加载失败:{failure.message}</span>
|
||||
<button type="button" className={css.retry} onClick={() => { load() }}>重试</button>
|
||||
</div>
|
||||
))}
|
||||
<div className={clsx(css.groups, 'scrollable')}>
|
||||
{state.groups.map((group) => {
|
||||
const headingId = `${id}-${group.id}`
|
||||
return (
|
||||
<section role="group" aria-labelledby={headingId} className={css.group} key={group.id}>
|
||||
<div className={css.groupTitle} id={headingId}>{group.name}</div>
|
||||
{group.models.map((model) => {
|
||||
const selected = state.current?.provider === group.id && state.current.model === model.id
|
||||
return (
|
||||
<button
|
||||
ref={itemRef()}
|
||||
type="button"
|
||||
role="menuitemradio"
|
||||
aria-checked={selected}
|
||||
className={clsx(css.option, selected && css.selected)}
|
||||
key={model.id}
|
||||
title={model.name}
|
||||
disabled={busy}
|
||||
onClick={() => { choose({ provider: group.id, model: model.id }) }}
|
||||
>
|
||||
<span className={css.optionCopy}>
|
||||
<span className={css.modelName}>{model.name}</span>
|
||||
{model.description !== undefined && (
|
||||
<span className={css.description}>{model.description}</span>
|
||||
)}
|
||||
{model.unlisted === true && (
|
||||
<span className={css.unlisted}>当前模型 · 未列入目录</span>
|
||||
)}
|
||||
</span>
|
||||
<span className={css.check}>
|
||||
{selected ? <IconCheckOutline16 /> : null}
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</section>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
{state.status === 'ready' && choices.length === 0 && (
|
||||
<div className={css.empty}>没有可用的模型。</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{state.failures.map(failure => (
|
||||
<div className={css.warning} key={failure.id}>
|
||||
<span>{failure.name} 加载失败:{failure.message}</span>
|
||||
<button type="button" className={css.retry} onClick={() => { load() }}>重试</button>
|
||||
</div>
|
||||
|
||||
{pane === 'effort' && EFFORT_LEVELS.map(level => (
|
||||
<button
|
||||
ref={itemRef()}
|
||||
type="button"
|
||||
role="menuitemradio"
|
||||
aria-checked={state.effort === level.id}
|
||||
className={clsx(css.option, state.effort === level.id && css.selected)}
|
||||
key={level.id}
|
||||
onClick={() => { setEffort(level.id); close(true) }}
|
||||
>
|
||||
<span className={css.optionCopy}>
|
||||
<span className={css.modelName}>{level.label}</span>
|
||||
</span>
|
||||
<span className={css.check}>
|
||||
{state.effort === level.id ? <IconCheckOutline16 /> : null}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
|
||||
<div className={clsx(css.groups, 'scrollable')}>
|
||||
{state.groups.map((group) => {
|
||||
const headingId = `${id}-${group.id}`
|
||||
return (
|
||||
<section role="group" aria-labelledby={headingId} className={css.group} key={group.id}>
|
||||
<div className={css.groupTitle} id={headingId}>{group.name}</div>
|
||||
{group.models.map((model) => {
|
||||
const index = choices.findIndex(c => c.target.provider === group.id && c.target.model === model.id)
|
||||
const selected = state.current?.provider === group.id && state.current.model === model.id
|
||||
return (
|
||||
<button
|
||||
ref={(node) => { itemRefs.current[index] = node }}
|
||||
type="button"
|
||||
role="menuitemradio"
|
||||
aria-checked={selected}
|
||||
className={clsx(css.option, selected && css.selected)}
|
||||
key={model.id}
|
||||
title={model.name}
|
||||
disabled={busy}
|
||||
onClick={() => { choose({ provider: group.id, model: model.id }) }}
|
||||
>
|
||||
<span className={css.optionCopy}>
|
||||
<span className={css.modelName}>{model.name}</span>
|
||||
{model.description !== undefined && (
|
||||
<span className={css.description}>{model.description}</span>
|
||||
)}
|
||||
{model.unlisted === true && (
|
||||
<span className={css.unlisted}>当前模型 · 未列入目录</span>
|
||||
)}
|
||||
</span>
|
||||
<span className={css.check}>
|
||||
{selected ? <IconCheckOutline16 /> : null}
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</section>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{state.status === 'ready' && choices.length === 0 && (
|
||||
<div className={css.empty}>没有可用的模型。</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -11,8 +11,19 @@ import type {
|
||||
import type { SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { createSnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
|
||||
/** Thinking-effort display levels (the deepseek wire vocabulary). */
|
||||
export type ModelEffort = 'high' | 'max'
|
||||
|
||||
/** Directory snapshot both entries render from. */
|
||||
export interface ModelDirectoryState {
|
||||
/**
|
||||
* Displayed thinking-effort level. Client-local echo only for now: the
|
||||
* design pairs model and effort as one two-level selection, but no wire
|
||||
* carries a per-session effort override yet (the deepseek adapter's
|
||||
* reasoningEffort is deployment config) — selecting it updates this
|
||||
* display state and nothing else.
|
||||
*/
|
||||
effort: ModelEffort
|
||||
/** Target the host reports for the next assembled step; null before the first load. */
|
||||
current: ModelTarget | null
|
||||
/** Successfully loaded provider groups (last good load). */
|
||||
@@ -29,7 +40,7 @@ export interface ModelDirectoryState {
|
||||
export class ModelDirectory {
|
||||
/** The shared snapshot both entries render from (uSES-safe store). */
|
||||
readonly store: SnapshotStore<ModelDirectoryState> = createSnapshotStore<ModelDirectoryState>({
|
||||
current: null, groups: [], failures: [], status: 'idle', error: null,
|
||||
effort: 'high', current: null, groups: [], failures: [], status: 'idle', error: null,
|
||||
})
|
||||
|
||||
/** Latest operation wins; an older response never overwrites a newer one. */
|
||||
@@ -63,7 +74,13 @@ export class ModelDirectory {
|
||||
throw new Error(`session.models failed: ${result.error.code}: ${result.error.message}`)
|
||||
}
|
||||
const { current, groups, failures } = result.value
|
||||
this.store.set({ current, groups, failures, status: 'ready', error: null })
|
||||
this.store.update((s) => {
|
||||
s.current = current
|
||||
s.groups = groups
|
||||
s.failures = failures
|
||||
s.status = 'ready'
|
||||
s.error = null
|
||||
})
|
||||
return result.value
|
||||
}
|
||||
|
||||
@@ -90,6 +107,15 @@ export class ModelDirectory {
|
||||
this.store.update((s) => { s.current = result.value.selected; s.status = 'ready'; s.error = null })
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the displayed effort level (client-local; see the state field's contract).
|
||||
* @param effort - the level to display.
|
||||
*/
|
||||
setEffort(effort: ModelEffort): void {
|
||||
if (this.disposed) return
|
||||
this.store.update((s) => { s.effort = effort })
|
||||
}
|
||||
|
||||
/** Scope teardown: late settlements lose write access to the store. */
|
||||
dispose(): void {
|
||||
this.disposed = true
|
||||
|
||||
@@ -20,7 +20,7 @@ import type { ModelSelectInjected } from './slots.ts'
|
||||
import { ModelSelect } from './ModelSelect.tsx'
|
||||
|
||||
export { ModelDirectory } from './directory.ts'
|
||||
export type { ModelDirectoryState } from './directory.ts'
|
||||
export type { ModelDirectoryState, ModelEffort } from './directory.ts'
|
||||
export { ModelService } from './service.ts'
|
||||
export type { ModelSelectInjected } from './slots.ts'
|
||||
|
||||
@@ -114,6 +114,7 @@ export function apply(ctx: ClientContext): void {
|
||||
directory: directory.store,
|
||||
load: () => { directory.load().catch(() => { /* surfaced on the store */ }) },
|
||||
select: (target: ModelTarget) => directory.select(target).then(() => true, () => false),
|
||||
setEffort: effort => directory.setEffort(effort),
|
||||
}
|
||||
},
|
||||
}, ModelSelect), 'ui-model: composer model seat registration')
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
import type { ModelTarget } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import type { SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { ModelDirectoryState } from './directory.ts'
|
||||
import type { ModelDirectoryState, ModelEffort } from './directory.ts'
|
||||
|
||||
/** Injected business face of the composer model seat. */
|
||||
export interface ModelSelectInjected {
|
||||
@@ -20,4 +20,10 @@ export interface ModelSelectInjected {
|
||||
* @returns whether the host accepted the selection.
|
||||
*/
|
||||
select(target: ModelTarget): Promise<boolean>
|
||||
/**
|
||||
* Set the displayed thinking-effort level (client-local echo; see the
|
||||
* directory state contract).
|
||||
* @param effort - the level to display.
|
||||
*/
|
||||
setEffort(effort: ModelEffort): void
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user