Przeglądaj źródła

fix(web): pin preset before subagent header action

Yichen Jiang 1 miesiąc temu
rodzic
commit
9ce8340dd9

+ 62 - 1
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<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"')

+ 3 - 0
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

+ 2 - 2
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

+ 1 - 1
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
 

+ 1 - 1
packages/client/ui-agent-preset/README.zh.md

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

+ 2 - 1
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)

+ 1 - 1
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)

+ 5 - 1
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;