Merge remote-tracking branch 'origin/master' into worktree/subagent-missing-tools-fb2359

This commit is contained in:
Yichen Jiang
2026-08-10 20:32:58 +08:00
222 changed files with 3457 additions and 694 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -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)

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)
@@ -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', () => {

View File

@@ -70,7 +70,12 @@ let unmount: (() => void) | undefined
export function installAssembledBootEnv(): void {
beforeEach(() => {
localStorage.clear()
localStorage.setItem('dsh.locale', 'en')
// The locale service derives its provisional locale from the browser and
// takes an explicit choice only from Host settings, which this lane's
// fixture transport does not serve; pinning the navigator is what selects
// English here.
Object.defineProperty(navigator, 'languages', { value: ['en-US'], configurable: true })
Object.defineProperty(navigator, 'language', { value: 'en-US', configurable: true })
document.title = 'DeepSeek Harness'
vi.stubGlobal('ResizeObserver', ResizeObserverStub)
vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) =>
@@ -88,6 +93,11 @@ export function installAssembledBootEnv(): void {
document.head.querySelectorAll('style[data-plugin]').forEach((style) => { style.remove() })
document.title = ''
history.replaceState(null, '', '/')
// Deleting the own properties uncovers jsdom's own accessors again
// (Navigator declares both readonly, hence the erased receiver).
const ownNavigator = navigator as unknown as Record<string, unknown>
delete ownNavigator.languages
delete ownNavigator.language
vi.unstubAllGlobals()
})
}

View File

