Преглед на файлове

Merge remote-tracking branch 'origin/master' into feat/read-image-context

creatixchu преди 1 месец
родител
ревизия
74adae57e9
променени са 57 файла, в които са добавени 352 реда и са изтрити 137 реда
  1. 1 1
      apps/cli/config/agent-presets/code/preset.yml
  2. 1 1
      apps/cli/config/agent-presets/cordis/preset.yml
  3. 1 1
      apps/cli/config/agent-presets/minimal/preset.yml
  4. 1 1
      apps/cli/config/agent-presets/standard/preset.yml
  5. 1 1
      apps/web/tests/agent-preset-authoring.e2e.ts
  6. 72 11
      apps/web/tests/agent-preset-selection.e2e.ts
  7. 5 5
      apps/web/tests/snapshots/agent-preset-authoring/created.expected.md
  8. 4 4
      apps/web/tests/snapshots/agent-preset-authoring/damaged.expected.md
  9. 4 4
      apps/web/tests/snapshots/agent-preset-authoring/section.expected.md
  10. 4 1
      apps/web/tests/snapshots/agent-preset-selection/header.expected.md
  11. 2 2
      apps/web/tests/snapshots/agent-preset-selection/hero.expected.md
  12. 5 5
      apps/web/tests/snapshots/agent-preset-selection/menu.expected.md
  13. 1 1
      apps/web/tests/snapshots/code-mode-round/ui.expected.md
  14. 1 1
      apps/web/tests/snapshots/cordis-tool-round/ui.expected.md
  15. 1 1
      apps/web/tests/snapshots/fresh-round-trip/ui.expected.md
  16. 1 1
      apps/web/tests/snapshots/goal-multi-turn-actions/ui.expected.md
  17. 2 2
      apps/web/tests/snapshots/lifecycle-chrome/hero.expected.md
  18. 2 2
      apps/web/tests/snapshots/lifecycle-chrome/plan-active.expected.md
  19. 1 1
      apps/web/tests/snapshots/lifecycle-chrome/reloaded.expected.md
  20. 1 1
      apps/web/tests/snapshots/live-interactions/cancel.expected.md
  21. 1 1
      apps/web/tests/snapshots/live-interactions/error-auth.expected.md
  22. 1 1
      apps/web/tests/snapshots/live-interactions/loading.expected.md
  23. 1 1
      apps/web/tests/snapshots/live-interactions/retry.expected.md
  24. 1 1
      apps/web/tests/snapshots/plan-review/approved.expected.md
  25. 1 1
      apps/web/tests/snapshots/question-composer/answered.expected.md
  26. 1 1
      apps/web/tests/snapshots/queue-actions/collapsed.expected.md
  27. 1 1
      apps/web/tests/snapshots/queue-actions/editing.expected.md
  28. 1 1
      apps/web/tests/snapshots/queue-actions/layout.expected.md
  29. 1 1
      apps/web/tests/snapshots/queue-actions/preserved.expected.md
  30. 1 1
      apps/web/tests/snapshots/queue-actions/ui.expected.md
  31. 1 1
      apps/web/tests/snapshots/skill-user-invoke/ui.expected.md
  32. 1 1
      apps/web/tests/snapshots/steer-all/mid-steer.expected.md
  33. 1 1
      apps/web/tests/snapshots/steer-all/settled.expected.md
  34. 1 1
      apps/web/tests/snapshots/steering/mid-steer.expected.md
  35. 1 1
      apps/web/tests/snapshots/steering/settled.expected.md
  36. 1 1
      apps/web/tests/snapshots/turn-tail-actions/running.expected.md
  37. 1 1
      apps/web/tests/snapshots/turn-tail-actions/settled.expected.md
  38. 1 1
      apps/web/tests/snapshots/web-search-round/ui.expected.md
  39. 1 1
      apps/web/tests/steering.e2e.ts
  40. 2 2
      packages/client/ui-agent-preset/README.i18n.yaml
  41. 2 0
      packages/client/ui-agent-preset/README.md
  42. 2 0
      packages/client/ui-agent-preset/README.zh.md
  43. 4 2
      packages/client/ui-agent-preset/src/client/AgentPresetLabel.tsx
  44. 6 6
      packages/client/ui-agent-preset/src/client/AgentPresetRow.tsx
  45. 17 12
      packages/client/ui-agent-preset/src/client/AgentPresetSeat.tsx
  46. 21 12
      packages/client/ui-agent-preset/src/client/AgentPresetSection.tsx
  47. 13 12
      packages/client/ui-agent-preset/src/client/PresetMenu.tsx
  48. 2 1
      packages/client/ui-agent-preset/src/client/index.ts
  49. 74 0
      packages/client/ui-agent-preset/src/client/locales.ts
  50. 1 1
      packages/client/ui-agent-preset/tests/apply.spec.ts
  51. 17 5
      packages/client/ui-agent-preset/tests/components.spec.tsx
  52. 33 0
      packages/client/ui-agent-preset/tests/locales.spec.ts
  53. 19 13
      packages/client/ui-agent-preset/tests/section.spec.tsx
  54. 5 1
      packages/client/ui-conversation/src/client/contract/slots.ts
  55. 2 2
      packages/preset/agent-presets/README.i18n.yaml
  56. 1 1
      packages/preset/agent-presets/README.md
  57. 1 1
      packages/preset/agent-presets/README.zh.md

+ 1 - 1
apps/cli/config/agent-presets/code/preset.yml

@@ -1,3 +1,3 @@
 name: 代码模式
-description: 标准模式的工具改为 Code Mode 呈现:模型写一段 TypeScript 调用 SDK,一次执行代替多轮工具调用
+description: 具备标准模式的全部能力,并通过 Code Mode SDK 呈现工具,让模型用一个 TypeScript 程序组合多步操作
 order: 2

+ 1 - 1
apps/cli/config/agent-presets/cordis/preset.yml

@@ -1,3 +1,3 @@
 name: 创造模式
-description: 标准模式加上自指工具集,可以读改自己运行的这套组装,并据此创作新的预设
+description: 用于创建自定义 Agent preset:具备标准模式的全部能力,并提供运行时检查、插件实验和 preset 创作指导
 order: 4

+ 1 - 1
apps/cli/config/agent-presets/minimal/preset.yml

@@ -1,3 +1,3 @@
 name: 极简模式
-description: 只向模型呈现 bash 与 str_replace_editor,适合 benchmark 与最小复现。
+description: 仅提供 bash 与 str_replace_editor 的双工具编码 Agent,用于基准测试和最小复现。
 order: 3

+ 1 - 1
apps/cli/config/agent-presets/standard/preset.yml

@@ -1,3 +1,3 @@
 name: 标准模式
-description: 完整的编码 agent:文件读写、shell、检索、计划、委派与工作流。
+description: 功能完整的编码 Agent,支持文件编辑、Shell、文件与网页检索、Skills、计划、目标、子代理和工作流。
 order: 1

+ 1 - 1
apps/web/tests/agent-preset-authoring.e2e.ts

@@ -161,7 +161,7 @@ describe('web e2e: agent-preset authoring is a host-side copy', () => {
     expect(composition).toBe(await readFile(join(SHIPPED_PRESETS, 'minimal', 'agent.cordis.yml'), 'utf8'))
     const metadata = await readFile(join(userRoot, 'my-agent', 'preset.yml'), 'utf8')
     expect(metadata).toContain('name: 我的模式')
-    expect(metadata).toContain('description: 只向模型呈现 bash 与 str_replace_editor,适合 benchmark 与最小复现。')
+    expect(metadata).toContain('description: 仅提供 bash 与 str_replace_editor 的双工具编码 Agent,用于基准测试和最小复现。')
     expect(metadata).not.toContain('order:')
   }, 60_000)
 

