Merge remote-tracking branch 'origin/master' into xtr/react-loop-simplification

# Conflicts:
#	.agents/notes/implemented/feature/2026-07-22-docked-web-goal-bar.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-22-docked-web-goal-bar.md
#	.agents/notes/implemented/feature/2026-07-22-docked-web-goal-bar.zh.md
#	docs/config-catalog.md
#	packages/client/ui-goal/README.i18n.yaml
#	packages/host/apiproxy/tests/api-proxy-fork.spec.ts
#	packages/ui/tui/README.i18n.yaml
This commit is contained in:
_Kerman
2026-08-02 00:59:11 +08:00
153 changed files with 1768 additions and 540 deletions

View File

@@ -70,14 +70,7 @@ describe('web e2e: Full access confirmation', () => {
const access = page.locator('button[aria-label^="访问模式"]').first()
await access.waitFor({ timeout: 10_000 })
// Normalize the starting preset through the real command path. The
// shipped web config may already start at Full access.
if ((await access.getAttribute('aria-label'))?.endsWith('Full access') === true) {
await access.click()
await page.getByRole('menuitem', { name: 'Workspace Write' }).click()
await expect.poll(() => access.getAttribute('aria-label'), { timeout: 10_000 })
.toBe('访问模式当前Workspace Write')
}
expect(await access.getAttribute('aria-label')).toBe('访问模式当前Workspace Write')
await access.click()
await page.getByRole('menuitem', { name: 'Full access' }).click()

View File

@@ -108,7 +108,7 @@ it('boots the built plugin graph and renders a fixture session end to end', asyn
// Opening a session reaches chat content through the fixture transport.
fireEvent.click(await within(tree).findByText('Fixture 历史会话'))
await waitFor(() => {
expect(document.querySelector('[data-sample="bash-global"]')).not.toBeNull()
expect(document.querySelector('[data-sample="bash"]')).not.toBeNull()
}, { timeout: 10_000 })
// The write/edit turns render a real diff card through the assembled graph

View File

@@ -112,7 +112,7 @@ describe('web e2e: Code Mode round renders nested sub-calls', () => {
// the bash sub-call landed in the bash sample registration.
const nest = page.locator('[data-subcalls]').first()
await nest.waitFor({ timeout: 10_000 })
expect(await nest.locator('[data-sample="bash-global"]').count()).toBeGreaterThanOrEqual(1)
expect(await nest.locator('[data-sample="bash"]').count()).toBeGreaterThanOrEqual(1)
// The failing read sub-call wears the same error state a native failed
// row wears (the recorded program tolerates a read of missing.txt).
expect(await nest.locator('[data-state="error"]').count()).toBeGreaterThanOrEqual(1)
@@ -123,7 +123,7 @@ describe('web e2e: Code Mode round renders nested sub-calls', () => {
const nest = page.locator('[data-subcalls]').first()
const frame = page.locator('[style*="grid-template-columns"]').first()
expect(await frame.getAttribute('data-details-collapsed')).toBe('true')
await nest.locator('[data-sample="bash-global"]').first().click()
await nest.locator('[data-sample="bash"]').first().click()
// Tool rows do not drive layout geometry; the Session's default panel stays closed.
await expect.poll(() => frame.getAttribute('data-details-collapsed'), { timeout: 5_000 }).toBe('true')
})

View File

@@ -0,0 +1,71 @@
// Keyless assembled-browser coverage for the goal bar over the shipped Web
// bundles and FixtureApiClient wire. The command creates a real projected
// goal in the fixture session; the golden pins the active strip, while the
// clear gesture proves the acknowledged tombstone leaves neither stale chrome
// nor a duplicate-mutation error.
import { fileURLToPath } from 'node:url'
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 {
assertFixtureInventory, captureStableAria, compareOrRefreshGolden,
launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold,
} from './scaffold.ts'
import { newEnglishPage, saveFailureShot } from './support.ts'
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/goal-bar', import.meta.url))
const ACTIVE_EXPECTED = join(SNAPSHOT_DIR, 'active.expected.md')
const OVERLAY = fileURLToPath(new URL('./goal-bar.overlay.yml', import.meta.url))
const MODE = webSnapshotMode()
describe('web e2e: goal bar clear convergence', () => {
let scaffold: WebScaffold
let browser: Browser
let page: Page
let tripwire: ReturnType<typeof watchConsole>
beforeAll(async () => {
scaffold = await launchWebScaffold({ extraOverlayPath: OVERLAY, welcomeNoticePending: true })
browser = await chromium.launch()
page = await newEnglishPage(browser)
tripwire = watchConsole(page)
await page.goto(`${scaffold.baseUrl}?fixture`, { waitUntil: 'load' })
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
}, 120_000)
afterAll(async () => {
await browser?.close()
await scaffold?.close()
})
it('renders one active goal and clears it without exposing a stale error', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-goal-bar-clear'))
// Startup reuses the fixture workspace's blank session, keeping this
// command independent of alpha's running replay and pending question.
const input = page.getByPlaceholder('Describe what you want to build')
await input.waitFor({ timeout: 10_000 })
await input.fill('/goal guard rapid clear clicks')
await input.press('Enter')
const bar = page.locator('[data-goal-bar]')
await bar.waitFor({ timeout: 10_000 })
const snapshot = await captureStableAria(page, '[data-goal-bar]', scaffold.workspaceCwd)
await compareOrRefreshGolden(ACTIVE_EXPECTED, snapshot, MODE)
const clear = bar.getByRole('button', { name: 'Clear goal' })
await clear.evaluate((button) => {
const control = button as HTMLButtonElement
control.click()
control.click()
})
await expect.poll(() => page.locator('[data-goal-bar]').count(), { timeout: 10_000 }).toBe(0)
expect(await page.getByText(/no current goal/iu).count()).toBe(0)
expect(tripwire.pageErrors).toEqual([])
expect(tripwire.warnings).toEqual([])
}, 60_000)
it.skipIf(MODE === 'record')('keeps the fixture inventory closed', async () => {
await assertFixtureInventory(SNAPSHOT_DIR, ['active.expected.md'])
})
})

View File

@@ -0,0 +1,5 @@
# The client-side FixtureApiClient intentionally rejects settings writes, so
# this goal-only scenario omits the durable welcome step that would otherwise
# cover the page. Onboarding owns separate assembled-browser coverage.
- id: ui-settings-general
disabled: true

View File

@@ -184,7 +184,7 @@ describe('web e2e: navigation & panes over a rich seeded session', () => {
it.skipIf(MODE === 'record')('bash and file-path rows leave the default details column closed', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-navigation-details'))
await page.getByRole('tab', { name: 'Chat' }).click()
const bashRow = page.locator('[data-sample="bash-global"]').first()
const bashRow = page.locator('[data-sample="bash"]').first()
await bashRow.waitFor({ timeout: 15_000 })
const frame = page.locator('[style*="grid-template-columns"]').first()
expect(await frame.getAttribute('data-details-collapsed')).toBe('true')
@@ -194,7 +194,7 @@ describe('web e2e: navigation & panes over a rich seeded session', () => {
await expect.poll(() => frame.getAttribute('data-details-collapsed'), { timeout: 5_000 }).toBe('true')
// The card's own controls are outside the summary row and must not open
// details either — the expanded terminal card is read in place.
await page.locator('[data-sample="bash-global"] ~ div [data-terminal] [class*="_copyButton_"]').first().click()
await page.locator('[data-sample="bash"] ~ div [data-terminal] [class*="_copyButton_"]').first().click()
await expect.poll(() => frame.getAttribute('data-details-collapsed'), { timeout: 5_000 }).toBe('true')
// Read summaries are host-open file links; they also must not open details.
const fileLink = page.locator('[data-variant="read"] button').first()
@@ -210,10 +210,10 @@ describe('web e2e: navigation & panes over a rich seeded session', () => {
// tool-row interaction): open it if a previous case left it collapsed.
// Expanded, the recorded command's own output sits in the message flow,
// derived from the logged call/result presentations alone.
const bashRow = page.locator('[data-sample="bash-global"]').first()
const bashRow = page.locator('[data-sample="bash"]').first()
await bashRow.waitFor({ timeout: 15_000 })
if (await bashRow.getAttribute('aria-expanded') !== 'true') await bashRow.click()
const card = page.locator('[data-sample="bash-global"] ~ div [data-terminal]').first()
const card = page.locator('[data-sample="bash"] ~ div [data-terminal]').first()
await card.waitFor({ timeout: 15_000 })
// Real layout, not jsdom's stub (which computes no geometry at all):
// squeeze the output pane below its content width and the line must keep

View File

@@ -143,7 +143,7 @@ describe('assembled search card', () => {
// Wait for chat content to reach the fixture's later turns (the bash sample
// is turn 65, the grep card turn 66).
await waitFor(() => {
expect(document.querySelector('[data-sample="bash-global"]')).not.toBeNull()
expect(document.querySelector('[data-sample="bash"]')).not.toBeNull()
}, { timeout: 10_000 })
// The grep turn's keyed SearchRow composes ToolRow: the card is collapsed
// by default, so wait for the summary row, then expand it to reach the card.

View File

@@ -339,19 +339,19 @@ describe('web e2e: seeded history renders through cold resume', () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-seeded-command-row'))
// The Access chip submits `/permission <preset>` — a host command with no
// model call, so the settled row renders keylessly over this cold history.
// The row copy is the assertion: `permission · preset workspace-write`,
// The row copy is the assertion: `permission · preset read-only`,
// where neither half repeats the other (the dispatched `/` and its
// argument stay out of the title, and the settlement text never restates
// the command's own name).
await page.getByRole('button', { name: 'Access mode, current: Full access' }).click()
await page.getByRole('menuitem', { name: 'Workspace Write' }).click()
await page.getByRole('button', { name: 'Access mode, current: Workspace Write' }).waitFor({ timeout: 10_000 })
await page.getByRole('button', { name: 'Access mode, current: Workspace Write' }).click()
await page.getByRole('menuitem', { name: 'Read Only' }).click()
await page.getByRole('button', { name: 'Access mode, current: Read Only' }).waitFor({ timeout: 10_000 })
// Scoped to the row itself, so unrelated page text that happens to read
// `permission` (a future resident slash menu) cannot satisfy or break it.
const row = page.locator('[data-variant="others"]').filter({ hasText: 'preset workspace-write' })
const row = page.locator('[data-variant="others"]').filter({ hasText: 'preset read-only' })
await expect.poll(() => row.count(), { timeout: 10_000 }).toBe(1)
expect(await row.getByText('permission', { exact: true }).count()).toBe(1)
expect(await row.getByText('/permission workspace-write', { exact: true }).count()).toBe(0)
expect(await row.getByText('/permission read-only', { exact: true }).count()).toBe(0)
const snapshot = (await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd))
.split(SEED_ID).join('{{seededId}}')
await compareOrRefreshGolden(COMMAND_ROW_EXPECTED, snapshot, MODE)

View File

@@ -57,7 +57,7 @@ describe('web e2e: settings modal and General preferences', () => {
expect(await trigger.getAttribute('aria-expanded')).toBe('true')
// General is active by default; Permission, Language and Appearance are functional.
expect(await dialog.getByRole('button', { name: '通用设置' }).getAttribute('aria-current')).toBe('true')
await dialog.getByRole('button', { name: 'Full access' }).waitFor({ timeout: 10_000 })
await dialog.getByRole('button', { name: 'Workspace Write' }).waitFor({ timeout: 10_000 })
await expect.poll(() => dialog.getByText('语言', { exact: true }).count(), { timeout: 5_000 }).toBe(1)
await expect.poll(() => dialog.getByText('外观', { exact: true }).count(), { timeout: 5_000 }).toBe(1)
// Golden of the freshly opened dialog (default zh, General active).
@@ -82,12 +82,12 @@ describe('web e2e: settings modal and General preferences', () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-settings-permission'))
const existing = scaffold.ctx.sessions.create(SessionId('settings-permission-before'))
expect(existing.events.find(event => event.type === 'permission/preset')?.data)
.toEqual({ preset: 'danger-full-access' })
.toEqual({ preset: 'workspace-write' })
await page.getByRole('button', { name: '设置', exact: true }).click()
const dialog = page.getByRole('dialog', { name: '设置' })
await dialog.waitFor({ timeout: 10_000 })
const selector = dialog.getByRole('button', { name: 'Full access' })
const selector = dialog.getByRole('button', { name: 'Workspace Write' })
await selector.waitFor({ timeout: 10_000 })
await expect.poll(() => selector.isEnabled(), { timeout: 5_000 }).toBe(true)
await selector.click()
@@ -98,7 +98,7 @@ describe('web e2e: settings modal and General preferences', () => {
expect(document).toContain('permission:')
expect(document).toContain('defaultPreset: read-only')
expect(existing.events.find(event => event.type === 'permission/preset')?.data)
.toEqual({ preset: 'danger-full-access' })
.toEqual({ preset: 'workspace-write' })
const created = scaffold.ctx.sessions.create(SessionId('settings-permission-after'))
expect(created.events.map(event => [event.type, event.data])).toEqual([

View File

@@ -10,6 +10,7 @@ import { canonicalPath, writableRoots } from '@deepseek-ai/dsh-sandbox'
import type {} from '@deepseek-ai/dsh-tools'
import type {} from '@deepseek-ai/dsh-sandbox-policy'
import type {} from '@deepseek-ai/dsh-user-approval'
import type {} from '@deepseek-ai/dsh-permission'
import { launchWebScaffold, type WebScaffold } from './scaffold.ts'
/**
@@ -63,7 +64,7 @@ afterEach(async () => {
scaffold = undefined
})
it('assembles the shipped Web catalog and keeps its access default', async () => {
it('assembles the shipped Web catalog with the confined access default', async () => {
scaffold = await launchWebScaffold()
const names = scaffold.ctx.tools.schemas().map(schema => schema.name).sort()
expect(names.filter(name => !RIPGREP_TOOLS.includes(name))).toEqual(EXPECTED_TOOLS)
@@ -76,8 +77,7 @@ it('assembles the shipped Web catalog and keeps its access default', async () =>
expect(writableRoots(scaffold.ctx.sandboxPolicy.resolve({ mode: 'workspace-write' }))).toEqual(
expect.arrayContaining([canonicalPath('/tmp'), canonicalPath(tmpdir())]),
)
// The Web surface keeps its shipped access default; the base's confined one
// reaches the TUI. Pinning both keeps a base change from moving Web silently.
expect(scaffold.ctx.sandboxPolicy.defaultMode).toBe('danger-full-access')
expect(scaffold.ctx.approval.config.policy).toBe('never')
expect(scaffold.ctx.sandboxPolicy.defaultMode).toBe('workspace-write')
expect(scaffold.ctx.approval.config.policy).toBe('ask')
expect(scaffold.ctx.permission.defaultPreset).toBe('workspace-write')
}, 120_000)

View File

@@ -588,7 +588,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY || notReady.length > 0)('web smoke
// Bash renders through the third-party sample registration. Match that
// exact row: other clickable variants (for example Think disclosure)
// may precede the tool call in document order.
const toolRow = page.locator('[data-sample="bash-global"]')
const toolRow = page.locator('[data-sample="bash"]')
await toolRow.waitFor({ timeout: 120_000 })
await screen(page, '08-bash-round')
expect(await detailsTrack(page)).toBe(0)

View File

@@ -38,7 +38,7 @@
- textbox "Message the agent"
- button "Commands":
- img
- 'button "Access mode, current: Full access"': Full access
- 'button "Access mode, current: Workspace Write"': Workspace Write
- button "Select model, current DeepSeek-V4-Flash":
- text: DeepSeek-V4-Flash
- img

View File

@@ -53,7 +53,7 @@
- textbox "Message the agent"
- button "Commands":
- img
- 'button "Access mode, current: Full access"': Full access
- 'button "Access mode, current: Workspace Write"': Workspace Write
- button "Select model, current DeepSeek-V4-Flash":
- text: DeepSeek-V4-Flash
- img

View File

@@ -33,7 +33,7 @@
- textbox "Message the agent"
- button "Commands":
- img
- 'button "Access mode, current: Full access"': Full access
- 'button "Access mode, current: Workspace Write"': Workspace Write
- button "Select model, current DeepSeek-V4-Flash":
- text: DeepSeek-V4-Flash
- img

View File

@@ -0,0 +1,8 @@
- img
- text: Ongoing Goal guard rapid clear clicks
- button "Pause goal":
- img
- button "Edit goal":
- img
- button "Clear goal":
- img

View File

@@ -28,7 +28,7 @@
- textbox "Describe what you want to build"
- button "Commands":
- img
- 'button "Access mode, current: Full access"': Full access
- 'button "Access mode, current: Workspace Write"': Workspace Write
- button "Select model, current DeepSeek-V4-Flash":
- text: DeepSeek-V4-Flash
- img

View File

@@ -28,7 +28,7 @@
- textbox "Describe what you want to build"
- button "Commands":
- img
- 'button "Access mode, current: Full access"': Full access
- 'button "Access mode, current: Workspace Write"': Workspace Write
- button "Plan mode on, press to turn off": Plan
- button "Select model, current deepseek-v4-flash":
- text: deepseek-v4-flash

View File

@@ -25,7 +25,7 @@
- textbox "Message the agent"
- button "Commands":
- img
- 'button "Access mode, current: Full access"': Full access
- 'button "Access mode, current: Workspace Write"': Workspace Write
- button "Select model, current DeepSeek-V4-Flash":
- text: DeepSeek-V4-Flash
- img

View File

@@ -22,7 +22,7 @@
- textbox "Message the agent"
- button "Commands":
- img
- 'button "Access mode, current: Full access"': Full access
- 'button "Access mode, current: Workspace Write"': Workspace Write
- button "Select model, current DeepSeek-V4-Flash":
- text: DeepSeek-V4-Flash
- img

View File

@@ -18,7 +18,7 @@
- textbox "Message the agent"
- button "Commands":
- img
- 'button "Access mode, current: Full access"': Full access
- 'button "Access mode, current: Workspace Write"': Workspace Write
- button "Select model, current DeepSeek-V4-Flash":
- text: DeepSeek-V4-Flash
- img

View File

@@ -17,7 +17,7 @@
- textbox "Message the agent"
- button "Commands":
- img
- 'button "Access mode, current: Full access"': Full access
- 'button "Access mode, current: Workspace Write"': Workspace Write
- button "Select model, current DeepSeek-V4-Flash":
- text: DeepSeek-V4-Flash
- img

View File

@@ -27,7 +27,7 @@
- textbox "Message the agent"
- button "Commands":
- img
- 'button "Access mode, current: Full access"': Full access
- 'button "Access mode, current: Workspace Write"': Workspace Write
- button "Select model, current DeepSeek-V4-Flash":
- text: DeepSeek-V4-Flash
- img

View File

@@ -36,7 +36,7 @@
- textbox "Message the agent"
- button "Commands":
- img
- 'button "Access mode, current: Full access"': Full access
- 'button "Access mode, current: Workspace Write"': Workspace Write
- button "Select model, current deepseek-v4-flash":
- text: deepseek-v4-flash
- img

View File

@@ -38,7 +38,7 @@
- textbox "Message the agent"
- button "Commands":
- img
- 'button "Access mode, current: Full access"': Full access
- 'button "Access mode, current: Workspace Write"': Workspace Write
- button "Select model, current DeepSeek-V4-Flash":
- text: DeepSeek-V4-Flash
- img

View File

@@ -33,7 +33,7 @@
- textbox "Message the agent"
- button "Commands":
- img
- 'button "Access mode, current: Full access"': Full access
- 'button "Access mode, current: Workspace Write"': Workspace Write
- button "Select model, current DeepSeek-V4-Flash":
- text: DeepSeek-V4-Flash
- img

View File

@@ -18,7 +18,7 @@
- textbox "Message the agent"
- button "Commands":
- img
- 'button "Access mode, current: Full access"': Full access
- 'button "Access mode, current: Workspace Write"': Workspace Write
- button "Select model, current DeepSeek-V4-Flash":
- text: DeepSeek-V4-Flash
- img

View File

@@ -31,7 +31,7 @@
- textbox "Message the agent"
- button "Commands":
- img
- 'button "Access mode, current: Full access"': Full access
- 'button "Access mode, current: Workspace Write"': Workspace Write
- button "Select model, current DeepSeek-V4-Flash":
- text: DeepSeek-V4-Flash
- img

View File

@@ -1,6 +1,5 @@
- banner:
- navigation "Session hierarchy":
- button "Reply with a one-sentence description" [disabled]
- heading "Reply with a one-sentence description" [level=1]
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"
@@ -36,7 +35,7 @@
- textbox "Message the agent"
- button "Commands":
- img
- 'button "Access mode, current: Full access"': Full access
- 'button "Access mode, current: Workspace Write"': Workspace Write
- button "Select model, current DeepSeek-V4-Flash":
- text: DeepSeek-V4-Flash
- img

View File

@@ -24,7 +24,7 @@
- textbox "Message the agent"
- button "Commands":
- img
- 'button "Access mode, current: Full access"': Full access
- 'button "Access mode, current: Workspace Write"': Workspace Write
- button "Select model, current DeepSeek-V4-Flash":
- text: DeepSeek-V4-Flash
- img

View File

@@ -40,11 +40,11 @@
- img
- text: Context injection
- img
- text: permission preset workspace-write
- text: permission preset read-only
- textbox "Message the agent"
- button "Commands":
- img
- 'button "Access mode, current: Workspace Write"': Workspace Write
- 'button "Access mode, current: Read Only"': Read Only
- button "Select model, current deepseek-v4-flash":
- text: deepseek-v4-flash
- img

View File

@@ -42,7 +42,7 @@
- textbox "Message the agent"
- button "Commands":
- img
- 'button "Access mode, current: Full access"': Full access
- 'button "Access mode, current: Workspace Write"': Workspace Write
- button "Select model, current deepseek-v4-flash":
- text: deepseek-v4-flash
- img

View File

@@ -11,8 +11,8 @@
- img
- text: 关闭
- text: 权限 选择新会话的默认权限模式
- button "Full access":
- text: Full access
- button "Workspace Write":
- text: Workspace Write
- img
- text: 语言
- button "中文":

View File

@@ -34,7 +34,7 @@
- textbox "Message the agent"
- button "Commands":
- img
- 'button "Access mode, current: Full access"': Full access
- 'button "Access mode, current: Workspace Write"': Workspace Write
- button "Select model, current DeepSeek-V4-Flash":
- text: DeepSeek-V4-Flash
- img

View File

@@ -25,7 +25,7 @@
- textbox "Message the agent"
- button "Commands":
- img
- 'button "Access mode, current: Full access"': Full access
- 'button "Access mode, current: Workspace Write"': Workspace Write
- button "Select model, current DeepSeek-V4-Flash":
- text: DeepSeek-V4-Flash
- img

View File

@@ -35,7 +35,7 @@ export async function newEnglishPage(browser: Browser, height = 1000): Promise<P
/** Fail loud on a stale checkout instead of testing yesterday's bundle. */
export function requireDist(): void {
if (!existsSync(DIST_INDEX)) {
throw new Error('web app dist not built — run `pnpm --filter @deepseek-ai/dsh-frontend build` (pnpm run test:web does this first)')
throw new Error('web app dist not built — run `pnpm run build` from the repository root (`pnpm run test:web` does this first)')
}
}

View File

@@ -50,6 +50,7 @@
"tests/permission-policy-context.e2e.ts",
"tests/access-confirmation.e2e.ts",
"tests/shipped-composition.e2e.ts",
"tests/goal-bar.e2e.ts",
"tests/startup-auto-selection.e2e.ts"
],
"references": [