fix(web): pin preset before subagent header action
This commit is contained in:
@@ -16,6 +16,10 @@ import { join } from 'node:path'
|
|||||||
import type { Browser, Page } from 'playwright'
|
import type { Browser, Page } from 'playwright'
|
||||||
import { chromium } from 'playwright'
|
import { chromium } from 'playwright'
|
||||||
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
|
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
|
||||||
|
import {
|
||||||
|
SESSION_FORMAT_VERSION, SessionId as sessionId, type SessionEvent, type SessionId,
|
||||||
|
} from '@deepseek-ai/dsh-session'
|
||||||
|
import { snapshotSubagentDescriptor } from '@deepseek-ai/dsh-subagent'
|
||||||
import {
|
import {
|
||||||
captureStableAria, compareOrRefreshGolden, launchWebScaffold, seedSession, watchConsole,
|
captureStableAria, compareOrRefreshGolden, launchWebScaffold, seedSession, watchConsole,
|
||||||
webSnapshotMode, type WebScaffold,
|
webSnapshotMode, type WebScaffold,
|
||||||
@@ -78,6 +82,60 @@ function seedLog(): string {
|
|||||||
].join('\n')
|
].join('\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persist one child so the assembled header snapshot exercises both action
|
||||||
|
* contributors whose relative order is the product contract under test.
|
||||||
|
* @param scaffold - the booted Web scaffold.
|
||||||
|
* @param parentId - the seeded session whose header the browser opens.
|
||||||
|
*/
|
||||||
|
async function seedSubagent(scaffold: WebScaffold, parentId: SessionId): Promise<void> {
|
||||||
|
const childId = sessionId('agent-preset-selection-child')
|
||||||
|
const createdAt = 1784974100100
|
||||||
|
await scaffold.ctx.sessionPersistence.create({
|
||||||
|
version: SESSION_FORMAT_VERSION,
|
||||||
|
id: childId,
|
||||||
|
createdAt,
|
||||||
|
cwd: scaffold.workspaceCwd,
|
||||||
|
parentSession: parentId,
|
||||||
|
origin: 'subagent',
|
||||||
|
delegationDepth: 1,
|
||||||
|
agentPreset: 'minimal',
|
||||||
|
})
|
||||||
|
await scaffold.ctx.sessionPersistence.append(childId, [
|
||||||
|
{
|
||||||
|
type: 'turn/start',
|
||||||
|
seq: 0,
|
||||||
|
time: createdAt,
|
||||||
|
data: { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'user/message',
|
||||||
|
seq: 1,
|
||||||
|
time: createdAt + 1,
|
||||||
|
data: {
|
||||||
|
content: [{ type: 'text', text: 'Check the session-header action order.' }],
|
||||||
|
source: { kind: 'user' },
|
||||||
|
},
|
||||||
|
surfaceOp: 'append',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'subagent/descriptor',
|
||||||
|
seq: 2,
|
||||||
|
time: createdAt + 2,
|
||||||
|
data: snapshotSubagentDescriptor({
|
||||||
|
mode: 'one-shot', provider: 'spawn', label: 'header order probe',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'turn/end',
|
||||||
|
seq: 3,
|
||||||
|
time: createdAt + 3,
|
||||||
|
data: { turn: 1, reason: { kind: 'completed' } },
|
||||||
|
},
|
||||||
|
] as SessionEvent[])
|
||||||
|
await scaffold.ctx.sessionProjectionCache.coldSnapshot(childId)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The preset the host reports for the blank session the workspace connect
|
* The preset the host reports for the blank session the workspace connect
|
||||||
* produced. Addressed by id rather than by scanning the serialized list: the
|
* produced. Addressed by id rather than by scanning the serialized list: the
|
||||||
@@ -120,7 +178,8 @@ describe('web e2e: agent-preset selection', () => {
|
|||||||
// A resumed session runs what it was created with; seeding one that
|
// A resumed session runs what it was created with; seeding one that
|
||||||
// records `minimal` is what makes the header label a claim about the
|
// records `minimal` is what makes the header label a claim about the
|
||||||
// session rather than an echo of the current default.
|
// session rather than an echo of the current default.
|
||||||
await seedSession(scaffold, seedLog(), SEED_ID, 'minimal')
|
const seededId = await seedSession(scaffold, seedLog(), SEED_ID, 'minimal')
|
||||||
|
await seedSubagent(scaffold, seededId)
|
||||||
await seedWorkspaceSkill(scaffold.workspaceCwd)
|
await seedWorkspaceSkill(scaffold.workspaceCwd)
|
||||||
browser = await chromium.launch()
|
browser = await chromium.launch()
|
||||||
page = await newEnglishPage(browser)
|
page = await newEnglishPage(browser)
|
||||||
@@ -222,6 +281,8 @@ describe('web e2e: agent-preset selection', () => {
|
|||||||
|
|
||||||
await compareOrRefreshGolden(HEADER_EXPECTED, snapshot, MODE)
|
await compareOrRefreshGolden(HEADER_EXPECTED, snapshot, MODE)
|
||||||
expect(snapshot).toContain('Minimal mode')
|
expect(snapshot).toContain('Minimal mode')
|
||||||
|
expect(snapshot).toContain('button "1 subagent"')
|
||||||
|
expect(snapshot.indexOf('Minimal mode')).toBeLessThan(snapshot.indexOf('button "1 subagent"'))
|
||||||
// Static chrome, not a control: the header can only report a composition
|
// Static chrome, not a control: the header can only report a composition
|
||||||
// the host would refuse to change.
|
// the host would refuse to change.
|
||||||
expect(snapshot).not.toContain('button "Minimal mode"')
|
expect(snapshot).not.toContain('button "Minimal mode"')
|
||||||
|
|||||||
@@ -2,3 +2,6 @@
|
|||||||
- button "Seeded turn" [disabled]
|
- button "Seeded turn" [disabled]
|
||||||
- img
|
- img
|
||||||
- text: Minimal mode
|
- text: Minimal mode
|
||||||
|
- button "1 subagent":
|
||||||
|
- text: 1 subagent
|
||||||
|
- img
|
||||||
|
|||||||
@@ -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: c4c7df4e6fbe0479cac4767247c1b10fd65aad77
|
README.md: 008066114e9c49e5c74299979e24c27a4c9621c9
|
||||||
README.zh.md: 84c02977ec18f89c06311b570428f92c2b459fb3
|
README.zh.md: e07d5994ae196cd03be7818fe4ade1aafda9aa55
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ A session that has started is refused rather than queued: the host answers `agen
|
|||||||
|
|
||||||
## The session-header label
|
## The session-header label
|
||||||
|
|
||||||
A third surface, beside the session title: the preset THIS session runs, as static chrome. It precedes the subagent catalog in the header action row. A control there would promise a switch the host refuses outright. It reads the preset from the session's own summary — a resumed session runs what it was created with, not today's default — and resolves the display name against the same roster the General row reads.
|
A third surface, beside the session title: the preset THIS session runs, as static chrome. A control there would promise a switch the host refuses outright. It reads the preset from the session's own summary — a resumed session runs what it was created with, not today's default — and resolves the display name against the same roster the General row reads.
|
||||||
|
|
||||||
## What it reads and writes
|
## What it reads and writes
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ chip 以部署默认值打开,其选择是**暂存**的——该界面先于
|
|||||||
|
|
||||||
## 会话标题旁的标签
|
## 会话标题旁的标签
|
||||||
|
|
||||||
第三个表层,位于会话标题旁:**本会话**所运行的 preset,作为静态装饰呈现。它在头部操作行中排在 subagent 列表之前。在那里放一个控件,等于承诺一次宿主会断然拒绝的切换。它从会话自身的摘要读取 preset——被恢复的会话运行的是它创建时的那一份,而非今天的默认值——并在 General 行所读的同一份名单上解析显示名称。
|
第三个表层,位于会话标题旁:**本会话**所运行的 preset,作为静态装饰呈现。在那里放一个控件,等于承诺一次宿主会断然拒绝的切换。它从会话自身的摘要读取 preset——被恢复的会话运行的是它创建时的那一份,而非今天的默认值——并在 General 行所读的同一份名单上解析显示名称。
|
||||||
|
|
||||||
## 它读什么、写什么
|
## 它读什么、写什么
|
||||||
|
|
||||||
|
|||||||
@@ -157,7 +157,8 @@ export function apply(ctx: ClientContext): void {
|
|||||||
const label = scope.slots.register({
|
const label = scope.slots.register({
|
||||||
name: 'conversation.session.header.actions',
|
name: 'conversation.session.header.actions',
|
||||||
id: 'agent-preset',
|
id: 'agent-preset',
|
||||||
order: 0,
|
// Static session context occupies the header's leading negative-order band.
|
||||||
|
order: -10,
|
||||||
locale: 'settings.agentPreset',
|
locale: 'settings.agentPreset',
|
||||||
inject: labelInjected,
|
inject: labelInjected,
|
||||||
}, AgentPresetLabel)
|
}, AgentPresetLabel)
|
||||||
|
|||||||
@@ -304,7 +304,7 @@ describe('ui-agent-preset apply', () => {
|
|||||||
expect(chip.component).toBe(AgentPresetSeat)
|
expect(chip.component).toBe(AgentPresetSeat)
|
||||||
const label = slots.entries('conversation.session.header.actions')[0]!
|
const label = slots.entries('conversation.session.header.actions')[0]!
|
||||||
expect(label.component).toBe(AgentPresetLabel)
|
expect(label.component).toBe(AgentPresetLabel)
|
||||||
expect(label.options).toMatchObject({ id: 'agent-preset', order: 0 })
|
expect(label.options).toMatchObject({ id: 'agent-preset', order: -10 })
|
||||||
await fiber.dispose()
|
await fiber.dispose()
|
||||||
expect(slots.entries('conversation.hero.agentPreset')).toHaveLength(0)
|
expect(slots.entries('conversation.hero.agentPreset')).toHaveLength(0)
|
||||||
expect(slots.entries('conversation.session.header.actions')).toHaveLength(0)
|
expect(slots.entries('conversation.session.header.actions')).toHaveLength(0)
|
||||||
|
|||||||
@@ -38,7 +38,11 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|||||||
'conversation.session': { kind: 'single'; scope: 'session' }
|
'conversation.session': { kind: 'single'; scope: 'session' }
|
||||||
/** Strict-session header above the resident conversation scrollport. */
|
/** Strict-session header above the resident conversation scrollport. */
|
||||||
'conversation.session.header': { kind: 'single'; scope: 'session' }
|
'conversation.session.header': { kind: 'single'; scope: 'session' }
|
||||||
/** Session-header actions contributed by feature plugins. */
|
/**
|
||||||
|
* Session-header actions contributed by feature plugins. Entries render
|
||||||
|
* by ascending `order`; negative values are reserved for static session
|
||||||
|
* context that precedes interactive actions.
|
||||||
|
*/
|
||||||
'conversation.session.header.actions': { kind: 'list'; scope: 'session'; owner: ConversationHeaderActionOwnerProps }
|
'conversation.session.header.actions': { kind: 'list'; scope: 'session'; owner: ConversationHeaderActionOwnerProps }
|
||||||
/**
|
/**
|
||||||
* The conversation view ring: one list entry per view tab (chat here;
|
* The conversation view ring: one list entry per view tab (chat here;
|
||||||
|
|||||||
Reference in New Issue
Block a user