feat(web): draft a custom preset in a creator-mode session

The settings section gains the conversational authoring entry beside
copying: a dashed add-card (the Models page's affordance) that stages the
self-referential cordis preset and starts a new session on it, closing the
settings panel through the shell's new section owner-prop `close` — the
one shell affordance a section receives, for flows that leave settings.

The seat learns the difference between picking and staging: `select()`'s
immediate apply meets the still-current running session and drops the
stage as unservable, so the entry uses `stage()` and leaves the apply to
the list-change applier that fires when the started session becomes
current. `load()` stops regressing the display when its reply lands after
an applied stage was consumed — staged pick first, then the composition
the current session already carries, then the deployment default.

The authoring lane connects a workspace and drives the gesture to a
composed host session; the section goldens gain the entry.
This commit is contained in:
Yichen Jiang
2026-08-09 00:19:18 +08:00
parent 564337997a
commit 43e93c6a67
16 changed files with 288 additions and 23 deletions

View File

@@ -20,7 +20,7 @@ import {
captureStableAria, compareOrRefreshGolden, launchWebScaffold, watchConsole,
webSnapshotMode, type WebScaffold,
} from './scaffold.ts'
import { ZH_BROWSER_LOCALE, saveFailureShot } from './support.ts'
import { ZH_BROWSER_LOCALE, connectFreshWorkspaceZh, saveFailureShot } from './support.ts'
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/agent-preset-authoring', import.meta.url))
const SECTION_EXPECTED = join(SNAPSHOT_DIR, 'section.expected.md')
@@ -174,6 +174,39 @@ describe('web e2e: agent-preset authoring is a host-side copy', () => {
expect(await dialog.getByText('标准模式').count()).toBeGreaterThan(0)
}, 60_000)
it('starts a creator-mode session from the section', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-preset-authoring-creator'))
// Without a workspace the flow only stages (there is no session to land
// in until one is connected); connect first so the gesture carries all
// the way to a composed host session.
await settingsDialog().getByRole('button', { name: '关闭' }).last().click()
await connectFreshWorkspaceZh(page, scaffold.workspaceCwd)
await page.getByRole('button', { name: '设置', exact: true }).click()
const dialog = settingsDialog()
await dialog.waitFor({ timeout: 10_000 })
await dialog.getByRole('button', { name: 'Agent 预设' }).click()
await dialog.getByRole('button', { name: '用「创造模式」创作自定义预设' }).click()
// Leaving settings is part of the gesture: the flow lands on the
// new-session screen with the self-referential preset staged, and the
// blank session the flow produces composes from it on the host.
await dialog.waitFor({ state: 'detached', timeout: 10_000 })
await page.getByRole('button', { name: '创造模式' }).waitFor({ timeout: 10_000 })
await expect.poll(async () => {
const response = await fetch(`${scaffold.baseUrl}/api/session.list`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
type: 'client-request', rpcId: 'creator-draft-stage', method: 'session.list', payload: {},
}),
})
const body = await response.json() as {
result: { value?: { sessions: unknown[] } }
}
return JSON.stringify(body.result.value?.sessions ?? body.result)
}, { timeout: 15_000 }).toContain('"agentPreset":"cordis"')
}, 60_000)
it('drove every surface without a page error or a stream warning', () => {
expect(tripwire.pageErrors).toEqual([])
expect(tripwire.warnings).toEqual([])

View File

@@ -76,3 +76,6 @@
- paragraph:
- text: 预设文件:
- code: {{presetRoot}}/my-agent
- button "用「创造模式」创作自定义预设":
- img
- text: 用「创造模式」创作自定义预设

View File

@@ -58,3 +58,6 @@
- 'button "复制: 创造模式"':
- img
- text: 复制
- button "用「创造模式」创作自定义预设":
- img
- text: 用「创造模式」创作自定义预设