diff --git a/apps/web/tests/agent-preset-selection.e2e.ts b/apps/web/tests/agent-preset-selection.e2e.ts index 553a21bb6f..4a7f6eb819 100644 --- a/apps/web/tests/agent-preset-selection.e2e.ts +++ b/apps/web/tests/agent-preset-selection.e2e.ts @@ -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 { + 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"') diff --git a/apps/web/tests/snapshots/agent-preset-selection/header.expected.md b/apps/web/tests/snapshots/agent-preset-selection/header.expected.md index d627a21945..5a78c6461e 100644 --- a/apps/web/tests/snapshots/agent-preset-selection/header.expected.md +++ b/apps/web/tests/snapshots/agent-preset-selection/header.expected.md @@ -2,3 +2,6 @@ - button "Seeded turn" [disabled] - img - text: Minimal mode +- button "1 subagent": + - text: 1 subagent + - img diff --git a/packages/client/ui-agent-preset/README.i18n.yaml b/packages/client/ui-agent-preset/README.i18n.yaml index a7377027a2..ca91d9f97e 100644 --- a/packages/client/ui-agent-preset/README.i18n.yaml +++ b/packages/client/ui-agent-preset/README.i18n.yaml @@ -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 diff --git a/packages/client/ui-agent-preset/README.md b/packages/client/ui-agent-preset/README.md index c4c7df4e6f..008066114e 100644 --- a/packages/client/ui-agent-preset/README.md +++ b/packages/client/ui-agent-preset/README.md @@ -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 diff --git a/packages/client/ui-agent-preset/README.zh.md b/packages/client/ui-agent-preset/README.zh.md index 84c02977ec..e07d5994ae 100644 --- a/packages/client/ui-agent-preset/README.zh.md +++ b/packages/client/ui-agent-preset/README.zh.md @@ -18,7 +18,7 @@ chip 以部署默认值打开,其选择是**暂存**的——该界面先于 ## 会话标题旁的标签 -第三个表层,位于会话标题旁:**本会话**所运行的 preset,作为静态装饰呈现。它在头部操作行中排在 subagent 列表之前。在那里放一个控件,等于承诺一次宿主会断然拒绝的切换。它从会话自身的摘要读取 preset——被恢复的会话运行的是它创建时的那一份,而非今天的默认值——并在 General 行所读的同一份名单上解析显示名称。 +第三个表层,位于会话标题旁:**本会话**所运行的 preset,作为静态装饰呈现。在那里放一个控件,等于承诺一次宿主会断然拒绝的切换。它从会话自身的摘要读取 preset——被恢复的会话运行的是它创建时的那一份,而非今天的默认值——并在 General 行所读的同一份名单上解析显示名称。 ## 它读什么、写什么 diff --git a/packages/client/ui-agent-preset/src/client/index.ts b/packages/client/ui-agent-preset/src/client/index.ts index 0737992337..913639e9ab 100644 --- a/packages/client/ui-agent-preset/src/client/index.ts +++ b/packages/client/ui-agent-preset/src/client/index.ts @@ -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) diff --git a/packages/client/ui-agent-preset/tests/apply.spec.ts b/packages/client/ui-agent-preset/tests/apply.spec.ts index 4a6f58075f..23e1944948 100644 --- a/packages/client/ui-agent-preset/tests/apply.spec.ts +++ b/packages/client/ui-agent-preset/tests/apply.spec.ts @@ -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) diff --git a/packages/client/ui-conversation/src/client/contract/slots.ts b/packages/client/ui-conversation/src/client/contract/slots.ts index c0749fcb46..bbca9254bc 100644 --- a/packages/client/ui-conversation/src/client/contract/slots.ts +++ b/packages/client/ui-conversation/src/client/contract/slots.ts @@ -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;