fix(web): start a new preset blank instead of copying the default

Copying is already its own action, offered on the row being copied, so the two
routes now arrive at the same editor by whichever one the author chose. **New
preset** starting from some preset nobody named put a composition in the editor
that the author had to recognise as unwanted before deleting it — and the
preset it copied was the deployment default, which is the one least likely to
be what someone reaching for "new" wanted.

A blank draft has no source, so it names none and makes no read: the editor's
"copied from" line and header clause appear only when there is a source.
This commit is contained in:
Yichen Jiang
2026-08-07 11:18:22 +08:00
parent 9fc1430b79
commit 8d6b6098cb
6 changed files with 45 additions and 25 deletions

View File

@@ -82,7 +82,9 @@ function Editor({ draft, blocker, t, actions }: EditorProps): ReactNode {
placeholder={t('presetIdPlaceholder')}
onChange={(event) => { actions.setId(event.target.value) }}
/>
<span className={css.hint}>{`${t('copyOf')} ${draft.source}`}</span>
{draft.source === undefined
? null
: <span className={css.hint}>{`${t('copyOf')} ${draft.source}`}</span>}
</label>
)
: null}
@@ -195,8 +197,8 @@ export function AgentPresetSection(props: AgentPresetSectionProps): ReactNode {
</button>
<span className={css.editorTitle}>
{draft.creating
? `${t('newPreset')} · ${t('copyOf')} ${draft.source}`
: `${draft.writable ? t('edit') : t('view')} · ${draft.name === '' ? draft.source : draft.name}`}
? (draft.source === undefined ? t('newPreset') : `${t('newPreset')} · ${t('copyOf')} ${draft.source}`)
: `${draft.writable ? t('edit') : t('view')} · ${draft.name === '' ? draft.id : draft.name}`}
</span>
</div>
<Editor draft={draft} blocker={blocker} t={t} actions={editorActions} />

View File

@@ -35,9 +35,10 @@ export interface PresetDraft {
id: string
/**
* The preset the text came from, shown while a new preset is unnamed so
* the editor can say what it is a copy of.
* the editor can say what it is a copy of. Absent on a preset started
* blank, which is a copy of nothing.
*/
source: string
source?: string
/** Whether saving creates a preset rather than replacing one. */
creating: boolean
/** Composition text being edited. */
@@ -169,17 +170,32 @@ export class AgentPresetSectionController {
}
/**
* Open a copy of one preset as a new, unnamed preset. With no argument the
* copy is taken from the default preset, which is the composition a new
* session gets and therefore the one worth starting from.
* @param from - the preset to copy, or undefined for the default.
* Open a new, unnamed preset. With no argument it starts blank; copying is
* its own action, offered on the row being copied, so the two arrive at the
* same editor by the route the author actually chose. Starting from some
* preset nobody named would put text in the editor that the author has to
* recognise as unwanted before deleting it.
* @param from - the preset to copy, or undefined to start blank.
* @returns once the composition loaded or the failure is on the page.
*/
async createFrom(from?: string): Promise<void> {
const source = from ?? this.store.getSnapshot().rows.find(row => row.isDefault)?.id
/* v8 ignore next -- the section only renders with a roster, and every roster marks a default */
if (source === undefined) return
await this.openDraft(source, true)
if (from === undefined) {
this.set({
error: null,
draft: {
id: '',
creating: true,
content: '',
writable: true,
name: '',
description: '',
saving: false,
error: null,
},
})
return
}
await this.openDraft(from, true)
}
private async openDraft(source: string, creating: boolean): Promise<void> {