Merge master into docs/post-v3-release-proofreading

This commit is contained in:
xjt
2026-08-12 20:48:08 +08:00
148 changed files with 2028 additions and 408 deletions

View File

@@ -283,6 +283,7 @@ describe('web e2e: agent-preset selection', () => {
expect(snapshot).toContain('Minimal mode')
expect(snapshot).toContain('button "1 subagent"')
expect(snapshot.indexOf('Minimal mode')).toBeLessThan(snapshot.indexOf('button "1 subagent"'))
expect(snapshot.indexOf('button "1 subagent"')).toBeLessThan(snapshot.indexOf('button "Session log"'))
// Static chrome, not a control: the header can only report a composition
// the host would refuse to change.
expect(snapshot).not.toContain('button "Minimal mode"')

View File

@@ -43,6 +43,7 @@ const PLUGINS: readonly (WebBootEntry & { bundlePath: string })[] = [
'@deepseek-ai/dsh-client-ui-sidebar',
],
},
{ id: '@deepseek-ai/dsh-session-export', bundlePath: 'packages/session-query/session-export/lib/client.js', url: '/plugins/session-export.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-command', '@deepseek-ai/dsh-client-ui-conversation'] },
{ id: '@deepseek-ai/dsh-client-ui-trajectory', bundlePath: 'packages/client/ui-trajectory/lib/client.js', url: '/plugins/ui-trajectory.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-conversation'] },
]

View File

@@ -1,7 +1,7 @@
// @vitest-environment jsdom
// Multimodal image surfaces over the BUILT client graph (the code-mode-fixture
// idiom: real bundles via AppWebEntry, keyless FixtureApiClient transport).
// Opens the fixture history session whose turn 72 carries an image in BOTH a
// Opens the fixture history session whose turn 73 carries an image in BOTH a
// user message and an assistant message, and pins the product surfaces: the
// history ImageGallery loading real fixture bytes through the authorized
// sessions.attachment route, the single-click ImageLightbox, and the composer

View File

@@ -0,0 +1,56 @@
// @vitest-environment jsdom
// Assembled max-tokens snapshot: boots the real built `packages/client/*/lib/
// client.js` bundles through AppWebEntry's ModuleLoader path against the
// keyless FixtureApiClient transport, opens the fixture session, and pins the
// surface its max-tokens turn (72) reaches — the turn-end notice row that a
// provider output-cap truncation must render instead of ending silently.
//
// The dot state is pinned beside the copy on purpose: `dot=warning` is what
// distinguishes this notice from the error row, so a regression that routes
// max-tokens through the turn-error presentation changes this file even when
// its own copy still renders.
import { mkdirSync, writeFileSync } from 'node:fs'
import { dirname, join } from 'node:path'
import { fireEvent, screen, waitFor, within } from '@testing-library/react'
import { describe, expect, it } from 'vitest'
import { hasClass, installAssembledBootEnv, mountAssembledApp, REFRESHING_GOLDEN } from './assembled-boot.ts'
const EXPECTED = join(process.cwd(), 'apps/web/tests/snapshots/max-tokens-notice/history-turn.expected.txt')
installAssembledBootEnv()
/** Normalize the notice row to stable fields: its dot state, title, and hint. */
function noticeShape(row: Element): string {
const first = (name: string): string =>
[...row.querySelectorAll('*')].filter(el => hasClass(el, name))[0]?.textContent?.trim() ?? '<absent>'
return [
`dot=${row.querySelector('[data-state]')?.getAttribute('data-state') ?? '<absent>'}`,
`title=${first('maxTokensTitle')}`,
`hint=${first('turnErrorMessage')}`,
].join('\n')
}
describe('assembled max-tokens turn-end notice', () => {
it('renders the localized truncation notice after the cut-off answer instead of ending silently', async () => {
mountAssembledApp()
const tree = await screen.findByRole('tree', { name: 'Sessions' }, { timeout: 10_000 })
fireEvent.click(await within(tree).findByText('Fixture 历史会话'))
// The truncated answer itself stays in the flow: the notice supplements the
// partial output, it never replaces it.
await screen.findByText(/条目 3这一条写到一半被/, undefined, { timeout: 10_000 })
const row = await waitFor(() => {
const found = [...document.querySelectorAll('[role="status"]')]
.find(candidate => [...candidate.querySelectorAll('*')].some(el => hasClass(el, 'maxTokensTitle')))
expect(found).not.toBeUndefined()
return found!
}, { timeout: 10_000 })
const shape = noticeShape(row)
if (REFRESHING_GOLDEN) {
mkdirSync(dirname(EXPECTED), { recursive: true })
writeFileSync(EXPECTED, shape)
}
await expect(shape).toMatchFileSnapshot(EXPECTED)
})
})

