feat(web): order the shipped presets, and edit one on its own screen

The picker listed presets alphabetically by id, so the shipped set read
cordis, minimal, standard — reverse order of capability. A preset may now
declare `order` in its metadata; the shipped three declare 1/2/3 and read
standard, minimal, cordis. A preset that declares none sorts behind those
that do, then by id, so authored presets stay stable.

Editing had nowhere good to live. Inside a card it was squeezed into a
~268px column; hanging off the end of the grid it was orphaned from the card
it edits. It now replaces the list: a back link, what is being edited, and
the form at full width. One thing on screen at a time, which is what the
form's height wanted all along.

Cards in different grid rows sized independently, so a short description made
a short card. `grid-auto-rows: 1fr` makes every row the same height.

The trust badge lost its pill when the card CSS was rewritten, and `In use`
never had one; both are tags now. Icon labels moved from `title` to a drawn
tooltip — the native one waits about a second, which reads as nothing
happening.
This commit is contained in:
Yichen Jiang
2026-08-05 17:04:57 +08:00
parent 4f41608cbc
commit 64fc536982
14 changed files with 202 additions and 45 deletions

View File

@@ -61,7 +61,12 @@ export async function scanRoot(root: PresetRoot): Promise<AgentPreset[]> {
const metadata = await readPresetMetadata(directory)
found.push({ id: child.name, trust: root.trust, path, ...metadata })
}
return found.sort((left, right) => left.id.localeCompare(right.id))
// Declared order first so the shipped set reads by capability; everything
// else falls back to the id, which keeps authored presets stable.
return found.sort((left, right) => {
const byOrder = (left.order ?? Number.POSITIVE_INFINITY) - (right.order ?? Number.POSITIVE_INFINITY)
return byOrder === 0 ? left.id.localeCompare(right.id) : byOrder
})
}
/**

View File

@@ -30,6 +30,12 @@ export interface PresetMetadata {
readonly name?: string
/** One sentence on what this preset is for. */
readonly description?: string
/**
* Position within its group; lower comes first. A preset that declares
* none sorts after every preset that does, then by id — so the shipped set
* can read in capability order while authored ones stay alphabetical.
*/
readonly order?: number
}
/** A non-empty trimmed string, or undefined for anything else. */
@@ -68,9 +74,13 @@ export async function readPresetMetadata(directory: string): Promise<PresetMetad
const record = parsed as Record<string, unknown>
const name = text(record.name)
const description = text(record.description)
const order = typeof record.order === 'number' && Number.isFinite(record.order)
? record.order
: undefined
return {
...name === undefined ? {} : { name },
...description === undefined ? {} : { description },
...order === undefined ? {} : { order },
}
}
@@ -85,9 +95,11 @@ export async function readPresetMetadata(directory: string): Promise<PresetMetad
export function renderPresetMetadata(metadata: PresetMetadata): string | undefined {
const name = text(metadata.name)
const description = text(metadata.description)
if (name === undefined && description === undefined) return undefined
const { order } = metadata
if (name === undefined && description === undefined && order === undefined) return undefined
return yaml.dump({
...name === undefined ? {} : { name },
...description === undefined ? {} : { description },
...order === undefined ? {} : { order },
}, { lineWidth: -1 })
}

View File

@@ -19,6 +19,8 @@ export interface AgentPreset {
readonly name?: string
/** One sentence on what this preset is for, when it published one. */
readonly description?: string
/** Declared position within its group; absent sorts after those that declare one. */
readonly order?: number
}
/** One directory scanned for preset subdirectories. */