fix(web): pin preset before subagent header action

This commit is contained in:
Yichen Jiang
2026-08-10 19:53:58 +08:00
parent cec716717f
commit 9ce8340dd9
8 changed files with 77 additions and 8 deletions

View File

@@ -16,6 +16,10 @@ import { join } from 'node:path'
import type { Browser, Page } from 'playwright'
import { chromium } from 'playwright'
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 {
captureStableAria, compareOrRefreshGolden, launchWebScaffold, seedSession, watchConsole,
webSnapshotMode, type WebScaffold,
@@ -78,6 +82,60 @@ function seedLog(): string {
].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
* 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
// records `minimal` is what makes the header label a claim about the
// 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)
browser = await chromium.launch()
page = await newEnglishPage(browser)
@@ -222,6 +281,8 @@ describe('web e2e: agent-preset selection', () => {
await compareOrRefreshGolden(HEADER_EXPECTED, snapshot, 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
// the host would refuse to change.
expect(snapshot).not.toContain('button "Minimal mode"')

View File

@@ -2,3 +2,6 @@
- button "Seeded turn" [disabled]
- img
- text: Minimal mode
- button "1 subagent":
- text: 1 subagent
- img

View File

@@ -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: c4c7df4e6fbe0479cac4767247c1b10fd65aad77
README.zh.md: 84c02977ec18f89c06311b570428f92c2b459fb3
README.md: 008066114e9c49e5c74299979e24c27a4c9621c9
README.zh.md: e07d5994ae196cd03be7818fe4ade1aafda9aa55

View File

@@ -18,7 +18,7 @@ A session that has started is refused rather than queued: the host answers `agen
## 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

View File

@@ -18,7 +18,7 @@ chip 以部署默认值打开,其选择是**暂存**的——该界面先于
## 会话标题旁的标签
第三个表层,位于会话标题旁:**本会话**所运行的 preset作为静态装饰呈现。它在头部操作行中排在 subagent 列表之前。在那里放一个控件,等于承诺一次宿主会断然拒绝的切换。它从会话自身的摘要读取 preset——被恢复的会话运行的是它创建时的那一份而非今天的默认值——并在 General 行所读的同一份名单上解析显示名称。
第三个表层,位于会话标题旁:**本会话**所运行的 preset作为静态装饰呈现。在那里放一个控件等于承诺一次宿主会断然拒绝的切换。它从会话自身的摘要读取 preset——被恢复的会话运行的是它创建时的那一份而非今天的默认值——并在 General 行所读的同一份名单上解析显示名称。
## 它读什么、写什么

View File

@@ -157,7 +157,8 @@ export function apply(ctx: ClientContext): void {
const label = scope.slots.register({
name: 'conversation.session.header.actions',
id: 'agent-preset',
order: 0,
// Static session context occupies the header's leading negative-order band.
order: -10,
locale: 'settings.agentPreset',
inject: labelInjected,
}, AgentPresetLabel)

View File

@@ -304,7 +304,7 @@ describe('ui-agent-preset apply', () => {
expect(chip.component).toBe(AgentPresetSeat)
const label = slots.entries('conversation.session.header.actions')[0]!
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()
expect(slots.entries('conversation.hero.agentPreset')).toHaveLength(0)
expect(slots.entries('conversation.session.header.actions')).toHaveLength(0)

View File

@@ -38,7 +38,11 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
'conversation.session': { kind: 'single'; scope: 'session' }
/** Strict-session header above the resident conversation scrollport. */
'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 }
/**
* The conversation view ring: one list entry per view tab (chat here;