View File

@@ -52,6 +52,11 @@ async function assertBaselineSucceeded(response: Response, method: string): Prom
}
async function ensureSeedOpen(page: Page): Promise<void> {
const welcome = page.locator('[class*="onboardingOverlay"]')
if (await welcome.count() > 0) {
await welcome.getByRole('button').click()
await welcome.waitFor({ state: 'detached', timeout: 15_000 })
}
const chat = page.getByRole('tab', { name: 'Chat', exact: true })
// Search is a collapsed header action; expand it so the input is actionable.
const searchButton = page.getByRole('button', { name: 'Search sessions' })
@@ -283,14 +288,30 @@ describe('web e2e: navigation & panes over a rich seeded session', () => {
await details.getByRole('button', { name: 'Close details' }).click()
}, 60_000)
it.skipIf(MODE === 'record')('downloads the session-log ZIP from the trajectory toolbar', async () => {
it.skipIf(MODE === 'record')('downloads through the Session Header and /export with one dialog', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-navigation-export'))
await ensureSeedOpen(page)
await page.getByRole('tab', { name: 'Trajectory' }).click()
const exportButton = page.getByRole('button', { name: 'Session log' })
expect(await exportButton.isDisabled()).toBe(false)
const header = exportButton.locator('xpath=ancestor::header[1]')
const [buttonBox, headerBox] = await Promise.all([
exportButton.boundingBox(), header.boundingBox(),
])
if (buttonBox === null || headerBox === null) {
throw new Error('Session Header export geometry is unavailable')
}
expect(headerBox.x + headerBox.width - (buttonBox.x + buttonBox.width)).toBeLessThanOrEqual(32)
const responsePromise = page.waitForResponse(response =>
response.request().method() === 'HEAD'
&& new URL(response.url()).pathname === '/api/session.export', { timeout: 30_000 })
const downloadPromise = page.waitForEvent('download', { timeout: 30_000 })
await page.getByRole('button', { name: 'Export session log' }).click()
await exportButton.click()
const response = await responsePromise
expect(response.status()).toBe(200)
const download = await downloadPromise
expect(download.suggestedFilename()).toMatch(/^dsh-session-.+\.zip$/)
const dialog = page.getByRole('dialog', { name: 'Session download started' })
await dialog.waitFor({ timeout: 30_000 })
// The real host streamed the ZIP; its root entry is the persisted log
// text verbatim (the assembled seam: real route, real persistence read).
const files = unzipSync(await readFile(await download.path()))
@@ -298,7 +319,63 @@ describe('web e2e: navigation & panes over a rich seeded session', () => {
const content = strFromU8(files['session.jsonl'] as Uint8Array)
expect(content.split('\n')[0]).toContain(SEED_ID)
expect(content).toContain('FIRST_DONE')
}, 60_000)
await dialog.getByText('Close', { exact: true }).click()
const observer = await newEnglishPage(browser)
const observerTripwire = watchConsole(observer)
const observerSlotErrors: string[] = []
let observerDownloads = 0
observer.on('download', () => { observerDownloads += 1 })
observer.on('console', (message) => {
if (message.type() === 'error' && /slot entry crashed/i.test(message.text())) {
observerSlotErrors.push(message.text())
}
})
const observerSessionBaseline = baselineResponse(observer, 'session.list')
const observerWorkspaceBaseline = baselineResponse(observer, 'workspace.list')
const [, observerSessionResponse, observerWorkspaceResponse] = await Promise.all([
observer.goto(scaffold.baseUrl, { waitUntil: 'load' }),
observerSessionBaseline,
observerWorkspaceBaseline,
])
await Promise.all([
assertBaselineSucceeded(observerSessionResponse, 'observer session.list'),
assertBaselineSucceeded(observerWorkspaceResponse, 'observer workspace.list'),
])
await observer.getByText('Ungrouped', { exact: true }).waitFor({ timeout: 30_000 })
await ensureSeedOpen(observer)
try {
const input = page.locator('textarea').first()
const slashDownloadPromise = page.waitForEvent('download', { timeout: 30_000 })
await input.fill('/export')
await page.getByRole('option', { name: /export/u }).waitFor({ timeout: 10_000 })
await input.press('Enter')
const slashDownload = await slashDownloadPromise
expect(slashDownload.suggestedFilename()).toBe(download.suggestedFilename())
const slashFiles = unzipSync(await readFile(await slashDownload.path()))
const slashContent = strFromU8(slashFiles['session.jsonl'] as Uint8Array)
const slashEvents = parseSessionLog(slashContent)
const exportRun = slashEvents.findLast(event => event.type === 'command/run' && event.data.name === 'export')
if (exportRun?.type !== 'command/run') throw new Error('slash ZIP has no export command/run')
const exportDone = slashEvents.find(event =>
event.type === 'command/done' && event.data.commandId === exportRun.data.commandId)
expect(exportDone?.type).toBe('command/done')
await page.getByRole('dialog', { name: 'Session download started' }).waitFor({ timeout: 30_000 })
await page.getByRole('dialog', { name: 'Session download started' })
.getByText('Close', { exact: true }).click()
await observer.getByText('Session log download requested.', { exact: true }).waitFor({ timeout: 30_000 })
expect(observerDownloads).toBe(0)
expect(await observer.getByRole('dialog', { name: 'Session download started' }).count()).toBe(0)
expect({
pageErrors: observerTripwire.pageErrors,
slotErrors: observerSlotErrors,
warnings: observerTripwire.warnings,
}).toEqual({ pageErrors: [], slotErrors: [], warnings: [] })
} finally {
await observer.close()
}
}, 120_000)
it.skipIf(MODE === 'record')('focuses the ledger by dragging an overview interval', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-navigation-timeline'))

View File

@@ -51,7 +51,7 @@ import { dshHomePath } from '@deepseek-ai/dsh-paths'
// } from '@deepseek-ai/dsh-client-ui-settings-general'
export const WELCOME_NOTICE_SETTINGS_NAMESPACE = 'ui-onboarding'
export const WELCOME_NOTICE_ACK_FIELD = 'welcomeNoticeVersion'
export const WELCOME_NOTICE_VERSION = '2026-07-30.7'
export const WELCOME_NOTICE_VERSION = '2026-08-11.1'
export const WELCOME_NOTICE_COPY = { zh: { title: '内测声明', continueLabel: '继续' } } as const
import { settingsNamespace } from '@deepseek-ai/dsh-settings'
@@ -421,7 +421,14 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
// disclosure passes a local dead endpoint instead of disabling the row.
options.telemetryUrl === undefined
? { id: 'telemetry-otel', disabled: true }
: { id: 'telemetry-otel', config: { exporter: { url: options.telemetryUrl }, shutdownTimeoutMillis: 1_000 } },
: {
id: 'telemetry-otel',
config: {
mode: 'FULL',
exporter: { url: options.telemetryUrl },
shutdownTimeoutMillis: 1_000,
},
},
{
id: 'webserver',
config: { host: '127.0.0.1', port: 0 },

View File

@@ -138,13 +138,13 @@ async function detailsTrack(page: Page): Promise<number> {
return Number(cols.split(' ').pop()!.replace('px', ''))
}
// Readiness gate: `dsh web` serves all ten production manifest plugins; until every UI
// Readiness gate: `dsh web` serves every production manifest plugin; until every UI
// plugin's client bundle exists and exports apply, the loader fail-louds and
// the frame never appears.
const UI_PLUGIN_DIRS = [
'connection', 'runtime', 'ui-theme', 'locale', 'ui-layout', 'ui-sidebar',
'ui-settings', 'ui-settings-general', 'ui-models', 'ui-conversation',
'ui-model', 'ui-question', 'ui-trajectory',
'ui-model', 'ui-question', 'ui-trajectory', '../session-query/session-export',
]
const ROUND_DONE_MARKER = 'WEB_ROUND_DONE'
const notReady = UI_PLUGIN_DIRS.filter((dir) => {

View File

@@ -5,3 +5,6 @@
- button "1 subagent":
- text: 1 subagent
- img
- button "Session log":
- text: Session log
- img

View File

@@ -1,6 +1,9 @@
- banner:
- navigation "Session hierarchy":
- 'button "Run two shell commands: wait" [disabled]'
- button "Session log":
- text: Session log
- img
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -3,6 +3,9 @@
- 'button "Using ONE run_code program: run" [disabled]'
- img
- text: Standard mode
- button "Session log":
- text: Session log
- img
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -3,6 +3,9 @@
- button "Use only Cordis tools. First" [disabled]
- img
- text: Standard mode
- button "Session log":
- text: Session log
- img
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -3,6 +3,9 @@
- button "Reply with the single word" [disabled]
- img
- text: Standard mode
- button "Session log":
- text: Session log
- img
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -3,6 +3,9 @@
- button "Use the bash tool to" [disabled]
- img
- text: Standard mode
- button "Session log":
- text: Session log
- img
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -3,6 +3,9 @@
- button "workspace" [disabled]
- img
- text: Standard mode
- button "Session log":
- text: Session log
- img
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -3,6 +3,9 @@
- button "workspace" [disabled]
- img
- text: Standard mode
- button "Session log":
- text: Session log
- img
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -1,6 +1,7 @@
- listbox "Trigger suggestions":
- text: Commands
- option "compact Compact older conversation history" [selected]
- option "export Download this Session log as a ZIP archive"
- option "feedback record feedback about this session"
- option "goal set or view the goal for a long-running task"
- option "permission Switch the permission preset (sandbox mode + approval policy)"

View File

@@ -3,6 +3,9 @@
- button "Reply with the single word" [disabled]
- img
- text: Standard mode
- button "Session log":
- text: Session log
- img
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -3,6 +3,9 @@
- button "Reply with a one-sentence description" [disabled]
- img
- text: Standard mode
- button "Session log":
- text: Session log
- img
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -3,6 +3,9 @@
- button "Reply with a one-sentence description" [disabled]
- img
- text: Standard mode
- button "Session log":
- text: Session log
- img
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -3,6 +3,9 @@
- button "Reply with a one-sentence description" [disabled]
- img
- text: Standard mode
- button "Session log":
- text: Session log
- img
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -3,6 +3,9 @@
- button "Reply with a one-sentence description" [disabled]
- img
- text: Standard mode
- button "Session log":
- text: Session log
- img
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -1,6 +1,9 @@
- banner:
- navigation "Session hierarchy":
- button "CJK strong emphasis" [disabled]
- button "Session log":
- text: Session log
- img
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -1,6 +1,9 @@
- banner:
- navigation "Session hierarchy":
- button "Markdown image policy" [disabled]
- button "Session log":
- text: Session log
- img
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -1,6 +1,9 @@
- banner:
- navigation "Session hierarchy":
- button "Inline code links" [disabled]
- button "Session log":
- text: Session log
- img
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -1,6 +1,9 @@
- banner:
- navigation "Session hierarchy":
- button "Math rendering" [disabled]
- button "Session log":
- text: Session log
- img
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -0,0 +1,3 @@
dot=warning
title=Output token limit reached
hint=The reply was cut off; earlier output is preserved in the conversation. Send "continue" to let the model resume.

View File

@@ -1,6 +1,9 @@
- banner:
- navigation "Session hierarchy":
- button "Use the read tool twice" [disabled]
- button "Session log":
- text: Session log
- img
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -2,7 +2,6 @@
- button "Use actual duration": Duration
- button "Collapse turns": Turns
- button "Collapse calls": Calls
- button "Export session log": Export
- img
- searchbox "Search trajectory"
- region "Trajectory timeline":

View File

@@ -3,6 +3,9 @@
- 'button "Plan a small change: add" [disabled]'
- img
- text: Standard mode
- button "Session log":
- text: Session log
- img
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -3,6 +3,9 @@
- button "Use the ask_user_question tool to" [disabled]
- img
- text: Standard mode
- button "Session log":
- text: Session log
- img
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -3,6 +3,9 @@
- button "Reply with a one-sentence description" [disabled]
- img
- text: Standard mode
- button "Session log":
- text: Session log
- img
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -3,6 +3,9 @@
- button "Reply with a one-sentence description" [disabled]
- img
- text: Standard mode
- button "Session log":
- text: Session log
- img
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -3,6 +3,9 @@
- button "workspace" [disabled]
- img
- text: Standard mode
- button "Session log":
- text: Session log
- img
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -3,6 +3,9 @@
- button "Reply with a one-sentence description" [disabled]
- img
- text: Standard mode
- button "Session log":
- text: Session log
- img
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -3,6 +3,9 @@
- button "Reply with a one-sentence description" [disabled]
- img
- text: Standard mode
- button "Session log":
- text: Session log
- img
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -1,6 +1,9 @@
- banner:
- navigation "Session hierarchy":
- button "Use the read tool twice" [disabled]
- button "Session log":
- text: Session log
- img
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -1,6 +1,9 @@
- banner:
- navigation "Session hierarchy":
- button "Use the read tool twice" [disabled]
- button "Session log":
- text: Session log
- img
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -1,6 +1,9 @@
- banner:
- navigation "Session hierarchy":
- button "Use the read tool twice" [disabled]
- button "Session log":
- text: Session log
- img
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -1,6 +1,9 @@
- banner:
- navigation "Session hierarchy":
- button "Load the snapshot-skill skill with" [disabled]
- button "Session log":
- text: Session log
- img
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -3,6 +3,9 @@
- button "/user-invoke-demo and confirm the fixtur" [disabled]
- img
- text: Standard mode
- button "Session log":
- text: Session log
- img
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -3,6 +3,9 @@
- button "Use the ask_user_question tool to" [disabled]
- img
- text: Standard mode
- button "Session log":
- text: Session log
- img
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -3,6 +3,9 @@
- button "Use the ask_user_question tool to" [disabled]
- img
- text: Standard mode
- button "Session log":
- text: Session log
- img
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -3,6 +3,9 @@
- button "Use the ask_user_question tool to" [disabled]
- img
- text: Standard mode
- button "Session log":
- text: Session log
- img
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -3,6 +3,9 @@
- button "Use the ask_user_question tool to" [disabled]
- img
- text: Standard mode
- button "Session log":
- text: Session log
- img
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -5,6 +5,9 @@
- button "event-sourcing researcher"
- text: /
- button "example editor" [disabled]
- button "Session log":
- text: Session log
- img
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -8,6 +8,9 @@
- button "1 subagent":
- text: 1 subagent
- img
- button "Session log":
- text: Session log
- img
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -5,6 +5,9 @@
- button "event-sourcing researcher" [disabled]
- img
- text: Standard mode
- button "Session log":
- text: Session log
- img
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -3,6 +3,9 @@
- button "Begin your reply with the" [disabled]
- img
- text: Standard mode
- button "Session log":
- text: Session log
- img
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -3,6 +3,9 @@
- button "Begin your reply with the" [disabled]
- img
- text: Standard mode
- button "Session log":
- text: Session log
- img
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -3,6 +3,9 @@
- button "Use web_search to search exactly" [disabled]
- img
- text: Standard mode
- button "Session log":
- text: Session log
- img
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -2,7 +2,7 @@
// Assembled todo snapshot: boots the real built `packages/client/*/lib/
// client.js` bundles through AppWebEntry's ModuleLoader path against the
// keyless FixtureApiClient transport, opens the fixture session, and pins the
// two surfaces the fixture's parallel plan (turn 73, two items `in_progress`)
// two surfaces the fixture's parallel plan (turn 74, two items `in_progress`)
// reaches — the `todo_write` tool row and the dock's plan strip.
//
// The row is pinned as three separate fields on purpose. `summary=` is the