@@ -14,7 +14,7 @@ installAssembledBootEnv()
/** Open the fixture history session (the alpha log carrying the turn-72 image pair) and wait for its gallery. */
async function openFixtureSession(): Promise<void> {
const tree = await screen.findByRole('tree', { name: '会话' }, { timeout: 10_000 })
const tree = await screen.findByRole('tree', { name: 'Sessions' }, { timeout: 10_000 })
const group = (await within(tree).findAllByText('fixture'))
.map(el => el.closest<HTMLElement>('[role="treeitem"]'))
.find(el => el?.getAttribute('aria-expanded') !== null)
@@ -33,7 +33,6 @@ async function openFixtureSession(): Promise<void> {
}
it('renders the history image pair through the authorized attachment route and opens the lightbox', async () => {
localStorage.setItem('dsh.locale', 'zh')
mountAssembledApp()
await openFixtureSession()
@@ -73,24 +72,23 @@ it('renders the history image pair through the authorized attachment route and o
fireEvent.doubleClick(frame)
const lightbox = await screen.findByRole('dialog')
expect(within(lightbox).getByRole('img').getAttribute('src')?.split(':')[0]).toBe('blob')
fireEvent.click(within(lightbox).getByRole('button', { name: /关闭/ }))
fireEvent.click(within(lightbox).getByRole('button', { name: /Close/ }))
await waitFor(() => {
expect(screen.queryByRole('dialog')).toBeNull()
})
})
it('accepts pasted images into the composer rail in order and removes them', async () => {
localStorage.setItem('dsh.locale', 'zh')
mountAssembledApp()
const tree = await screen.findByRole('tree', { name: '会话' }, { timeout: 10_000 })
const start = tree.querySelector<HTMLButtonElement>('button[aria-label="在“fixture”中新建会话"]')
const tree = await screen.findByRole('tree', { name: 'Sessions' }, { timeout: 10_000 })
const start = tree.querySelector<HTMLButtonElement>('button[aria-label="New session in fixture"]')
if (start === null) throw new Error('fixture Workspace new-session action missing')
fireEvent.click(start)
// Image-only send arming is pinned at package level (input-bar.spec.tsx);
// this assembled lane pins the intake chain over the built graph.
const textarea = await screen.findByPlaceholderText('描述你想要构建的内容', {}, { timeout: 10_000 })
const textarea = await screen.findByPlaceholderText('Describe what you want to build', {}, { timeout: 10_000 })
const image = new File([new Uint8Array([137, 80, 78, 71])], 'pasted.png', { type: 'image/png' })
fireEvent.paste(textarea, {
clipboardData: {
@@ -102,7 +100,7 @@ it('accepts pasted images into the composer rail in order and removes them', asy
// The rail is an accessible group holding the draft thumbnail (queried via
// DOM: jsdom's a11y-visibility computation hides the composer subtree).
const rail = await waitFor(() => {
const el = document.querySelector('[role="group"][aria-label="待发送图片"]')
const el = document.querySelector('[role="group"][aria-label="Pending images"]')
if (el === null) throw new Error('attachment rail missing')
return el
}, { timeout: 5_000 })
@@ -129,10 +127,10 @@ it('accepts pasted images into the composer rail in order and removes them', asy
.toEqual(['pasted.png', 'second.png'])
})
const remove = [...rail.querySelectorAll('button[aria-label^="移除图片"]')]
const remove = [...rail.querySelectorAll('button[aria-label^="Remove image"]')]
if (remove.length !== 2) throw new Error('remove buttons missing')
for (const button of remove) fireEvent.click(button)
await waitFor(() => {
expect(document.querySelector('[role="group"][aria-label="待发送图片"]')).toBeNull()
expect(document.querySelector('[role="group"][aria-label="Pending images"]')).toBeNull()
})
})

View File

@@ -251,6 +251,8 @@ export interface LaunchOptions {
* 127.0.0.1; a non-resolving authority fails before Host trust is exercised.
*/
remoteAuthority?: string
/** Reuse an existing harness home so a second Host can verify user settings across origins. */
harnessHome?: string
}
/** Dispose the booted tree and remove both owned temp roots, reporting every independent cleanup failure. */
@@ -297,16 +299,19 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
// Isolated harness home: the settings/credentials rows resolve $DSH_HOME
// paths at load, and an in-process boot must NEVER touch the developer's
// real ~/.dsh document or credential file.
const harnessHome = join(workspaceCwd, '.dsh-home')
const harnessHome = options.harnessHome ?? join(workspaceCwd, '.dsh-home')
// Skill discovery is model-visible input, and its roots now resolve inside a
// PRESET — a subtree this lane's include patches cannot reach, because the
// roster mounts it directly per session rather than as a row of the booted
// tree. The row's documented fallback is the environment, so pin that: the
// whole scaffold lifetime, not just the boot, since presets mount when a
// session is created. Without this a developer's real ~/.dsh/skills silently
// enters replay requests and goldens while CI sees none.
// enters replay requests and goldens while CI sees none. `DSH_HOME` follows
// the resolved harness home so a scaffold sharing another's home — the
// cross-port persistence scenario — pins the same roots the settings and
// credentials rows were configured with.
const skillRootEnvironment = {
DSH_HOME: join(workspaceCwd, '.dsh-home'),
DSH_HOME: harnessHome,
DSH_AGENTS_HOME: join(workspaceCwd, '.agents-home'),
DSH_BUNDLED_SKILL_DIR: join(workspaceCwd, '.bundled-skills'),
}

View File

@@ -1,11 +1,10 @@
// Web e2e scenarios: the settings surface — the modal shell (trigger, nav,
// section switching, both close paths), the Appearance preference row (the
// real theme gesture — click 深色 and the whole cascade runs: ThemeService preference -> localStorage dsh.theme
// real theme gesture — click 深色 and the whole cascade runs: ThemeService preference -> Host settings
// -> theme/change -> ui-layout's presenter -> body attribute -> alias token +
// browser theme-color metadata)
// the Language row (settings-scoped localization + persisted dsh.locale),
// the busy-state Enter preference, plus Permission as the persisted default
// for subsequently created sessions.
// the Language row and busy-state Enter preference (both Host-backed), plus
// Permission as the persisted default for subsequently created sessions.
// Zero model calls: everything is pure client + persistence state on a blank
// frame, so there is no fixture and a stray stream would fail loud on the
// open llm seam.
@@ -153,23 +152,24 @@ describe('web e2e: settings modal and General preferences', () => {
expect(tripwire.pageErrors).toEqual([])
}, 60_000)
it('flips the theme through the Appearance cubes and persists across reload', async () => {
it('flips the theme through the Appearance cubes and persists across reload and a distinct port', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-settings-appearance'))
interface ThemeState {
attr: boolean
background: string
stored: string | null
/** Pre-migration localStorage key; the Host-backed world never writes it. */
legacy: string | null
themeColor: string | null
themeColorCount: number
token: string
}
const readState = async (): Promise<ThemeState> => await page.evaluate(() => {
const readState = async (target: Page = page): Promise<ThemeState> => await target.evaluate(() => {
const metas = document.head.querySelectorAll<HTMLMetaElement>('meta[name="theme-color"]')
const computed = getComputedStyle(document.body)
return {
attr: document.body.hasAttribute('data-ds-dark-theme'),
background: computed.backgroundColor,
stored: localStorage.getItem('dsh.theme'),
legacy: localStorage.getItem('dsh.theme'),
themeColor: metas[0]?.content ?? null,
themeColorCount: metas.length,
token: computed.getPropertyValue('--dsw-alias-bg-base').trim(),
@@ -193,27 +193,51 @@ describe('web e2e: settings modal and General preferences', () => {
const darkCube = dialog.getByRole('button', { name: '深色' })
expect(await darkCube.getAttribute('aria-pressed')).toBe('false')
await darkCube.click()
// The full cascade: pressed state, persisted preference, body attribute,
// The full cascade: pressed state, Host-backed preference, body attribute,
// alias token flip — all from one real user gesture.
await expect.poll(() => darkCube.getAttribute('aria-pressed'), { timeout: 5_000 }).toBe('true')
const dark = await readState()
expect(dark.attr).toBe(true)
expect(dark.stored).toBe('dark')
expect(dark.legacy).toBeNull()
expect(dark.token).not.toBe(light.token)
expectThemeColorSynchronized(dark)
await expect.poll(async () => readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8'), { timeout: 5_000 })
.toMatch(/ui-theme:\n\s+preference: dark/)
await page.keyboard.press('Escape')
// Reload: the preference survives boot (restore + presenter initial apply).
// Reload: the preference survives the background Host read + presenter update.
const warningStart = tripwire.warnings.length
await page.reload({ waitUntil: 'load' })
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
acknowledgeReloadConnectionLoss(tripwire, warningStart)
await page.emulateMedia({ colorScheme: 'light' })
await expect.poll(async () => (await readState()).attr, { timeout: 5_000 }).toBe(true)
const reloaded = await readState()
expect(reloaded.attr).toBe(true)
expect(reloaded.stored).toBe('dark')
expect(reloaded.legacy).toBeNull()
expectThemeColorSynchronized(reloaded)
// A second live Host binds another ephemeral port but shares the same
// user-settings home. Its fresh origin has no theme localStorage and still
// converges to dark before the settings dialog opens.
const second = await launchWebScaffold({ harnessHome: scaffold.harnessHome })
const secondPage = await browser.newPage({ viewport: { width: 1680, height: 1000 }, locale: ZH_BROWSER_LOCALE })
const secondTripwire = watchConsole(secondPage)
try {
expect(second.baseUrl).not.toBe(scaffold.baseUrl)
await secondPage.emulateMedia({ colorScheme: 'light' })
await secondPage.goto(second.baseUrl, { waitUntil: 'load' })
await secondPage.waitForSelector('[class*="frame"]', { timeout: 30_000 })
await expect.poll(async () => (await readState(secondPage)).attr, { timeout: 5_000 }).toBe(true)
const secondState = await readState(secondPage)
expect(secondState.legacy).toBeNull()
expectThemeColorSynchronized(secondState)
expect(secondTripwire.pageErrors).toEqual([])
expect(secondTripwire.warnings).toEqual([])
} finally {
await secondPage.close()
await second.close()
}
// `system` follows the emulated OS scheme (dark stays dark, light clears).
await page.getByRole('button', { name: '设置', exact: true }).click()
const systemCube = page.getByRole('dialog', { name: '设置' }).getByRole('button', { name: '跟随系统' })
@@ -233,7 +257,7 @@ describe('web e2e: settings modal and General preferences', () => {
expect(tripwire.pageErrors).toEqual([])
}, 90_000)
it('persists the busy-state Enter behavior across reload and restores Queue', async () => {
it('persists the busy-state Enter behavior across reload and a distinct port', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-settings-enter-behavior'))
await page.getByRole('button', { name: '设置', exact: true }).click()
const dialog = page.getByRole('dialog', { name: '设置' })
@@ -241,7 +265,9 @@ describe('web e2e: settings modal and General preferences', () => {
await dialog.getByRole('button', { name: '排队发送' }).click()
await page.getByRole('menuitem', { name: '插话发送' }).click()
await dialog.getByRole('button', { name: '插话发送' }).waitFor({ timeout: 10_000 })
expect(await page.evaluate(() => localStorage.getItem('dsh.conversation.busyEnter'))).toBe('steer')
expect(await page.evaluate(() => localStorage.getItem('dsh.conversation.busyEnter'))).toBeNull()
await expect.poll(async () => readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8'), { timeout: 5_000 })
.toMatch(/ui-conversation:\n\s+busyEnter: steer/)
await page.keyboard.press('Escape')
const warningStart = tripwire.warnings.length
@@ -251,15 +277,36 @@ describe('web e2e: settings modal and General preferences', () => {
await page.getByRole('button', { name: '设置', exact: true }).click()
const reloaded = page.getByRole('dialog', { name: '设置' })
await reloaded.getByRole('button', { name: '插话发送' }).waitFor({ timeout: 10_000 })
const second = await launchWebScaffold({ harnessHome: scaffold.harnessHome })
const secondPage = await browser.newPage({ viewport: { width: 1680, height: 1000 }, locale: ZH_BROWSER_LOCALE })
const secondTripwire = watchConsole(secondPage)
try {
expect(second.baseUrl).not.toBe(scaffold.baseUrl)
await secondPage.goto(second.baseUrl, { waitUntil: 'load' })
await secondPage.waitForSelector('[class*="frame"]', { timeout: 30_000 })
await secondPage.getByRole('button', { name: '设置', exact: true }).click()
await secondPage.getByRole('dialog', { name: '设置' })
.getByRole('button', { name: '插话发送' }).waitFor({ timeout: 10_000 })
expect(await secondPage.evaluate(() => localStorage.getItem('dsh.conversation.busyEnter'))).toBeNull()
expect(secondTripwire.pageErrors).toEqual([])
expect(secondTripwire.warnings).toEqual([])
} finally {
await secondPage.close()
await second.close()
}
await reloaded.getByRole('button', { name: '插话发送' }).click()
await page.getByRole('menuitem', { name: '排队发送' }).click()
await reloaded.getByRole('button', { name: '排队发送' }).waitFor({ timeout: 10_000 })
expect(await page.evaluate(() => localStorage.getItem('dsh.conversation.busyEnter'))).toBe('queue')
expect(await page.evaluate(() => localStorage.getItem('dsh.conversation.busyEnter'))).toBeNull()
await expect.poll(async () => readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8'), { timeout: 5_000 })
.toMatch(/ui-conversation:\n\s+busyEnter: queue/)
await page.keyboard.press('Escape')
expect(tripwire.pageErrors).toEqual([])
}, 90_000)
it('switches the settings surface language and persists dsh.locale', async () => {
it('persists the settings language across reload and a distinct port', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-settings-language'))
await page.getByRole('button', { name: '设置', exact: true }).click()
const zhDialog = page.getByRole('dialog', { name: '设置' })
@@ -276,7 +323,9 @@ describe('web e2e: settings modal and General preferences', () => {
await enDialog.waitFor({ timeout: 10_000 })
expect(await enDialog.getByRole('button', { name: 'General' }).getAttribute('aria-current')).toBe('true')
await expect.poll(() => enDialog.getByText('Appearance', { exact: true }).count(), { timeout: 5_000 }).toBe(1)
expect(await page.evaluate(() => localStorage.getItem('dsh.locale'))).toBe('en')
expect(await page.evaluate(() => localStorage.getItem('dsh.locale'))).toBeNull()
await expect.poll(async () => readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8'), { timeout: 5_000 })
.toMatch(/locale:\n\s+preference: en/)
// Reload keeps English; then restore zh so shared page state (and the
// other specs' 设置-anchored selectors + goldens) see the default again.
const warningStart = tripwire.warnings.length
@@ -285,24 +334,47 @@ describe('web e2e: settings modal and General preferences', () => {
acknowledgeReloadConnectionLoss(tripwire, warningStart)
const enTrigger = page.getByRole('button', { name: 'Settings' })
await enTrigger.waitFor({ timeout: 10_000 })
// A Chinese browser on another port still receives the explicit English
// preference from the shared Host settings document.
const second = await launchWebScaffold({ harnessHome: scaffold.harnessHome })
const secondPage = await browser.newPage({ viewport: { width: 1680, height: 1000 }, locale: ZH_BROWSER_LOCALE })
const secondTripwire = watchConsole(secondPage)
try {
expect(second.baseUrl).not.toBe(scaffold.baseUrl)
await secondPage.goto(second.baseUrl, { waitUntil: 'load' })
await secondPage.waitForSelector('[class*="frame"]', { timeout: 30_000 })
await secondPage.getByRole('button', { name: 'Settings', exact: true }).click()
await secondPage.getByRole('dialog', { name: 'Settings' })
.getByRole('button', { name: 'English' }).waitFor({ timeout: 10_000 })
expect(await secondPage.evaluate(() => localStorage.getItem('dsh.locale'))).toBeNull()
expect(secondTripwire.pageErrors).toEqual([])
expect(secondTripwire.warnings).toEqual([])
} finally {
await secondPage.close()
await second.close()
}
await enTrigger.click()
await page.getByRole('dialog', { name: 'Settings' }).getByRole('button', { name: 'English' }).click()
await page.getByRole('menuitem', { name: '中文' }).click()
await page.getByRole('dialog', { name: '设置' }).waitFor({ timeout: 10_000 })
expect(await page.evaluate(() => localStorage.getItem('dsh.locale'))).toBe('zh')
expect(await page.evaluate(() => localStorage.getItem('dsh.locale'))).toBeNull()
await expect.poll(async () => readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8'), { timeout: 5_000 })
.toMatch(/locale:\n\s+preference: zh/)
await page.keyboard.press('Escape')
expect(tripwire.pageErrors).toEqual([])
}, 90_000)
it('opens an English browser in English without any stored preference', async () => {
// A second page under a different browser language: nothing is persisted
// for it, so the settings surface must follow the browser rather than the
// product fallback the shared zh page shows.
// A fresh Host home has no locale preference, so its surface follows the
// browser rather than the product fallback.
const fresh = await launchWebScaffold({})
const enPage = await browser.newPage({ viewport: { width: 1680, height: 1000 }, locale: 'en-US' })
const enTripwire = watchConsole(enPage)
onTestFailed(() => saveFailureShot(enPage, 'web-e2e-settings-browser-language'))
try {
await enPage.goto(scaffold.baseUrl, { waitUntil: 'load' })
await enPage.goto(fresh.baseUrl, { waitUntil: 'load' })
await enPage.waitForSelector('[class*="frame"]', { timeout: 30_000 })
expect(await enPage.evaluate(() => localStorage.getItem('dsh.locale'))).toBeNull()
await enPage.getByRole('button', { name: 'Settings', exact: true }).click()
@@ -315,6 +387,7 @@ describe('web e2e: settings modal and General preferences', () => {
expect(enTripwire.warnings).toEqual([])
} finally {
await enPage.close()
await fresh.close()
}
}, 90_000)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

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

View File

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

View File

@@ -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."

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

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

View File

@@ -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":

View File

@@ -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":

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

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

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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 () => {

View File

@@ -18,18 +18,17 @@ export const REPO_ROOT = fileURLToPath(new URL('../../..', import.meta.url))
export const ZH_BROWSER_LOCALE = 'zh-CN'
/**
* Open the standard browser-test page with English selected before client
* boot. This keeps role locators and goldens deterministic across localized
* component migrations; the scenarios asserting the Chinese surface bypass
* this helper and advertise {@link ZH_BROWSER_LOCALE} instead.
* Open the standard browser-test page advertising English before client boot.
* This keeps role locators and goldens deterministic while leaving the Host
* settings document free to override the provisional browser-derived locale;
* scenarios asserting the Chinese surface advertise
* {@link ZH_BROWSER_LOCALE} instead.
* @param browser - Playwright browser owning the page.
* @param height - Viewport height; width is fixed to the lane baseline.
* @returns the initialized page.
*/
export async function newEnglishPage(browser: Browser, height = 1000): Promise<Page> {
const page = await browser.newPage({ viewport: { width: 1680, height } })
await page.addInitScript(() => { localStorage.setItem('dsh.locale', 'en') })
return page
return await browser.newPage({ viewport: { width: 1680, height }, locale: 'en-US' })
}
/** Fail loud on a stale checkout instead of testing yesterday's bundle. */