Merge branch 'stack/agent-profiles-8-authoring' into stack/agent-profiles-9-rename
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/client/ui-agent-preset/README.md
|
||||
README.md: 6261984835a70b6b669545acc8f9bfafc93ab64d
|
||||
README.zh.md: fb860812b1646f6b1912e4aedf56b00696957352
|
||||
README.md: c9aa370ebb7612cdc18bbe3d63c85cbb7097f69b
|
||||
README.zh.md: 707745c547f2172abb4d11cda09f3de1c5c1ec78
|
||||
|
||||
@@ -32,7 +32,7 @@ The row re-reads on `settings/changed` for its own namespace and on `connection/
|
||||
|
||||
A fourth surface, its own settings page (`settings.section` id `agent-presets`, ordered after Models — choosing a model is routine, composing an agent is the deployment-shaping act behind it): the roster as rows, and one composition open in a YAML editor at a time.
|
||||
|
||||
A shipped preset opens read-only. It is the known-good composition a local one is written against, so reading it is the point and overwriting it is not — the deployment's copy is what a broken local preset is compared against. Authoring therefore starts by duplicating: **New preset** copies the current default, and **Duplicate** copies any row, because a copy always lands in the local root regardless of where the text came from.
|
||||
A shipped preset opens read-only. It is the known-good composition a local one is written against, so reading it is the point and overwriting it is not — the deployment's copy is what a broken local preset is compared against. **Duplicate** copies any row, because a copy always lands in the local root regardless of where the text came from; **New preset** starts blank, since copying is already offered on the row being copied and a composition nobody named is text the author has to recognise as unwanted before deleting it.
|
||||
|
||||
An id becomes a directory name, so the editor mirrors the host's own containment rule (`[a-z0-9][a-z0-9-]*`) and refuses a name already in use — a create landing on an existing name would overwrite a preset the user never opened. Both checks are conveniences: the host re-applies them, along with the composition's shape, and its answer is what the editor reports on failure. A save that parses is still only a save; a composition naming a plugin that does not exist fails at the next session that selects it.
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ chip 以部署默认值打开,其选择是**暂存**的——该界面先于
|
||||
|
||||
第四个表层,独立的设置页(`settings.section`,id 为 `agent-presets`,排在「模型」之后——选模型是日常操作,而组装 agent 是它背后那件塑造部署形态的事):名单以行呈现,同一时刻有一份组装在 YAML 编辑器中打开。
|
||||
|
||||
随部署提供的 preset 以只读方式打开。它是本地 preset 据以编写的已知良好组装,因此能读到它正是意义所在,而覆写它则不是——部署自带的那一份正是用来对照有问题的本地 preset 的。因此创作从复制开始:**新建 preset** 复制当前默认值,**复制**则复制任意一行;无论文本来自何处,副本总是落在本地根目录,所以副本总是可写的。
|
||||
随部署提供的 preset 以只读方式打开。它是本地 preset 据以编写的已知良好组装,因此能读到它正是意义所在,而覆写它则不是——部署自带的那一份正是用来对照有问题的本地 preset 的。**复制**复制任意一行;无论文本来自何处,副本总是落在本地根目录,所以副本总是可写的。**新建 preset** 则从空白开始——复制这件事已经由被复制那一行自己提供,而一份没人指名的组装,只会让作者先认出它不是自己想要的、再把它删掉。
|
||||
|
||||
id 会成为目录名,因此编辑器复刻宿主自身的约束规则(`[a-z0-9][a-z0-9-]*`),并拒绝已被占用的名称——新建若落在已存在的名称上,就会覆盖用户从未打开过的 preset。这两项检查只是便利:宿主会连同组装的形状一起重新校验,失败时编辑器报告的正是宿主的答复。能解析的保存也仅仅是保存;引用了不存在插件的组装,会在下一个选择它的会话处失败。
|
||||
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -239,16 +239,18 @@ describe('opening a composition', () => {
|
||||
})
|
||||
|
||||
describe('creating a preset', () => {
|
||||
it('copies the default composition when no source is named', async () => {
|
||||
const { controller } = harness()
|
||||
it('starts blank when no source is named', async () => {
|
||||
const { controller, calls } = harness()
|
||||
await controller.load()
|
||||
const before = calls.length
|
||||
|
||||
await controller.createFrom()
|
||||
|
||||
// The default is the composition a new session gets, so it is the one
|
||||
// worth starting from — and a copy is writable wherever it came from.
|
||||
expect(draftOf(controller)).toMatchObject({ id: '', source: 'standard', creating: true, writable: true })
|
||||
expect(draftOf(controller).content).toBe('- id: tool-bash\n')
|
||||
// Copying is its own action on the row being copied, so this one is a copy
|
||||
// of nothing: no source to name, and no read to make.
|
||||
expect(draftOf(controller)).toMatchObject({ id: '', creating: true, writable: true, content: '' })
|
||||
expect(draftOf(controller).source).toBeUndefined()
|
||||
expect(calls).toHaveLength(before)
|
||||
})
|
||||
|
||||
it('copies a named preset', async () => {
|
||||
@@ -260,12 +262,12 @@ describe('creating a preset', () => {
|
||||
expect(draftOf(controller)).toMatchObject({ source: 'mine', creating: true, content: '- id: tool-read\n' })
|
||||
})
|
||||
|
||||
it('does nothing before the roster loaded', async () => {
|
||||
it('opens the blank editor without the roster, which it no longer reads', async () => {
|
||||
const { controller, calls } = harness()
|
||||
|
||||
await controller.createFrom()
|
||||
|
||||
expect(controller.store.getSnapshot().draft).toBeNull()
|
||||
expect(draftOf(controller)).toMatchObject({ id: '', creating: true, content: '' })
|
||||
expect(calls).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user