From d6121031359f95ad83cdf22eec8b10a0a3324c8e Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Fri, 7 Aug 2026 15:53:18 +0800 Subject: [PATCH 1/2] refactor(apiproxy): one wording for a preset failure on both paths Session create and the preset switch can be handed the same two failures, and a client branching on the code needs them worded identically from either. --- packages/host/apiproxy/src/api-proxy.ts | 61 +++++++++++++------------ 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/packages/host/apiproxy/src/api-proxy.ts b/packages/host/apiproxy/src/api-proxy.ts index 654fc96a3f..16b9b4e622 100644 --- a/packages/host/apiproxy/src/api-proxy.ts +++ b/packages/host/apiproxy/src/api-proxy.ts @@ -198,6 +198,35 @@ function err(request: RpcRequest, error: RpcError): RpcResponse { return { rpcId: request.rpcId, result: { ok: false, error } } } +/** + * The RPC refusal a preset failure becomes, or undefined when the failure is + * about something else. + * + * Both the session-create path and the switch path can be handed the same two + * failures, and a client that has to branch on the code needs them worded the + * same from either. + * @param request - the request being answered. + * @param error - the thrown value. + * @returns the refusal, or undefined when the caller should keep handling. + */ +function presetFailure(request: RpcRequest, error: unknown): RpcResponse | undefined { + if (error instanceof UnknownPresetError) { + return err(request, { + code: 'agent-preset-not-found', + message: error.message, + details: { agentPreset: error.presetId, available: [...error.available] }, + }) + } + if (error instanceof PresetMountError) { + return err(request, { + code: 'agent-preset-invalid', + message: error.message, + details: { agentPreset: error.presetId, reason: error.reason }, + }) + } + return undefined +} + /** Simple async queue: core callbacks push, the AsyncIterable pulls; abort/return cleans up. */ class FrameQueue { private buffer: F[] = [] @@ -1771,20 +1800,8 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro }, }) } - if (error instanceof UnknownPresetError) { - return err(request, { - code: 'agent-preset-not-found', - message: error.message, - details: { agentPreset: error.presetId, available: [...error.available] }, - }) - } - if (error instanceof PresetMountError) { - return err(request, { - code: 'agent-preset-invalid', - message: error.message, - details: { agentPreset: error.presetId, reason: error.reason }, - }) - } + const refused = presetFailure(request, error) + if (refused !== undefined) return refused if (error instanceof SessionCwdConflict) { return err(request, { code: 'session-conflict', @@ -2595,20 +2612,8 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro agent.session.append('agent-preset/selected', { agentPreset: preset.id }) return ok(request, { agentPreset: preset.id }) } catch (error: unknown) { - if (error instanceof UnknownPresetError) { - return err(request, { - code: 'agent-preset-not-found', - message: error.message, - details: { agentPreset: error.presetId, available: [...error.available] }, - }) - } - if (error instanceof PresetMountError) { - return err(request, { - code: 'agent-preset-invalid', - message: error.message, - details: { agentPreset: error.presetId, reason: error.reason }, - }) - } + const refused = presetFailure(request, error) + if (refused !== undefined) return refused return err(request, { code: 'internal', message: `failed to select agent preset "${agentPreset}": ${String(error)}`, From e9648bdeee64a68ed3817e658e55a9530e335c77 Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Fri, 7 Aug 2026 15:59:23 +0800 Subject: [PATCH 2/2] refactor(ui-agent-preset): one preset picker behind both surfaces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 behaves. Extracting it also stops the row from reading like the permission row it has nothing to do with. --- .../src/client/AgentPresetRow.tsx | 41 +++------- .../src/client/AgentPresetSeat.tsx | 40 +++------ .../ui-agent-preset/src/client/PresetMenu.tsx | 81 +++++++++++++++++++ 3 files changed, 104 insertions(+), 58 deletions(-) create mode 100644 packages/client/ui-agent-preset/src/client/PresetMenu.tsx diff --git a/packages/client/ui-agent-preset/src/client/AgentPresetRow.tsx b/packages/client/ui-agent-preset/src/client/AgentPresetRow.tsx index 492f593134..9056f32266 100644 --- a/packages/client/ui-agent-preset/src/client/AgentPresetRow.tsx +++ b/packages/client/ui-agent-preset/src/client/AgentPresetRow.tsx @@ -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. */ @@ -61,36 +61,17 @@ export function AgentPresetRow({ load, select, useAgentPreset, t }: AgentPresetR
{t('title')}
{description}
- { 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.id} · ${t('userTrust')}` : option.id, - }))} + { - setOpen(false) - void select(id) - }} - align="end" - portal - anchor={( - - )} + 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) }} /> ) diff --git a/packages/client/ui-agent-preset/src/client/AgentPresetSeat.tsx b/packages/client/ui-agent-preset/src/client/AgentPresetSeat.tsx index 3e8d08479a..352ca66274 100644 --- a/packages/client/ui-agent-preset/src/client/AgentPresetSeat.tsx +++ b/packages/client/ui-agent-preset/src/client/AgentPresetSeat.tsx @@ -9,10 +9,10 @@ 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' // 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 composer seat. */ @@ -62,34 +62,18 @@ export function AgentPresetSeat({ load, select, useAgentPresetSeat, locked, t }: } return ( - { setOpen(false) }} - items={state.options.map(option => ({ - id: option.id, - label: option.trust === 'user' ? `${option.id} · ${t('userTrust')}` : option.id, - }))} + { - setOpen(false) - void select(id) - }} - align="end" - portal - anchor={( - - )} + 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) }} /> ) } diff --git a/packages/client/ui-agent-preset/src/client/PresetMenu.tsx b/packages/client/ui-agent-preset/src/client/PresetMenu.tsx new file mode 100644 index 0000000000..bdf22ee56f --- /dev/null +++ b/packages/client/ui-agent-preset/src/client/PresetMenu.tsx @@ -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 ( + { 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={( + + )} + /> + ) +}