feat(agent-presets): give a preset a name and a description

A picker showed directory names, so the settings page could only ever list
`standard` / `core-web` / `cordis` and hope the reader knew what they meant.
A preset may now publish display text in an optional `preset.yml` beside
its composition, and the section renders cards — name, description, and the
one in use — instead of rows.

The file carries display text ONLY. `id` is the directory name and `trust`
comes from the root a preset was discovered under, so neither is writable
there: otherwise a locally authored preset could name itself into the
shipped set. It is a separate file because a composition is a top-level list
of plugin rows — YAML cannot carry sibling keys beside it, and a fake
metadata row would hand the Loader something to load.

Every read failure degrades to no metadata; absent, malformed, wrongly
typed, and blank all mean the same thing and the picker falls back to the
id. Presentation is not capability: a preset whose name is broken still
mounts.

The editor gained name and description fields above the YAML, and clearing
both removes the file rather than storing a blank name.
This commit is contained in:
Yichen Jiang
2026-08-05 16:30:14 +08:00
parent 5b93f48f78
commit 7281615d44
27 changed files with 593 additions and 68 deletions
+9 -2
View File
@@ -2544,6 +2544,8 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
id: preset.id,
trust: preset.trust,
isDefault: preset.id === defaultId,
...preset.name === undefined ? {} : { name: preset.name },
...preset.description === undefined ? {} : { description: preset.description },
})),
authorable: presets.authorable,
})
@@ -2615,6 +2617,8 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
trust: preset.trust,
content: await presets.read(preset.id),
writable: preset.trust === 'user' && presets.authorable,
...preset.name === undefined ? {} : { name: preset.name },
...preset.description === undefined ? {} : { description: preset.description },
})
} catch (error: unknown) {
return err(request, presetError(agentPreset, error))
@@ -2622,11 +2626,14 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
},
async write(request) {
const { agentPreset, content } = request.payload
const { agentPreset, content, name, description } = request.payload
const presets = ctx.get('agentPresets')
if (presets === undefined) return err(request, noRoster(agentPreset))
try {
await presets.write(agentPreset, content)
await presets.write(agentPreset, content, {
...name === undefined ? {} : { name },
...description === undefined ? {} : { description },
})
return ok(request, { agentPreset })
} catch (error: unknown) {
return err(request, presetError(agentPreset, error))
@@ -14,6 +14,8 @@ export const agentPresetEntrySchema = z.object({
id: z.string().min(1),
trust: z.union([z.literal('system'), z.literal('user')]),
isDefault: z.boolean(),
name: z.string().optional(),
description: z.string().optional(),
}) satisfies z.ZodType<Wire<AgentPresetEntry>>
/** agentPreset.list request payload. */
@@ -48,12 +50,16 @@ export const agentPresetReadValueSchema = z.object({
trust: z.union([z.literal('system'), z.literal('user')]),
content: z.string(),
writable: z.boolean(),
name: z.string().optional(),
description: z.string().optional(),
}) satisfies z.ZodType<Wire<ResponseValue<'agentPreset.read'>>>
/** agentPreset.write request payload. */
export const agentPresetWriteRequestSchema = z.object({
agentPreset: z.string().min(1),
content: z.string(),
name: z.string().optional(),
description: z.string().optional(),
}) satisfies z.ZodType<Wire<RequestPayload<'agentPreset.write'>>>
/** agentPreset.write response value. */
@@ -24,6 +24,15 @@ export interface AgentPresetEntry {
readonly trust: 'system' | 'user'
/** Whether a session that names no preset gets this one. */
readonly isDefault: boolean
/**
* Display name the preset published, absent when it published none. A
* surface falls back to {@link id}; it is never a second identity, and it
* never decides trust — a locally authored preset cannot name itself into
* the shipped set.
*/
readonly name?: string
/** One sentence on what the preset is for, when it published one. */
readonly description?: string
}
/** agent-preset-domain unary methods (the map key agentPreset.* of RpcMethodMap). */
@@ -56,14 +65,21 @@ export interface AgentPresetsApi {
* is reconnaissance and writing one is arbitrary capability.
*/
read(request: RpcRequest<{ agentPreset: string }>):
Promise<RpcResponse<{ agentPreset: string; trust: 'system' | 'user'; content: string; writable: boolean }>>
Promise<RpcResponse<{
agentPreset: string
trust: 'system' | 'user'
content: string
writable: boolean
name?: string
description?: string
}>>
/**
* Create or replace a locally authored preset. Shipped presets are refused;
* the text is shape-checked before it lands, so a save cannot leave a file no
* session could load.
*/
write(request: RpcRequest<{ agentPreset: string; content: string }>):
write(request: RpcRequest<{ agentPreset: string; content: string; name?: string; description?: string }>):
Promise<RpcResponse<{ agentPreset: string }>>
/** Delete a locally authored preset. Shipped presets are refused. */