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;
|
# 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:
|
# 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
|
# pnpm run verify-translation-pairing --write packages/client/ui-agent-preset/README.md
|
||||||
README.md: 6261984835a70b6b669545acc8f9bfafc93ab64d
|
README.md: c9aa370ebb7612cdc18bbe3d63c85cbb7097f69b
|
||||||
README.zh.md: fb860812b1646f6b1912e4aedf56b00696957352
|
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 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.
|
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 编辑器中打开。
|
第四个表层,独立的设置页(`settings.section`,id 为 `agent-presets`,排在「模型」之后——选模型是日常操作,而组装 agent 是它背后那件塑造部署形态的事):名单以行呈现,同一时刻有一份组装在 YAML 编辑器中打开。
|
||||||
|
|
||||||
随部署提供的 preset 以只读方式打开。它是本地 preset 据以编写的已知良好组装,因此能读到它正是意义所在,而覆写它则不是——部署自带的那一份正是用来对照有问题的本地 preset 的。因此创作从复制开始:**新建 preset** 复制当前默认值,**复制**则复制任意一行;无论文本来自何处,副本总是落在本地根目录,所以副本总是可写的。
|
随部署提供的 preset 以只读方式打开。它是本地 preset 据以编写的已知良好组装,因此能读到它正是意义所在,而覆写它则不是——部署自带的那一份正是用来对照有问题的本地 preset 的。**复制**复制任意一行;无论文本来自何处,副本总是落在本地根目录,所以副本总是可写的。**新建 preset** 则从空白开始——复制这件事已经由被复制那一行自己提供,而一份没人指名的组装,只会让作者先认出它不是自己想要的、再把它删掉。
|
||||||
|
|
||||||
id 会成为目录名,因此编辑器复刻宿主自身的约束规则(`[a-z0-9][a-z0-9-]*`),并拒绝已被占用的名称——新建若落在已存在的名称上,就会覆盖用户从未打开过的 preset。这两项检查只是便利:宿主会连同组装的形状一起重新校验,失败时编辑器报告的正是宿主的答复。能解析的保存也仅仅是保存;引用了不存在插件的组装,会在下一个选择它的会话处失败。
|
id 会成为目录名,因此编辑器复刻宿主自身的约束规则(`[a-z0-9][a-z0-9-]*`),并拒绝已被占用的名称——新建若落在已存在的名称上,就会覆盖用户从未打开过的 preset。这两项检查只是便利:宿主会连同组装的形状一起重新校验,失败时编辑器报告的正是宿主的答复。能解析的保存也仅仅是保存;引用了不存在插件的组装,会在下一个选择它的会话处失败。
|
||||||
|
|
||||||
|
|||||||
@@ -82,7 +82,9 @@ function Editor({ draft, blocker, t, actions }: EditorProps): ReactNode {
|
|||||||
placeholder={t('presetIdPlaceholder')}
|
placeholder={t('presetIdPlaceholder')}
|
||||||
onChange={(event) => { actions.setId(event.target.value) }}
|
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>
|
</label>
|
||||||
)
|
)
|
||||||
: null}
|
: null}
|
||||||
@@ -195,8 +197,8 @@ export function AgentPresetSection(props: AgentPresetSectionProps): ReactNode {
|
|||||||
</button>
|
</button>
|
||||||
<span className={css.editorTitle}>
|
<span className={css.editorTitle}>
|
||||||
{draft.creating
|
{draft.creating
|
||||||
? `${t('newPreset')} · ${t('copyOf')} ${draft.source}`
|
? (draft.source === undefined ? t('newPreset') : `${t('newPreset')} · ${t('copyOf')} ${draft.source}`)
|
||||||
: `${draft.writable ? t('edit') : t('view')} · ${draft.name === '' ? draft.source : draft.name}`}
|
: `${draft.writable ? t('edit') : t('view')} · ${draft.name === '' ? draft.id : draft.name}`}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Editor draft={draft} blocker={blocker} t={t} actions={editorActions} />
|
<Editor draft={draft} blocker={blocker} t={t} actions={editorActions} />
|
||||||
|
|||||||
@@ -35,9 +35,10 @@ export interface PresetDraft {
|
|||||||
id: string
|
id: string
|
||||||
/**
|
/**
|
||||||
* The preset the text came from, shown while a new preset is unnamed so
|
* 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. */
|
/** Whether saving creates a preset rather than replacing one. */
|
||||||
creating: boolean
|
creating: boolean
|
||||||
/** Composition text being edited. */
|
/** 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
|
* Open a new, unnamed preset. With no argument it starts blank; copying is
|
||||||
* copy is taken from the default preset, which is the composition a new
|
* its own action, offered on the row being copied, so the two arrive at the
|
||||||
* session gets and therefore the one worth starting from.
|
* same editor by the route the author actually chose. Starting from some
|
||||||
* @param from - the preset to copy, or undefined for the default.
|
* 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.
|
* @returns once the composition loaded or the failure is on the page.
|
||||||
*/
|
*/
|
||||||
async createFrom(from?: string): Promise<void> {
|
async createFrom(from?: string): Promise<void> {
|
||||||
const source = from ?? this.store.getSnapshot().rows.find(row => row.isDefault)?.id
|
if (from === undefined) {
|
||||||
/* v8 ignore next -- the section only renders with a roster, and every roster marks a default */
|
this.set({
|
||||||
if (source === undefined) return
|
error: null,
|
||||||
await this.openDraft(source, true)
|
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> {
|
private async openDraft(source: string, creating: boolean): Promise<void> {
|
||||||
|
|||||||
@@ -239,16 +239,18 @@ describe('opening a composition', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('creating a preset', () => {
|
describe('creating a preset', () => {
|
||||||
it('copies the default composition when no source is named', async () => {
|
it('starts blank when no source is named', async () => {
|
||||||
const { controller } = harness()
|
const { controller, calls } = harness()
|
||||||
await controller.load()
|
await controller.load()
|
||||||
|
const before = calls.length
|
||||||
|
|
||||||
await controller.createFrom()
|
await controller.createFrom()
|
||||||
|
|
||||||
// The default is the composition a new session gets, so it is the one
|
// Copying is its own action on the row being copied, so this one is a copy
|
||||||
// worth starting from — and a copy is writable wherever it came from.
|
// of nothing: no source to name, and no read to make.
|
||||||
expect(draftOf(controller)).toMatchObject({ id: '', source: 'standard', creating: true, writable: true })
|
expect(draftOf(controller)).toMatchObject({ id: '', creating: true, writable: true, content: '' })
|
||||||
expect(draftOf(controller).content).toBe('- id: tool-bash\n')
|
expect(draftOf(controller).source).toBeUndefined()
|
||||||
|
expect(calls).toHaveLength(before)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('copies a named preset', async () => {
|
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' })
|
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()
|
const { controller, calls } = harness()
|
||||||
|
|
||||||
await controller.createFrom()
|
await controller.createFrom()
|
||||||
|
|
||||||
expect(controller.store.getSnapshot().draft).toBeNull()
|
expect(draftOf(controller)).toMatchObject({ id: '', creating: true, content: '' })
|
||||||
expect(calls).toHaveLength(0)
|
expect(calls).toHaveLength(0)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user