+ 72 - 11
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)
@@ -143,12 +202,12 @@ describe('web e2e: agent-preset selection', () => {
     await compareOrRefreshGolden(HERO_EXPECTED, snapshot, MODE)
     // The chip opens on the deployment default, by the name that preset
     // publishes rather than its directory name.
-    expect(snapshot).toContain('标准模式')
+    expect(snapshot).toContain('Standard mode')
   })
 
   it('names every preset and what it is for', async () => {
     onTestFailed(() => saveFailureShot(page, 'web-e2e-agent-preset-menu'))
-    await page.getByRole('button', { name: '标准模式' }).click()
+    await page.getByRole('button', { name: 'Standard mode' }).click()
     const menu = page.getByRole('menu')
     await menu.waitFor({ timeout: 10_000 })
 
@@ -157,15 +216,15 @@ describe('web e2e: agent-preset selection', () => {
     await compareOrRefreshGolden(MENU_EXPECTED, snapshot, MODE)
     // Every shipped preset, each with the sentence saying what it composes —
     // the id alone never said what a preset does.
-    expect(snapshot).toContain('极简模式')
-    expect(snapshot).toContain('创造模式')
+    expect(snapshot).toContain('Minimal mode')
+    expect(snapshot).toContain('Creator mode')
     await page.keyboard.press('Escape')
   })
 
   it('applies the staged pick to the blank session, and the host honors it', async () => {
     onTestFailed(() => saveFailureShot(page, 'web-e2e-agent-preset-stage'))
-    await page.getByRole('button', { name: '标准模式' }).click()
-    await page.getByRole('menuitem', { name: /极简模式/ }).click()
+    await page.getByRole('button', { name: 'Standard mode' }).click()
+    await page.getByRole('menuitem', { name: /Minimal mode/ }).click()
 
     // The chip stages; the blank session the workspace connect produced is
     // what the stage lands on. The host's own answer is what comes back.
@@ -197,8 +256,8 @@ describe('web e2e: agent-preset selection', () => {
     // against its list row, so a row that never reprojected the first switch
     // answers "already standard" and sends nothing — and restores the catalog
     // instead of leaving the session reading the narrower composition.
-    await page.getByRole('button', { name: '极简模式' }).click()
-    await page.getByRole('menuitem', { name: /^标准模式/ }).first().click()
+    await page.getByRole('button', { name: 'Minimal mode' }).click()
+    await page.getByRole('menuitem', { name: /^Standard mode/ }).first().click()
     await expect.poll(() => livePreset(scaffold.baseUrl), { timeout: 15_000 }).toBe('standard')
 
     await composer.fill('/')
@@ -221,10 +280,12 @@ describe('web e2e: agent-preset selection', () => {
     const snapshot = await captureStableAria(page, '[class*="titleRow"]', scaffold.workspaceCwd)
 
     await compareOrRefreshGolden(HEADER_EXPECTED, snapshot, MODE)
-    expect(snapshot).toContain('极简模式')
+    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 "极简模式"')
+    expect(snapshot).not.toContain('button "Minimal mode"')
   })
 
   it('drove every surface without a page error or a stream warning', () => {

+ 5 - 5
apps/web/tests/snapshots/agent-preset-authoring/created.expected.md

@@ -20,7 +20,7 @@
   - list:
     - listitem:
       - 'button "当前使用: 标准模式" [disabled] [pressed]':
-        - text: 标准模式 内置 当前使用 完整的编码 agent:文件读写、shell、检索、计划、委派与工作流。
+        - text: 标准模式 内置 当前使用 功能完整的编码 Agent,支持文件编辑、Shell、文件与网页检索、Skills、计划、目标、子代理和工作流。
         - code: standard
       - 'button "查看: 标准模式"':
         - img
@@ -30,7 +30,7 @@
         - text: 复制
     - listitem:
       - 'button "设为默认: 代码模式"':
-        - text: 代码模式 内置 标准模式的工具改为 Code Mode 呈现:模型写一段 TypeScript 调用 SDK,一次执行代替多轮工具调用
+        - text: 代码模式 内置 具备标准模式的全部能力,并通过 Code Mode SDK 呈现工具,让模型用一个 TypeScript 程序组合多步操作
         - code: code
       - 'button "查看: 代码模式"':
         - img
@@ -40,7 +40,7 @@
         - text: 复制
     - listitem:
       - 'button "设为默认: 极简模式"':
-        - text: 极简模式 内置 只向模型呈现 bash 与 str_replace_editor,适合 benchmark 与最小复现。
+        - text: 极简模式 内置 仅提供 bash 与 str_replace_editor 的双工具编码 Agent,用于基准测试和最小复现。
         - code: minimal
       - 'button "查看: 极简模式"':
         - img
@@ -50,7 +50,7 @@
         - text: 复制
     - listitem:
       - 'button "设为默认: 创造模式"':
-        - text: 创造模式 内置 标准模式加上自指工具集,可以读改自己运行的这套组装,并据此创作新的预设
+        - text: 创造模式 内置 用于创建自定义 Agent preset:具备标准模式的全部能力,并提供运行时检查、插件实验和 preset 创作指导
         - code: cordis
       - 'button "查看: 创造模式"':
         - img
@@ -62,7 +62,7 @@
   - list:
     - listitem:
       - 'button "设为默认: 我的模式"':
-        - text: 我的模式 自定义 只向模型呈现 bash 与 str_replace_editor,适合 benchmark 与最小复现。
+        - text: 我的模式 自定义 仅提供 bash 与 str_replace_editor 的双工具编码 Agent,用于基准测试和最小复现。
         - code: my-agent
       - 'button "查看路径: 我的模式"':
         - img

+ 4 - 4
apps/web/tests/snapshots/agent-preset-authoring/damaged.expected.md

@@ -20,7 +20,7 @@
   - list:
     - listitem:
       - 'button "当前使用: 标准模式" [disabled] [pressed]':
-        - text: 标准模式 内置 当前使用 完整的编码 agent:文件读写、shell、检索、计划、委派与工作流。
+        - text: 标准模式 内置 当前使用 功能完整的编码 Agent,支持文件编辑、Shell、文件与网页检索、Skills、计划、目标、子代理和工作流。
         - code: standard
       - 'button "查看: 标准模式"':
         - img
@@ -30,7 +30,7 @@
         - text: 复制
     - listitem:
       - 'button "设为默认: 代码模式"':
-        - text: 代码模式 内置 标准模式的工具改为 Code Mode 呈现:模型写一段 TypeScript 调用 SDK,一次执行代替多轮工具调用
+        - text: 代码模式 内置 具备标准模式的全部能力,并通过 Code Mode SDK 呈现工具,让模型用一个 TypeScript 程序组合多步操作
         - code: code
       - 'button "查看: 代码模式"':
         - img
@@ -40,7 +40,7 @@
         - text: 复制
     - listitem:
       - 'button "设为默认: 极简模式"':
-        - text: 极简模式 内置 只向模型呈现 bash 与 str_replace_editor,适合 benchmark 与最小复现。
+        - text: 极简模式 内置 仅提供 bash 与 str_replace_editor 的双工具编码 Agent,用于基准测试和最小复现。
         - code: minimal
       - 'button "查看: 极简模式"':
         - img
@@ -50,7 +50,7 @@
         - text: 复制
     - listitem:
       - 'button "设为默认: 创造模式"':
-        - text: 创造模式 内置 标准模式加上自指工具集,可以读改自己运行的这套组装,并据此创作新的预设
+        - text: 创造模式 内置 用于创建自定义 Agent preset:具备标准模式的全部能力,并提供运行时检查、插件实验和 preset 创作指导
         - code: cordis
       - 'button "查看: 创造模式"':
         - img

+ 4 - 4
apps/web/tests/snapshots/agent-preset-authoring/section.expected.md

@@ -20,7 +20,7 @@
   - list:
     - listitem:
       - 'button "当前使用: 标准模式" [disabled] [pressed]':
-        - text: 标准模式 内置 当前使用 完整的编码 agent:文件读写、shell、检索、计划、委派与工作流。
+        - text: 标准模式 内置 当前使用 功能完整的编码 Agent,支持文件编辑、Shell、文件与网页检索、Skills、计划、目标、子代理和工作流。
         - code: standard
       - 'button "查看: 标准模式"':
         - img
@@ -30,7 +30,7 @@
         - text: 复制
     - listitem:
       - 'button "设为默认: 代码模式"':
-        - text: 代码模式 内置 标准模式的工具改为 Code Mode 呈现:模型写一段 TypeScript 调用 SDK,一次执行代替多轮工具调用
+        - text: 代码模式 内置 具备标准模式的全部能力,并通过 Code Mode SDK 呈现工具,让模型用一个 TypeScript 程序组合多步操作
         - code: code
       - 'button "查看: 代码模式"':
         - img
@@ -40,7 +40,7 @@
         - text: 复制
     - listitem:
       - 'button "设为默认: 极简模式"':
-        - text: 极简模式 内置 只向模型呈现 bash 与 str_replace_editor,适合 benchmark 与最小复现。
+        - text: 极简模式 内置 仅提供 bash 与 str_replace_editor 的双工具编码 Agent,用于基准测试和最小复现。
         - code: minimal
       - 'button "查看: 极简模式"':
         - img
@@ -50,7 +50,7 @@
         - text: 复制
     - listitem:
       - 'button "设为默认: 创造模式"':
-        - text: 创造模式 内置 标准模式加上自指工具集,可以读改自己运行的这套组装,并据此创作新的预设
+        - text: 创造模式 内置 用于创建自定义 Agent preset:具备标准模式的全部能力,并提供运行时检查、插件实验和 preset 创作指导
         - code: cordis
       - 'button "查看: 创造模式"':
         - img

+ 4 - 1
apps/web/tests/snapshots/agent-preset-selection/header.expected.md

@@ -1,4 +1,7 @@
 - navigation "Session hierarchy":
   - button "Seeded turn" [disabled]
 - img
-- text: 极简模式
+- text: Minimal mode
+- button "1 subagent":
+  - text: 1 subagent
+  - img

+ 2 - 2
apps/web/tests/snapshots/agent-preset-selection/hero.expected.md

@@ -2,7 +2,7 @@
   - img
   - text: workspace
   - img
-- button "标准模式":
+- button "Standard mode":
   - img
-  - text: 标准模式
+  - text: Standard mode
   - img

+ 5 - 5
apps/web/tests/snapshots/agent-preset-selection/menu.expected.md

@@ -1,7 +1,7 @@
 - menu:
-  - menuitem "标准模式 完整的编码 agent:文件读写、shell、检索、计划、委派与工作流。":
-    - text: 标准模式 完整的编码 agent:文件读写、shell、检索、计划、委派与工作流。
+  - menuitem "Standard mode Full coding agent with file editing, shell, file and web search, skills, planning, goals, subagents, and workflows.":
+    - text: Standard mode Full coding agent with file editing, shell, file and web search, skills, planning, goals, subagents, and workflows.
     - img
-  - menuitem "代码模式 标准模式的工具改为 Code Mode 呈现:模型写一段 TypeScript 调用 SDK,一次执行代替多轮工具调用。"
-  - menuitem "极简模式 只向模型呈现 bash 与 str_replace_editor,适合 benchmark 与最小复现。"
-  - menuitem "创造模式 标准模式加上自指工具集,可以读改自己运行的这套组装,并据此创作新的预设。"
+  - menuitem "Code mode All Standard mode capabilities, with tools exposed through the Code Mode SDK so the model can combine multi-step operations in one TypeScript program."
+  - menuitem "Minimal mode Two-tool coding agent with only bash and str_replace_editor, for benchmarks and minimal reproductions."
+  - menuitem "Creator mode Built for creating custom agent presets, with all Standard mode capabilities plus runtime inspection, plugin experiments, and preset-authoring guidance."

+ 1 - 1
apps/web/tests/snapshots/code-mode-round/ui.expected.md

@@ -2,7 +2,7 @@
   - navigation "Session hierarchy":
     - 'button "Using ONE run_code program: run" [disabled]'
   - img
-  - text: 标准模式
+  - text: Standard mode
   - tablist:
     - tab "Chat" [selected]
     - tab "Trajectory"

+ 1 - 1
apps/web/tests/snapshots/cordis-tool-round/ui.expected.md

@@ -2,7 +2,7 @@
   - navigation "Session hierarchy":
     - button "Use only Cordis tools. First" [disabled]
   - img
-  - text: 标准模式
+  - text: Standard mode
   - tablist:
     - tab "Chat" [selected]
     - tab "Trajectory"

+ 1 - 1
apps/web/tests/snapshots/fresh-round-trip/ui.expected.md

@@ -2,7 +2,7 @@
   - navigation "Session hierarchy":
     - button "Use the bash tool to" [disabled]
   - img
-  - text: 标准模式
+  - text: Standard mode
   - tablist:
     - tab "Chat" [selected]
     - tab "Trajectory"

+ 1 - 1
apps/web/tests/snapshots/goal-multi-turn-actions/ui.expected.md

@@ -2,7 +2,7 @@
   - navigation "Session hierarchy":
     - button "workspace" [disabled]
   - img
-  - text: 标准模式
+  - text: Standard mode
   - tablist:
     - tab "Chat" [selected]
     - tab "Trajectory"

+ 2 - 2
apps/web/tests/snapshots/lifecycle-chrome/hero.expected.md

@@ -25,9 +25,9 @@
   - img
   - text: workspace
   - img
-- button "标准模式":
+- button "Standard mode":
   - img
-  - text: 标准模式
+  - text: Standard mode
   - img
 - textbox "Describe what you want to build"
 - button "Commands":

+ 2 - 2
apps/web/tests/snapshots/lifecycle-chrome/plan-active.expected.md

@@ -25,9 +25,9 @@
   - img
   - text: workspace
   - img
-- button "标准模式":
+- button "Standard mode":
   - img
-  - text: 标准模式
+  - text: Standard mode
   - img
 - textbox "Describe what you want to build"
 - button "Commands":

+ 1 - 1
apps/web/tests/snapshots/lifecycle-chrome/reloaded.expected.md

@@ -2,7 +2,7 @@
   - navigation "Session hierarchy":
     - button "Reply with the single word" [disabled]
   - img
-  - text: 标准模式
+  - text: Standard mode
   - tablist:
     - tab "Chat" [selected]
     - tab "Trajectory"

+ 1 - 1
apps/web/tests/snapshots/live-interactions/cancel.expected.md

@@ -2,7 +2,7 @@
   - navigation "Session hierarchy":
     - button "Reply with a one-sentence description" [disabled]
   - img
-  - text: 标准模式
+  - text: Standard mode
   - tablist:
     - tab "Chat" [selected]
     - tab "Trajectory"

+ 1 - 1
apps/web/tests/snapshots/live-interactions/error-auth.expected.md

@@ -2,7 +2,7 @@
   - navigation "Session hierarchy":
     - button "Reply with a one-sentence description" [disabled]
   - img
-  - text: 标准模式
+  - text: Standard mode
   - tablist:
     - tab "Chat" [selected]
     - tab "Trajectory"

+ 1 - 1
apps/web/tests/snapshots/live-interactions/loading.expected.md

@@ -2,7 +2,7 @@
   - navigation "Session hierarchy":
     - button "Reply with a one-sentence description" [disabled]
   - img
-  - text: 标准模式
+  - text: Standard mode
   - tablist:
     - tab "Chat" [selected]
     - tab "Trajectory"

+ 1 - 1
apps/web/tests/snapshots/live-interactions/retry.expected.md

@@ -2,7 +2,7 @@
   - navigation "Session hierarchy":
     - button "Reply with a one-sentence description" [disabled]
   - img
-  - text: 标准模式
+  - text: Standard mode
   - tablist:
     - tab "Chat" [selected]
     - tab "Trajectory"

+ 1 - 1
apps/web/tests/snapshots/plan-review/approved.expected.md

@@ -2,7 +2,7 @@
   - navigation "Session hierarchy":
     - 'button "Plan a small change: add" [disabled]'
   - img
-  - text: 标准模式
+  - text: Standard mode
   - tablist:
     - tab "Chat" [selected]
     - tab "Trajectory"

+ 1 - 1
apps/web/tests/snapshots/question-composer/answered.expected.md

@@ -2,7 +2,7 @@
   - navigation "Session hierarchy":
     - button "Use the ask_user_question tool to" [disabled]
   - img
-  - text: 标准模式
+  - text: Standard mode
   - tablist:
     - tab "Chat" [selected]
     - tab "Trajectory"

+ 1 - 1
apps/web/tests/snapshots/queue-actions/collapsed.expected.md

@@ -2,7 +2,7 @@
   - navigation "Session hierarchy":
     - button "Reply with a one-sentence description" [disabled]
   - img
-  - text: 标准模式
+  - text: Standard mode
   - tablist:
     - tab "Chat" [selected]
     - tab "Trajectory"

+ 1 - 1
apps/web/tests/snapshots/queue-actions/editing.expected.md

@@ -2,7 +2,7 @@
   - navigation "Session hierarchy":
     - button "Reply with a one-sentence description" [disabled]
   - img
-  - text: 标准模式
+  - text: Standard mode
   - tablist:
     - tab "Chat" [selected]
     - tab "Trajectory"

+ 1 - 1
apps/web/tests/snapshots/queue-actions/layout.expected.md

@@ -2,7 +2,7 @@
   - navigation "Session hierarchy":
     - button "workspace" [disabled]
   - img
-  - text: 标准模式
+  - text: Standard mode
   - tablist:
     - tab "Chat" [selected]
     - tab "Trajectory"

+ 1 - 1
apps/web/tests/snapshots/queue-actions/preserved.expected.md

@@ -2,7 +2,7 @@
   - navigation "Session hierarchy":
     - button "Reply with a one-sentence description" [disabled]
   - img
-  - text: 标准模式
+  - text: Standard mode
   - tablist:
     - tab "Chat" [selected]
     - tab "Trajectory"

+ 1 - 1
apps/web/tests/snapshots/queue-actions/ui.expected.md

@@ -2,7 +2,7 @@
   - navigation "Session hierarchy":
     - button "Reply with a one-sentence description" [disabled]
   - img
-  - text: 标准模式
+  - text: Standard mode
   - tablist:
     - tab "Chat" [selected]
     - tab "Trajectory"

+ 1 - 1
apps/web/tests/snapshots/skill-user-invoke/ui.expected.md

@@ -2,7 +2,7 @@
   - navigation "Session hierarchy":
     - button "/user-invoke-demo and confirm the fixtur" [disabled]
   - img
-  - text: 标准模式
+  - text: Standard mode
   - tablist:
     - tab "Chat" [selected]
     - tab "Trajectory"

+ 1 - 1
apps/web/tests/snapshots/steer-all/mid-steer.expected.md

@@ -2,7 +2,7 @@
   - navigation "Session hierarchy":
     - button "Use the ask_user_question tool to" [disabled]
   - img
-  - text: 标准模式
+  - text: Standard mode
   - tablist:
     - tab "Chat" [selected]
     - tab "Trajectory"

+ 1 - 1
apps/web/tests/snapshots/steer-all/settled.expected.md

@@ -2,7 +2,7 @@
   - navigation "Session hierarchy":
     - button "Use the ask_user_question tool to" [disabled]
   - img
-  - text: 标准模式
+  - text: Standard mode
   - tablist:
     - tab "Chat" [selected]
     - tab "Trajectory"

+ 1 - 1
apps/web/tests/snapshots/steering/mid-steer.expected.md

@@ -2,7 +2,7 @@
   - navigation "Session hierarchy":
     - button "Use the ask_user_question tool to" [disabled]
   - img
-  - text: 标准模式
+  - text: Standard mode
   - tablist:
     - tab "Chat" [selected]
     - tab "Trajectory"

+ 1 - 1
apps/web/tests/snapshots/steering/settled.expected.md

@@ -2,7 +2,7 @@
   - navigation "Session hierarchy":
     - button "Use the ask_user_question tool to" [disabled]
   - img
-  - text: 标准模式
+  - text: Standard mode
   - tablist:
     - tab "Chat" [selected]
     - tab "Trajectory"

+ 1 - 1
apps/web/tests/snapshots/turn-tail-actions/running.expected.md

@@ -2,7 +2,7 @@
   - navigation "Session hierarchy":
     - button "Begin your reply with the" [disabled]
   - img
-  - text: 标准模式
+  - text: Standard mode
   - tablist:
     - tab "Chat" [selected]
     - tab "Trajectory"

+ 1 - 1
apps/web/tests/snapshots/turn-tail-actions/settled.expected.md

@@ -2,7 +2,7 @@
   - navigation "Session hierarchy":
     - button "Begin your reply with the" [disabled]
   - img
-  - text: 标准模式
+  - text: Standard mode
   - tablist:
     - tab "Chat" [selected]
     - tab "Trajectory"

+ 1 - 1
apps/web/tests/snapshots/web-search-round/ui.expected.md

@@ -2,7 +2,7 @@
   - navigation "Session hierarchy":
     - button "Use web_search to search exactly" [disabled]
   - img
-  - text: 标准模式
+  - text: Standard mode
   - tablist:
     - tab "Chat" [selected]
     - tab "Trajectory"

+ 1 - 1
apps/web/tests/steering.e2e.ts

@@ -314,7 +314,7 @@ describe('web e2e: empty-draft Cmd+Enter steers the whole queue', () => {
     await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
     await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
     await connectFreshWorkspace(page, scaffold.workspaceCwd)
-    await page.getByText('标准模式', { exact: true }).waitFor({ timeout: 10_000 })
+    await page.getByText('Standard mode', { exact: true }).waitFor({ timeout: 10_000 })
   }, 120_000)
 
   afterAll(async () => {

+ 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: 32a4e7d9e25d3c70d2cc2e8a01c94d093d19659c
-README.zh.md: b65a1bdf926f7a34bc3813833ca5ac2d3b6dfabd
+README.md: 008066114e9c49e5c74299979e24c27a4c9621c9
+README.zh.md: e07d5994ae196cd03be7818fe4ade1aafda9aa55

+ 2 - 0
packages/client/ui-agent-preset/README.md

@@ -26,6 +26,8 @@ Options and the current default both come from one `agentPreset.list` call. The
 
 A locally authored preset is exactly as privileged as the plugins it names, so the list marks `user` rows rather than presenting every preset as shipped and vetted.
 
+Preset files publish one unlocalized `name` and `description`, which Web uses for every `user` row and unknown `system` row. For the four shipped ids (`standard`, `code`, `minimal`, and `cordis`), Web resolves both fields from its active locale only when the roster marks the row `system`; an identically named `user` preset keeps its file metadata.
+
 The row re-reads on `settings/changed` for its own namespace and on `connection/reset`: the roster is a live directory and the default is a settings field, so an external edit or a reconnect can both move it.
 
 ## The management section

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

@@ -26,6 +26,8 @@ chip 以部署默认值打开,其选择是**暂存**的——该界面先于
 
 本地创作的 preset 的权限恰好等于它所引用的插件,因此列表会标注 `user` 行,而不是把每个 preset 都呈现为随附且已审核的。
 
+preset 文件提供一套未国际化的 `name` 与 `description`,Web 将其用于所有 `user` 行和未知的 `system` 行。对于四个随附 id(`standard`、`code`、`minimal` 与 `cordis`),只有名单将该行标记为 `system` 时,Web 才会从当前 locale 解析这两个字段;同名的 `user` preset 仍使用其文件元数据。
+
 本行在自身命名空间的 `settings/changed` 以及 `connection/reset` 时重新读取:名单是一个活动目录,默认值是一项设置,外部编辑与重新连接都可能改变它。
 
 ## 管理分区

+ 4 - 2
packages/client/ui-agent-preset/src/client/AgentPresetLabel.tsx

@@ -15,6 +15,7 @@ import { IconThinkOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
 // Type-only: pulls the ui-conversation SlotMap merge (the header actions).
 import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
 import type { AgentPresetSettingsState } from './settings-store.ts'
+import { presetDisplayText } from './locales.ts'
 import css from './AgentPresetLabel.module.css'
 
 /** Registration-side business face for the header label. */
@@ -53,10 +54,11 @@ export function AgentPresetLabel({
   if (preset === undefined) return null
 
   const option = options.find(entry => entry.id === preset)
+  const text = option === undefined ? undefined : presetDisplayText(option, t)
   return (
-    <span className={css.label} title={option?.description ?? t('headerHint')}>
+    <span className={css.label} title={text?.description ?? t('headerHint')}>
       <IconThinkOutline16 className={css.icon} />
-      {option?.name ?? preset}
+      {text?.name ?? preset}
     </span>
   )
 }

+ 6 - 6
packages/client/ui-agent-preset/src/client/AgentPresetRow.tsx

@@ -8,7 +8,7 @@ import { useEffect, useState } from 'react'
 import type { SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
 import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
 import type { AgentPresetSettingsState } from './settings-store.ts'
-import type { AgentPresetSettingsKey } from './locales.ts'
+import { presetDisplayText, type AgentPresetSettingsKey } from './locales.ts'
 import { PresetMenu } from './PresetMenu.tsx'
 import css from './AgentPresetRow.module.css'
 
@@ -52,11 +52,11 @@ export function AgentPresetRow({ load, select, useAgentPreset, t }: AgentPresetR
   // every session shares the host composition — the row simply does not exist.
   if (state.status === 'unavailable') return null
   const busy = state.status === 'loading' || state.status === 'saving'
-  // The metadata name is what every other surface shows — the id is the
-  // addressing, not the label. A preset that names itself nothing falls back
-  // to its id, which is then all there is to say about it.
+  // Every preset surface applies the same display-copy rule. The id remains
+  // addressing rather than a label, except where no display name exists.
   const chosen = state.options.find(option => option.id === state.currentValue)
-  const label = state.currentValue === '' ? t('loading') : (chosen?.name ?? state.currentValue)
+  const chosenText = chosen === undefined ? undefined : presetDisplayText(chosen, t)
+  const label = state.currentValue === '' ? t('loading') : (chosenText?.name ?? state.currentValue)
   const description: string = state.error ?? t('description')
 
   return (
@@ -69,7 +69,7 @@ export function AgentPresetRow({ load, select, useAgentPreset, t }: AgentPresetR
         options={state.options}
         selectedId={state.currentValue}
         label={label}
-        userTrustLabel={t('userTrust')}
+        t={t}
         buttonClassName={css.selector}
         chevronClassName={css.chevron}
         disabled={busy || !state.writable || state.options.length === 0}

+ 17 - 12
packages/client/ui-agent-preset/src/client/AgentPresetSeat.tsx

@@ -19,6 +19,7 @@ import { IconChevronDownOutline14, IconThinkOutline16, Menu } from '@deepseek-ai
 // Type-only: pulls the ui-conversation SlotMap merge (the hero seat).
 import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
 import type { AgentPresetSeatState } from './seat-store.ts'
+import { presetDisplayText } from './locales.ts'
 import css from './AgentPresetSeat.module.css'
 
 /** Registration-side business face for the hero chip. */
@@ -57,22 +58,26 @@ export function AgentPresetSeat({ load, select, useAgentPresetSeat, t }: AgentPr
   if (state.options.length === 0 || state.current === '') return null
 
   const chosen = state.options.find(option => option.id === state.current)
+  const chosenText = chosen === undefined ? undefined : presetDisplayText(chosen, t)
 
   return (
     <Menu
       open={open}
       onClose={() => { setOpen(false) }}
-      items={state.options.map(option => ({
-        id: option.id,
-        // Name and description together: the id alone never said what a
-        // preset does, which is the whole reason the metadata exists.
-        label: (
-          <span className={css.item}>
-            <span className={css.itemName}>{option.name ?? option.id}</span>
-            <span className={css.itemDesc}>{option.description ?? t('noDescription')}</span>
-          </span>
-        ),
-      }))}
+      items={state.options.map((option) => {
+        const text = presetDisplayText(option, t)
+        return {
+          id: option.id,
+          // Name and description together: the id alone never says what a
+          // preset does, which is why the roster carries display copy.
+          label: (
+            <span className={css.item}>
+              <span className={css.itemName}>{text.name}</span>
+              <span className={css.itemDesc}>{text.description ?? t('noDescription')}</span>
+            </span>
+          ),
+        }
+      })}
       selectedId={state.current}
       onSelect={(id) => {
         setOpen(false)
@@ -91,7 +96,7 @@ export function AgentPresetSeat({ load, select, useAgentPresetSeat, t }: AgentPr
           onClick={() => { setOpen(value => !value) }}
         >
           <IconThinkOutline16 className={css.seatIcon} />
-          {chosen?.name ?? state.current}
+          {chosenText?.name ?? state.current}
           <IconChevronDownOutline14 className={css.chevron} />
         </button>
       )}

+ 21 - 12
packages/client/ui-agent-preset/src/client/AgentPresetSection.tsx

@@ -18,7 +18,7 @@ import {
 import type { SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
 import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
 import { draftBlocker, type AgentPresetSectionState } from './section-store.ts'
-import type { AgentPresetSettingsKey } from './locales.ts'
+import { presetDisplayText, type AgentPresetSettingsKey } from './locales.ts'
 import css from './AgentPresetSection.module.css'
 
 /** Registration-side business face for the management section. */
@@ -77,11 +77,13 @@ function CopyDialog({ state, t, actions }: CopyDialogProps): ReactNode {
   const draft = state.copy
   const blocker = draft === null ? undefined : draftBlocker(draft, state.rows)
   const message = draft === null ? null : draft.error ?? (blocker === undefined ? null : t(blocker))
+  const source = draft === null ? undefined : state.rows.find(row => row.id === draft.from)
+  const sourceTitle = source === undefined ? draft?.fromTitle : presetDisplayText(source, t).name
   return (
     <Modal
       open={draft !== null}
       onClose={() => { actions.cancelCopy() }}
-      title={draft === null ? t('copyTitle') : `${t('copyTitle')} · ${t('copyOf')} ${draft.fromTitle}`}
+      title={draft === null ? t('copyTitle') : `${t('copyTitle')} · ${t('copyOf')} ${sourceTitle}`}
       closeLabel={t('close')}
       description={t('copyIntro')}
       className={css.dialog as string}
@@ -143,6 +145,11 @@ function CopyDialog({ state, t, actions }: CopyDialogProps): ReactNode {
 export function AgentPresetSection(props: AgentPresetSectionProps): ReactNode {
   const { useAgentPresetSection, t, load } = props
   const state = useAgentPresetSection(snapshot => snapshot)
+  const viewedId = state.view?.id
+  const viewedRow = viewedId === undefined ? undefined : state.rows.find(row => row.id === viewedId)
+  const viewedTitle = state.view === null
+    ? ''
+    : viewedRow === undefined ? state.view.title : presetDisplayText(viewedRow, t).name
 
   useEffect(() => {
     void load()
@@ -170,13 +177,15 @@ export function AgentPresetSection(props: AgentPresetSectionProps): ReactNode {
       <p className={css.intro}>{t('sectionIntro')}</p>
       {state.error === null ? null : <p className={css.error} role="alert">{state.error}</p>}
       {([['system', t('builtInGroup')], ['user', t('customGroup')]] as const).map(([trust, heading]) => {
-        const group = state.rows.filter(row => row.trust === trust)
+        const group = state.rows
+          .filter(row => row.trust === trust)
+          .map(row => ({ row, text: presetDisplayText(row, t) }))
         if (group.length === 0) return null
         return (
           <section key={trust} className={css.group}>
             <h3 className={css.groupHead}>{heading}</h3>
             <ul className={css.cards}>
-              {group.map(row => (
+              {group.map(({ row, text }) => (
                 <li
                   key={row.id}
                   className={row.broken !== undefined
@@ -196,12 +205,12 @@ export function AgentPresetSection(props: AgentPresetSectionProps): ReactNode {
                     disabled={row.isDefault || row.broken !== undefined}
                     // Without this the name is the whole card read aloud —
                     // title, badge, description, id.
-                    aria-label={`${row.broken !== undefined ? t('brokenBadge') : row.isDefault ? t('inUse') : t('setDefault')}: ${row.name ?? row.id}`}
+                    aria-label={`${row.broken !== undefined ? t('brokenBadge') : row.isDefault ? t('inUse') : t('setDefault')}: ${text.name}`}
                     title={row.broken ?? (row.isDefault ? t('inUse') : t('setDefault'))}
                     onClick={() => { void props.makeDefault(row.id) }}
                   >
                     <span className={css.cardHead}>
-                      <span className={css.cardName}>{row.name ?? row.id}</span>
+                      <span className={css.cardName}>{text.name}</span>
                       {row.broken !== undefined
                         ? <span className={css.brokenBadge}>{t('brokenBadge')}</span>
                         : null}
@@ -210,7 +219,7 @@ export function AgentPresetSection(props: AgentPresetSectionProps): ReactNode {
                       </span>
                       {row.isDefault ? <span className={css.inUse}>{t('inUse')}</span> : null}
                     </span>
-                    <span className={css.cardDesc}>{row.description ?? t('noDescription')}</span>
+                    <span className={css.cardDesc}>{text.description ?? t('noDescription')}</span>
                     {row.broken === undefined
                       ? null
                       : <span className={css.cardBrokenReason} role="alert">{row.broken}</span>}
@@ -231,7 +240,7 @@ export function AgentPresetSection(props: AgentPresetSectionProps): ReactNode {
                             type="button"
                             className={css.iconButton}
                             data-tip={t('view')}
-                            aria-label={`${t('view')}: ${row.name ?? row.id}`}
+                            aria-label={`${t('view')}: ${text.name}`}
                             onClick={() => { void props.view(row.id) }}
                           >
                             <IconBrowseOutline16 />
@@ -243,7 +252,7 @@ export function AgentPresetSection(props: AgentPresetSectionProps): ReactNode {
                           type="button"
                           className={css.iconButton}
                           data-tip={state.hasDocument ? t('openLocation') : t('showLocation')}
-                          aria-label={`${state.hasDocument ? t('openLocation') : t('showLocation')}: ${row.name ?? row.id}`}
+                          aria-label={`${state.hasDocument ? t('openLocation') : t('showLocation')}: ${text.name}`}
                           onClick={() => { void props.openLocation(row.id) }}
                         >
                           <IconFolderOpenOutline16 />
@@ -256,7 +265,7 @@ export function AgentPresetSection(props: AgentPresetSectionProps): ReactNode {
                       data-tip={row.broken !== undefined
                         ? t('brokenNoCopy')
                         : state.authorable ? t('duplicate') : t('duplicateUnavailable')}
-                      aria-label={`${t('duplicate')}: ${row.name ?? row.id}`}
+                      aria-label={`${t('duplicate')}: ${text.name}`}
                       onClick={() => { props.beginCopy(row.id) }}
                     >
                       <IconCopyOutline16 />
@@ -267,7 +276,7 @@ export function AgentPresetSection(props: AgentPresetSectionProps): ReactNode {
                           type="button"
                           className={`${css.iconButton} ${css.iconDanger}`}
                           data-tip={t('delete')}
-                          aria-label={`${t('delete')}: ${row.name ?? row.id}`}
+                          aria-label={`${t('delete')}: ${text.name}`}
                           onClick={() => { props.confirmDelete(row.id) }}
                         >
                           <IconTrashOutline16 />
@@ -325,7 +334,7 @@ export function AgentPresetSection(props: AgentPresetSectionProps): ReactNode {
       <Modal
         open={state.view !== null}
         onClose={() => { props.closeView() }}
-        title={state.view === null ? '' : `${t('view')} · ${state.view.title}`}
+        title={state.view === null ? '' : `${t('view')} · ${viewedTitle}`}
         closeLabel={t('close')}
         description={t('composition')}
         className={css.dialog as string}

+ 13 - 12
packages/client/ui-agent-preset/src/client/PresetMenu.tsx

@@ -11,6 +11,7 @@
 
 import { IconChevronDownOutline14, Menu } from '@deepseek-ai/dsh-client-ui-primitives'
 import type { AgentPresetOption } from './settings-store.ts'
+import { presetDisplayText, type AgentPresetSettingsKey } from './locales.ts'
 
 /** What one surface passes to the shared picker. */
 export interface PresetMenuProps {
@@ -20,8 +21,8 @@ export interface PresetMenuProps {
   selectedId: string
   /** Text on the button; the surfaces word a pending roster differently. */
   label: string
-  /** Suffix marking a locally authored preset in the menu. */
-  userTrustLabel: string
+  /** Active Web locale lookup. */
+  t: (key: AgentPresetSettingsKey) => string
   /** Class for the trigger button, owned by the calling surface. */
   buttonClassName: string | undefined
   /** Class for the chevron, owned by the calling surface. */
@@ -42,22 +43,22 @@ export interface PresetMenuProps {
  * @returns the menu and its trigger.
  */
 export function PresetMenu({
-  options, selectedId, label, userTrustLabel, buttonClassName, chevronClassName,
+  options, selectedId, label, t, buttonClassName, chevronClassName,
   disabled, open, onOpenChange, onSelect,
 }: PresetMenuProps) {
   return (
     <Menu
       open={open}
       onClose={() => { onOpenChange(false) }}
-      items={options.map(option => ({
-        id: option.id,
-        // The metadata name is what every surface shows; the id is addressing,
-        // not a label. A preset that names itself nothing falls back to its id,
-        // which is then all there is to say about it.
-        label: option.trust === 'user'
-          ? `${option.name ?? option.id} · ${userTrustLabel}`
-          : option.name ?? option.id,
-      }))}
+      items={options.map((option) => {
+        const name = presetDisplayText(option, t).name
+        return {
+          id: option.id,
+          // All preset surfaces resolve copy the same way; the id is addressing,
+          // not a label, except where no display name exists.
+          label: option.trust === 'user' ? `${name} · ${t('userTrust')}` : name,
+        }
+      })}
       selectedId={selectedId}
       onSelect={(id) => {
         onOpenChange(false)

+ 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: 20,
+        // Static session context occupies the header's leading negative-order band.
+        order: -10,
         locale: 'settings.agentPreset',
         inject: labelInjected,
       }, AgentPresetLabel)

+ 74 - 0
packages/client/ui-agent-preset/src/client/locales.ts

@@ -4,6 +4,10 @@
 export type AgentPresetSettingsKey =
   | 'title' | 'description' | 'loading' | 'error' | 'userTrust' | 'seatHint' | 'headerHint'
   | 'nav' | 'sectionIntro' | 'builtIn' | 'setDefault' | 'view'
+  | 'presetStandardName' | 'presetStandardDescription'
+  | 'presetCodeName' | 'presetCodeDescription'
+  | 'presetMinimalName' | 'presetMinimalDescription'
+  | 'presetCordisName' | 'presetCordisDescription'
   | 'duplicate' | 'duplicateUnavailable' | 'delete' | 'presetId' | 'presetIdPlaceholder' | 'copyOf'
   | 'displayName' | 'displayNamePlaceholder'
   | 'inUse' | 'noDescription' | 'builtInGroup' | 'customGroup'
@@ -30,6 +34,18 @@ export const en: Record<AgentPresetSettingsKey, string> = {
   builtIn: 'Built-in',
   setDefault: 'Set as default',
   view: 'View',
+  presetStandardName: 'Standard mode',
+  presetStandardDescription:
+    'Full coding agent with file editing, shell, file and web search, skills, planning, goals, subagents, and workflows.',
+  presetCodeName: 'Code mode',
+  presetCodeDescription:
+    'All Standard mode capabilities, with tools exposed through the Code Mode SDK so the model can combine multi-step operations in one TypeScript program.',
+  presetMinimalName: 'Minimal mode',
+  presetMinimalDescription:
+    'Two-tool coding agent with only bash and str_replace_editor, for benchmarks and minimal reproductions.',
+  presetCordisName: 'Creator mode',
+  presetCordisDescription:
+    'Built for creating custom agent presets, with all Standard mode capabilities plus runtime inspection, plugin experiments, and preset-authoring guidance.',
   duplicate: 'Duplicate',
   duplicateUnavailable: 'This deployment has no writable preset directory',
   delete: 'Delete',
@@ -82,6 +98,14 @@ export const zh: Record<AgentPresetSettingsKey, string> = {
   builtIn: '内置',
   setDefault: '设为默认',
   view: '查看',
+  presetStandardName: '标准模式',
+  presetStandardDescription: '功能完整的编码 Agent,支持文件编辑、Shell、文件与网页检索、Skills、计划、目标、子代理和工作流。',
+  presetCodeName: '代码模式',
+  presetCodeDescription: '具备标准模式的全部能力,并通过 Code Mode SDK 呈现工具,让模型用一个 TypeScript 程序组合多步操作。',
+  presetMinimalName: '极简模式',
+  presetMinimalDescription: '仅提供 bash 与 str_replace_editor 的双工具编码 Agent,用于基准测试和最小复现。',
+  presetCordisName: '创造模式',
+  presetCordisDescription: '用于创建自定义 Agent preset:具备标准模式的全部能力,并提供运行时检查、插件实验和 preset 创作指导。',
   duplicate: '复制',
   duplicateUnavailable: '此部署未配置可写的预设目录',
   delete: '删除',
@@ -116,3 +140,53 @@ export const zh: Record<AgentPresetSettingsKey, string> = {
   deleteConfirm: '删除',
   deleting: '正在删除…',
 }
+
+/** Preset roster fields needed to resolve Web display copy. */
+export interface PresetDisplaySource {
+  /** Stable preset id. */
+  readonly id: string
+  /** Whether the deployment ships the preset or the user owns it. */
+  readonly trust: 'system' | 'user'
+  /** Unlocalized name published by the preset. */
+  readonly name?: string
+  /** Unlocalized description published by the preset. */
+  readonly description?: string
+}
+
+/** Display copy resolved for the active Web locale. */
+export interface PresetDisplayText {
+  /** Localized built-in name or the preset's own fallback name. */
+  readonly name: string
+  /** Localized built-in description or the preset's own description. */
+  readonly description?: string
+}
+
+interface PresetLocaleKeys {
+  readonly name: AgentPresetSettingsKey
+  readonly description: AgentPresetSettingsKey
+}
+
+const BUILT_IN_PRESET_KEYS: Readonly<Partial<Record<string, PresetLocaleKeys>>> = {
+  standard: { name: 'presetStandardName', description: 'presetStandardDescription' },
+  code: { name: 'presetCodeName', description: 'presetCodeDescription' },
+  minimal: { name: 'presetMinimalName', description: 'presetMinimalDescription' },
+  cordis: { name: 'presetCordisName', description: 'presetCordisDescription' },
+}
+
+/**
+ * Resolve preset display copy without making user-authored metadata translatable.
+ * @param preset - roster row whose copy is being rendered.
+ * @param t - active Web locale lookup.
+ * @returns localized copy for a known shipped preset, otherwise file metadata.
+ */
+export function presetDisplayText(
+  preset: PresetDisplaySource,
+  t: (key: AgentPresetSettingsKey) => string,
+): PresetDisplayText {
+  const keys = preset.trust === 'system' ? BUILT_IN_PRESET_KEYS[preset.id] : undefined
+  if (keys !== undefined) return { name: t(keys.name), description: t(keys.description) }
+  return {
+    name: preset.name ?? preset.id,
+    ...preset.description === undefined ? {} : { description: preset.description },
+  }
+}

+ 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: 20 })
+    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)

+ 17 - 5
packages/client/ui-agent-preset/tests/components.spec.tsx

@@ -90,7 +90,7 @@ describe('the General-settings row', () => {
     const actions = renderRow()
 
     await waitFor(() => { expect(actions.load).toHaveBeenCalledTimes(1) })
-    expect(screen.getByRole('button').textContent).toContain('标准模式')
+    expect(screen.getByRole('button').textContent).toContain(en.presetStandardName)
   })
 
   it('marks a locally authored option as local', () => {
@@ -102,7 +102,7 @@ describe('the General-settings row', () => {
     // list says which rows are local rather than presenting all as vetted.
     expect(screen.getByText(`mine · ${en.userTrust}`)).toBeTruthy()
     // The shipped one carries no marker; only local rows are called out.
-    expect(screen.getAllByText('标准模式')).toHaveLength(2)
+    expect(screen.getAllByText(en.presetStandardName)).toHaveLength(2)
   })
 
   it('falls back to the id for a preset that published no name', () => {
@@ -128,6 +128,12 @@ describe('the General-settings row', () => {
     expect(screen.getByText('bare')).toBeTruthy()
   })
 
+  it('shows the selected id until a stale roster contains it', () => {
+    renderRow({ currentValue: 'arriving', options: [] })
+
+    expect(screen.getByRole('button').textContent).toContain('arriving')
+  })
+
   it('writes the picked preset and closes the menu', () => {
     const actions = renderRow()
     fireEvent.click(screen.getByRole('button'))
@@ -194,7 +200,7 @@ describe('the new-session chip', () => {
     const actions = renderSeat()
 
     await waitFor(() => { expect(actions.load).toHaveBeenCalledTimes(1) })
-    expect(screen.getByRole('button').textContent).toContain('标准模式')
+    expect(screen.getByRole('button').textContent).toContain(en.presetStandardName)
     expect(screen.getByRole('button').getAttribute('title')).toBe(en.seatHint)
   })
 
@@ -205,7 +211,7 @@ describe('the new-session chip', () => {
 
     // The id alone never said what a preset does; the description is the
     // whole reason a preset can publish metadata at all.
-    expect(screen.getByText('完整的编码 agent。')).toBeTruthy()
+    expect(screen.getByText(en.presetStandardDescription)).toBeTruthy()
     // A preset that published none still reads as a row, with its id standing
     // in for the name.
     expect(screen.getByText(en.noDescription)).toBeTruthy()
@@ -218,6 +224,12 @@ describe('the new-session chip', () => {
     expect(screen.getByRole('button').textContent).toContain('mine')
   })
 
+  it('shows the staged id until a stale roster contains it', () => {
+    renderSeat({ current: 'arriving' })
+
+    expect(screen.getByRole('button').textContent).toContain('arriving')
+  })
+
   it('stages the picked preset and closes the menu', () => {
     const actions = renderSeat()
     fireEvent.click(screen.getByRole('button'))
@@ -267,7 +279,7 @@ describe('the session-header label', () => {
     await waitFor(() => { expect(load).toHaveBeenCalledTimes(1) })
     // A control here would promise a switch the host refuses outright.
     expect(screen.queryByRole('button')).toBeNull()
-    expect(screen.getByTitle('完整的编码 agent。').textContent).toBe('标准模式')
+    expect(screen.getByTitle(en.presetStandardDescription).textContent).toBe(en.presetStandardName)
   })
 
   it('falls back to the id, and to the generic hint, when metadata is absent', () => {

+ 33 - 0
packages/client/ui-agent-preset/tests/locales.spec.ts

@@ -0,0 +1,33 @@
+/** Web-localized copy for the four shipped presets and file copy for every other row. */
+
+import { describe, expect, it } from 'vitest'
+import { en, presetDisplayText, zh } from '../src/client/locales.ts'
+
+const translate = (bundle: typeof en) => (key: keyof typeof en): string => bundle[key]
+
+describe('preset display copy', () => {
+  it.each([
+    ['standard', 'presetStandardName', 'presetStandardDescription'],
+    ['code', 'presetCodeName', 'presetCodeDescription'],
+    ['minimal', 'presetMinimalName', 'presetMinimalDescription'],
+    ['cordis', 'presetCordisName', 'presetCordisDescription'],
+  ] as const)('localizes the shipped %s preset in English and Chinese', (id, nameKey, descriptionKey) => {
+    const preset = { id, trust: 'system' as const, name: 'file name', description: 'file description' }
+
+    expect(presetDisplayText(preset, translate(en)))
+      .toEqual({ name: en[nameKey], description: en[descriptionKey] })
+    expect(presetDisplayText(preset, translate(zh)))
+      .toEqual({ name: zh[nameKey], description: zh[descriptionKey] })
+  })
+
+  it('keeps file metadata for user and unknown system presets', () => {
+    const fileCopy = { name: '我的标准', description: '团队自己的 preset。' }
+
+    expect(presetDisplayText({ id: 'standard', trust: 'user', ...fileCopy }, translate(en)))
+      .toEqual(fileCopy)
+    expect(presetDisplayText({ id: 'deployment-extra', trust: 'system', ...fileCopy }, translate(en)))
+      .toEqual(fileCopy)
+    expect(presetDisplayText({ id: 'bare', trust: 'user' }, translate(en)))
+      .toEqual({ name: 'bare' })
+  })
+})

+ 19 - 13
packages/client/ui-agent-preset/tests/section.spec.tsx

@@ -85,13 +85,13 @@ describe('the preset list', () => {
     await waitFor(() => { expect(actions.load).toHaveBeenCalledTimes(1) })
   })
 
-  it('shows the published name and description, falling back to the id', () => {
+  it('shows resolved copy for built-ins and falls back to custom ids', () => {
     renderSection()
 
-    // The name is what a picker reads; the id stays visible as the key the
+    // Display copy is what a picker reads; the id stays visible as the key the
     // composition and the session header actually carry.
-    expect(screen.getByText('标准模式')).toBeTruthy()
-    expect(screen.getByText('完整的编码 agent。')).toBeTruthy()
+    expect(screen.getByText(en.presetStandardName)).toBeTruthy()
+    expect(screen.getByText(en.presetStandardDescription)).toBeTruthy()
     const mine = rowFor('mine')
     expect(within(mine).getAllByText('mine').length).toBeGreaterThan(0)
     expect(within(mine).getByText(en.noDescription)).toBeTruthy()
@@ -134,7 +134,7 @@ describe('the preset list', () => {
   it('picks a preset by clicking its card, and the one in use is inert', () => {
     const actions = renderSection()
 
-    const inUse = within(rowFor('standard')).getByRole('button', { name: `${en.inUse}: 标准模式` })
+    const inUse = within(rowFor('standard')).getByRole('button', { name: `${en.inUse}: ${en.presetStandardName}` })
     expect(inUse).toHaveProperty('disabled', true)
     fireEvent.click(inUse)
 
@@ -150,8 +150,8 @@ describe('the preset list', () => {
     // the point. A custom preset is edited in its files, so its row leads
     // there instead; there is no editor for either.
     const standard = rowFor('standard')
-    expect(within(standard).getByRole('button', { name: `${en.view}: 标准模式` })).toBeTruthy()
-    expect(within(standard).queryByRole('button', { name: `${en.openLocation}: 标准模式` })).toBeNull()
+    expect(within(standard).getByRole('button', { name: `${en.view}: ${en.presetStandardName}` })).toBeTruthy()
+    expect(within(standard).queryByRole('button', { name: `${en.openLocation}: ${en.presetStandardName}` })).toBeNull()
     const mine = rowFor('mine')
     expect(within(mine).getByRole('button', { name: `${en.openLocation}: mine` })).toBeTruthy()
     expect(within(mine).queryByRole('button', { name: `${en.view}: mine` })).toBeNull()
@@ -161,13 +161,13 @@ describe('the preset list', () => {
     renderSection()
 
     expect(within(rowFor('mine')).getByRole('button', { name: `${en.delete}: mine` })).toBeTruthy()
-    expect(within(rowFor('standard')).queryByRole('button', { name: `${en.delete}: 标准模式` })).toBeNull()
+    expect(within(rowFor('standard')).queryByRole('button', { name: `${en.delete}: ${en.presetStandardName}` })).toBeNull()
   })
 
   it('disables duplication when nothing is writable, and says why', () => {
     renderSection({ authorable: false })
 
-    const duplicate = within(rowFor('standard')).getByRole('button', { name: `${en.duplicate}: 标准模式` })
+    const duplicate = within(rowFor('standard')).getByRole('button', { name: `${en.duplicate}: ${en.presetStandardName}` })
     expect(duplicate).toHaveProperty('disabled', true)
     expect(duplicate.getAttribute('data-tip')).toBe(en.duplicateUnavailable)
   })
@@ -205,7 +205,7 @@ describe('the preset list', () => {
     // There is no readable composition to offer; the reason on the card is
     // the whole story a shipped row can tell.
     const standard = rowFor('standard')
-    expect(within(standard).queryByRole('button', { name: `${en.view}: 标准模式` })).toBeNull()
+    expect(within(standard).queryByRole('button', { name: `${en.view}: ${en.presetStandardName}` })).toBeNull()
     expect(within(standard).getByRole('alert').textContent).toContain('not valid YAML')
   })
 
@@ -232,7 +232,7 @@ describe('the preset list', () => {
     fireEvent.click(within(rowFor('mine')).getByRole('button', { name: `${en.setDefault}: mine` }))
     fireEvent.click(within(rowFor('mine')).getByRole('button', { name: `${en.openLocation}: mine` }))
     fireEvent.click(within(rowFor('mine')).getByRole('button', { name: `${en.duplicate}: mine` }))
-    fireEvent.click(within(rowFor('standard')).getByRole('button', { name: `${en.view}: 标准模式` }))
+    fireEvent.click(within(rowFor('standard')).getByRole('button', { name: `${en.view}: ${en.presetStandardName}` }))
 
     expect(actions.makeDefault).toHaveBeenCalledWith('mine')
     expect(actions.openLocation).toHaveBeenCalledWith('mine')
@@ -311,7 +311,7 @@ describe('the copy dialog', () => {
     const actions = renderSection({ copy: draft })
 
     const dialog = screen.getByRole('dialog')
-    expect(dialog.getAttribute('aria-label')).toBe(`${en.copyTitle} · ${en.copyOf} 标准模式`)
+    expect(dialog.getAttribute('aria-label')).toBe(`${en.copyTitle} · ${en.copyOf} ${en.presetStandardName}`)
     expect(within(dialog).getByText(en.copyIntro)).toBeTruthy()
     fireEvent.change(within(dialog).getByPlaceholderText(en.presetIdPlaceholder), { target: { value: 'my-agent' } })
     fireEvent.change(within(dialog).getByPlaceholderText(en.displayNamePlaceholder), { target: { value: '我的模式' } })
@@ -374,11 +374,17 @@ describe('the read-only viewer', () => {
     renderSection({ view: { id: 'standard', title: '标准模式', content: '- id: tool-bash\n' } })
 
     const dialog = screen.getByRole('dialog')
-    expect(dialog.getAttribute('aria-label')).toBe(`${en.view} · 标准模式`)
+    expect(dialog.getAttribute('aria-label')).toBe(`${en.view} · ${en.presetStandardName}`)
     expect(within(dialog).getByText(en.composition)).toBeTruthy()
     expect(within(dialog).getByText(/tool-bash/).textContent).toBe('- id: tool-bash\n')
   })
 
+  it('keeps the loaded title when the viewed row leaves the roster', () => {
+    renderSection({ view: { id: 'retired', title: 'Retired mode', content: '- id: tool-bash\n' } })
+
+    expect(screen.getByRole('dialog').getAttribute('aria-label')).toBe(`${en.view} · Retired mode`)
+  })
+
   it('closes through the controller', () => {
     const actions = renderSection({ view: { id: 'standard', title: '标准模式', content: '- id: x\n' } })
 

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

+ 2 - 2
packages/preset/agent-presets/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/preset/agent-presets/README.md
-README.md: b6d469b26a0254adc654e5cc49d3df2d10817b2d
-README.zh.md: 60c7bc695c27bf2c0169a0e405aa84aa711b9b21
+README.md: 4f41361bc2f22ea87ecbd3e5699e119d8019cc16
+README.zh.md: 148bb06cfcc1c469db7b590e5facb45c35b5fb6d

+ 1 - 1
packages/preset/agent-presets/README.md

@@ -63,7 +63,7 @@ A preset may publish display text in an optional `preset.yml` beside its composi
 
 ```yaml
 name: 极简模式
-description: 只向模型呈现 bash 与 str_replace_editor,适合 benchmark 与最小复现。
+description: 仅提供 bash 与 str_replace_editor 的双工具编码 Agent,用于基准测试和最小复现。
 ```
 
 It carries display text ONLY. `id` is the directory name and `trust` comes from the root the preset was discovered under, so neither is writable here — otherwise a locally authored preset could name itself into the shipped set. It is a separate file because the composition is a top-level list of plugin rows: YAML cannot carry sibling keys beside it, and a fake metadata row would hand the Loader something to load.

+ 1 - 1
packages/preset/agent-presets/README.zh.md

@@ -63,7 +63,7 @@ preset 可以在组装文件旁的可选 `preset.yml` 里发布展示文本:
 
 ```yaml
 name: 极简模式
-description: 只向模型呈现 bash 与 str_replace_editor,适合 benchmark 与最小复现。
+description: 仅提供 bash 与 str_replace_editor 的双工具编码 Agent,用于基准测试和最小复现。
 ```
 
 它**只**承载展示文本。`id` 是目录名,`trust` 取自 preset 被发现时所在的根目录,两者都不可写在这里——否则本地创作的 preset 就能把自己命名进随附集合。之所以是独立文件:组装是插件行的顶层列表,YAML 无法在其旁携带同级键,而伪造一个元信息行等于递给 Loader 一个要加载的东西。