Merge remote-tracking branch 'origin/master' into fix/4775-multimodal-tool-retention

# Conflicts:
#	packages/spill/spill-policy/package.json
This commit is contained in:
creatixchu committed 2026-09-22 12:35:56 +08:00
commit c5ca36387b
392 files changed
+852 -669

No files matched your search

@@ -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 .agents/notes/implemented/architecture/2026-09-14-deepseek-account-login.md
2026-09-14-deepseek-account-login.md: 65405b293cdea14a231d8d700589f9c3e7ef6db7
2026-09-14-deepseek-account-login.zh.md: 0aca011826006a0fb8f0557464187b73da377238
2026-09-14-deepseek-account-login.md: 9d9853617a3b7e288084c93eac4e150f3c180999
2026-09-14-deepseek-account-login.zh.md: d9cdbed25b0ae79caa4c61b310383cc52d8b586a
@@ -46,7 +46,7 @@ Profile and recharge-wallet queries use independent getProfile and getBalance op
Deployment authentication uses explicit Host-only requestHeaders on the configured Platform origin. The provider rejects redirects and reserved-header overrides so deployment cookies cannot replace account authorization or follow a browser destination. Environment-specific authentication protocols remain outside the account provider.
The Host sends login_source (desktop or web) in auth_init so Platform can choose the completion experience. Web failures close the authorization tab while the original tab receives Host state; no Web UI return URL is used. The backend accepts localhost callbacks. DSH preserves the browser-supplied localhost hostname and port without DNS resolution or conversion to an IP literal.
The bundled account UI is Desktop-only: its preload bridge enables the account launcher, settings and sign-in onboarding. Plain Web retains API-key onboarding and the standard Settings launcher without account subscription or login. The Host protocol accepts login_source (desktop or web) in auth_init; the bundled UI sends desktop. The backend accepts localhost callbacks. DSH preserves the browser-supplied localhost hostname and port without DNS resolution or conversion to an IP literal.
The macOS development launcher registers an isolated, ad-hoc-signed application bundle for `dsh://open`. The bundle retains the workspace entry and development paths for Launch Services cold starts without copying credentials or changing the package-manager-owned Electron application. Protocol registration targets the latest launched development or packaged application.
@@ -46,7 +46,7 @@ DSH 授权通过 x-dsh-auth-token 请求头鉴权 Platform、推理和 Files 请
开发环境认证通过配置的平台来源上的显式 Host 请求头 requestHeaders 完成。提供者拒绝重定向和保留请求头覆盖,防止开发环境 Cookie 替换账号授权或跟随浏览器跳转地址。特定环境的认证协议不属于账号提供者。
Host 在 auth_init 中发送 login_source(desktop 或 web),供 Platform 选择完成页交互。Web 失败时关闭授权标签页,原标签页接收 Host 状态,不使用 Web UI 返回地址。后端接受 localhost 回调。DSH 保留浏览器提供的 localhost 主机名和端口,不做 DNS 解析或 IP 字面量转换。
内置账号界面仅用于 Desktop:preload 桥接启用账号入口、设置和登录引导。普通 Web 保留 API Key 引导及标准设置入口,不订阅账号状态或显示登录。Host 协议在 auth_init 中接受 login_source(desktop 或 web);内置界面发送 desktop。后端接受 localhost 回调。DSH 保留浏览器提供的 localhost 主机名和端口,不做 DNS 解析或 IP 字面量转换。
macOS 开发启动器为 `dsh://open` 注册独立、经临时签名的应用包。该应用包保留工作区入口和开发路径,使 Launch Services 能够冷启动;凭证不会被复制,也不修改包管理器安装的 Electron 应用。协议注册指向最近启动的开发版或打包版应用。
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh",
"description": "dsh CLI: profile launch, plugin management, and configuration inspection",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-desktop-host",
"description": "Private Node-mode host process for the Electron desktop application",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"private": true,
"license": "MIT",
"type": "module",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-desktop",
"description": "Electron desktop shell for a bundled dsh runtime and external plugins",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"private": true,
"license": "MIT",
"type": "module",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-web-frontend",
"description": "Web application entry: vite build over the @deepseek-ai/dsh-client-web shell library; dist/ served by apps/cli's dsh web",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"publishConfig": {
"access": "public"
},
+3 -3
View File
@@ -5,7 +5,7 @@ import type { Browser, Page } from 'playwright'
import { chromium } from 'playwright'
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
import { captureStableAria, compareOrRefreshGolden, launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold } from './scaffold.ts'
import { openSettingsFromAccountMenu, ZH_BROWSER_LOCALE, connectFreshWorkspaceZh, saveFailureShot } from './support.ts'
import { openSettings, ZH_BROWSER_LOCALE, connectFreshWorkspaceZh, saveFailureShot } from './support.ts'
const EXPECTED = fileURLToPath(new URL('./expected/agent-preset-authoring', import.meta.url))
const mode = webSnapshotMode()
@@ -21,7 +21,7 @@ describe('web e2e: preset roster guidance', () => {
page = await browser.newPage({ viewport: { width: 1680, height: 1000 }, locale: ZH_BROWSER_LOCALE })
tripwire = watchConsole(page)
await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
await openSettingsFromAccountMenu(page, 'zh')
await openSettings(page, 'zh')
await page.getByRole('dialog', { name: '设置' }).getByRole('button', { name: 'Agent 预设' }).click()
await page.getByRole('heading', { name: 'Agent 预设' }).waitFor()
}, 120_000)
@@ -61,7 +61,7 @@ describe('web e2e: preset roster guidance', () => {
// so the flow needs a connected workspace to enter.
await page.getByRole('dialog', { name: '设置' }).getByRole('button', { name: '关闭', exact: true }).last().click()
await connectFreshWorkspaceZh(page, scaffold.workspaceCwd)
await openSettingsFromAccountMenu(page, 'zh')
await openSettings(page, 'zh')
const settings = page.getByRole('dialog', { name: '设置' })
await settings.getByRole('button', { name: 'Agent 预设' }).click()
await settings.getByRole('button', { name: '让 Agent 帮我创建预设模式', exact: true }).click()
+4 -4
View File
@@ -24,7 +24,7 @@ import {
captureStableAria, compareOrRefreshGolden, launchWebScaffold, seedSession, watchConsole,
webSnapshotMode, type WebScaffold,
} from './scaffold.ts'
import { openSettingsFromAccountMenu,
import { openSettings,
connectFreshWorkspace, newEnglishPage, saveFailureShot, writeComposerDraft,
} from './support.ts'
@@ -254,7 +254,7 @@ describe('web e2e: agent-preset selection', () => {
await connectFreshWorkspace(page, scaffold.workspaceCwd)
await page.getByRole('button', { name: 'Standard mode', exact: true }).waitFor({ timeout: 10_000 })
await openSettingsFromAccountMenu(page, 'en')
await openSettings(page, 'en')
const dialog = page.getByRole('dialog', { name: 'Settings' })
await dialog.getByRole('button', { name: 'Agent presets' }).click()
const toggle = dialog.getByRole('switch', { name: 'Choose a mode for new tasks' })
@@ -347,7 +347,7 @@ describe('web e2e: agent-preset selection', () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-agent-preset-disabled'))
await expect.poll(() => livePreset(scaffold), { timeout: 15_000 }).toBe('standard')
await openSettingsFromAccountMenu(page, 'en')
await openSettings(page, 'en')
const dialog = page.getByRole('dialog', { name: 'Settings' })
await dialog.getByRole('button', { name: 'Agent presets' }).click()
await dialog.getByRole('button', { name: 'Set as new task default: Minimal mode' }).click()
@@ -364,7 +364,7 @@ describe('web e2e: agent-preset selection', () => {
// The switch controls availability only: re-enabling restores the saved
// default and aligns this same still-blank task with it.
await openSettingsFromAccountMenu(page, 'en')
await openSettings(page, 'en')
const reopened = page.getByRole('dialog', { name: 'Settings' })
await reopened.getByRole('button', { name: 'Agent presets' }).click()
const reopenedToggle = reopened.getByRole('switch', { name: 'Choose a mode for new tasks' })
+1 -1
View File
@@ -158,7 +158,7 @@ describe('web e2e: Agent Teams panel', () => {
expect(await trigger.evaluate(element => element === document.activeElement)).toBe(true)
await page.keyboard.press('Enter')
await panel.waitFor()
const outside = page.getByRole('button', { name: 'Account menu', exact: true })
const outside = page.getByRole('button', { name: 'Settings', exact: true })
await outside.focus()
await panel.waitFor({ state: 'detached' })
expect(await outside.evaluate(element => element === document.activeElement)).toBe(true)
+2 -2
View File
@@ -14,7 +14,7 @@ import {
fixtureUserPrompts, launchWebScaffold, recordFixture, watchConsole,
webSnapshotMode, type WebScaffold,
} from './scaffold.ts'
import { openSettingsFromAccountMenu, connectFreshWorkspaceZh, ZH_BROWSER_LOCALE } from './support.ts'
import { openSettings, connectFreshWorkspaceZh, ZH_BROWSER_LOCALE } from './support.ts'
const DIR = fileURLToPath(new URL('../../../snapshots/web/changed-files-turn', import.meta.url))
const FIXTURE = join(DIR, 'session.v3.jsonl')
@@ -136,7 +136,7 @@ describe('web e2e: a git workspace turn ends with its changed files', () => {
expect(await header.getByRole('tablist').count()).toBe(0)
const compactHeight = await header.evaluate(element => element.getBoundingClientRect().height)
expect(compactHeight).toBeLessThan(60)
await openSettingsFromAccountMenu(page, 'zh')
await openSettings(page, 'zh')
const settings = page.getByRole('dialog', { name: '设置' })
await settings.getByRole('switch', { name: '开发者工具' }).click()
await expect.poll(() => settings.getByRole('switch', { name: '开发者工具' }).getAttribute('aria-checked')).toBe('true')
@@ -35,7 +35,7 @@ import {
webSnapshotMode,
type WebScaffold,
} from './scaffold.ts'
import { openSettingsFromAccountMenu, expandTurnProcesses, newEnglishPage, saveFailureShot } from './support.ts'
import { openSettings, expandTurnProcesses, newEnglishPage, saveFailureShot } from './support.ts'
const SNAPSHOT_DIR = fileURLToPath(new URL('./expected/clickable-links-gallery', import.meta.url))
const UI_EXPECTED = fileURLToPath(new URL('./expected/clickable-links-gallery/ui.expected.md', import.meta.url))
@@ -453,7 +453,7 @@ describe('web e2e: clickable links gallery', () => {
await markdown.locator(`a[href="${HTTP_URL}"]`).click()
await expect.poll(() => browserAddress.inputValue()).toBe(HTTP_URL)
await openSettingsFromAccountMenu(page, 'en')
await openSettings(page, 'en')
await page.getByRole('button', { name: 'Built-in browser', exact: true }).click()
await page.getByRole('menuitem', { name: 'New browser tab', exact: true }).click()
await expect.poll(() => scaffold.ctx.settings.describe().find(row => row.ns === 'ui-chat')?.value).toMatchObject({ linkOpening: 'new-tab' })
@@ -470,7 +470,7 @@ describe('web e2e: clickable links gallery', () => {
}
await page.reload()
await openSettingsFromAccountMenu(page, 'en')
await openSettings(page, 'en')
await page.getByRole('button', { name: 'New browser tab', exact: true }).click()
await page.getByRole('menuitem', { name: 'Built-in browser', exact: true }).click()
await expect.poll(() => scaffold.ctx.settings.describe().find(row => row.ns === 'ui-chat')?.value).toMatchObject({ linkOpening: 'sidebar' })
+2 -2
View File
@@ -9,7 +9,7 @@ import {
assertFixtureInventory, captureStableAria, compareOrRefreshGolden, launchWebScaffold,
seedSession, watchConsole, webSnapshotMode,
} from './scaffold.ts'
import { openSettingsFromAccountMenu, newEnglishPage, saveFailureShot, ZH_BROWSER_LOCALE } from './support.ts'
import { openSettings, newEnglishPage, saveFailureShot, ZH_BROWSER_LOCALE } from './support.ts'
const FIXTURE = fileURLToPath(new URL('./fixtures/plugins/fixture-live-client', import.meta.url))
const EXPECTED = fileURLToPath(new URL('./expected/client-plugin-live', import.meta.url))
@@ -19,7 +19,7 @@ const SESSION_TITLE = 'Session action extension'
async function openInventory(page: Page, url: string) {
await page.goto(url, { waitUntil: 'load' })
await openSettingsFromAccountMenu(page, 'zh')
await openSettings(page, 'zh')
const dialog = page.getByRole('dialog', { name: '设置' })
await dialog.getByRole('button', { name: '内置插件', exact: true }).click()
await dialog.getByRole('searchbox', { name: '搜索插件' }).waitFor()
@@ -8,7 +8,7 @@ import {
captureStableAria, compareOrRefreshGolden, launchWebScaffold,
watchConsole, webSnapshotMode, type WebScaffold,
} from './scaffold.ts'
import { openSettingsFromAccountMenu, connectFreshWorkspaceZh, saveFailureShot, ZH_BROWSER_LOCALE } from './support.ts'
import { openSettings, connectFreshWorkspaceZh, saveFailureShot, ZH_BROWSER_LOCALE } from './support.ts'
const EXPECTED = fileURLToPath(new URL('./expected/deepseek-messages-settings/', import.meta.url))
@@ -39,13 +39,11 @@ describe.skipIf(webSnapshotMode() === 'record')('web e2e: DeepSeek Messages sett
expect(scaffold.ctx.llm.listProviders()).toContainEqual({ id: 'deepseek-official', name: 'DeepSeek' })
expect(scaffold.ctx.llm.listProviders().filter(provider => provider.id === 'deepseek-official')).toHaveLength(1)
expect(scaffold.ctx.agentDefaultModel.currentSelection()).toEqual({ provider: 'deepseek-official', model: 'deepseek-flash' })
await page.getByRole('dialog', { name: '开始你的创作' })
.getByRole('button', { name: '添加 API Key', exact: true }).click()
const onboarding = page.getByRole('dialog', { name: '添加一个 API Key 开始使用' })
await onboarding.getByLabel('API 密钥', { exact: true }).fill('sk-messages-onboarding')
await onboarding.getByRole('button', { name: '保存并继续' }).click()
await onboarding.waitFor({ state: 'detached' })
await openSettingsFromAccountMenu(page, 'zh')
await openSettings(page, 'zh')
const dialog = page.getByRole('dialog', { name: '设置', exact: true })
await dialog.getByRole('button', { name: '模型', exact: true }).click()
await dialog.getByText('DeepSeek', { exact: true }).waitFor()
+3 -3
View File
@@ -8,7 +8,7 @@ import {
acknowledgeReloadConnectionLoss, assertFixtureInventory, captureStableAria, compareOrRefreshGolden,
launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold,
} from './scaffold.ts'
import { openSettingsFromAccountMenu, saveFailureShot } from './support.ts'
import { openSettings, saveFailureShot } from './support.ts'
const SNAPSHOT_DIR = fileURLToPath(new URL('./expected/desktop-locale', import.meta.url))
const MODE = webSnapshotMode()
@@ -53,7 +53,7 @@ describe.skipIf(MODE === 'record')('web e2e: native and Client locale preference
it('uses OS languages without saving them, then shares an explicit choice across reloads', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-desktop-locale'))
await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
await openSettingsFromAccountMenu(page, 'zh')
await openSettings(page, 'zh')
const zhDialog = page.getByRole('dialog', { name: '设置', exact: true })
await zhDialog.getByRole('button', { name: '中文', exact: true }).waitFor()
await expect.poll(() => reported).toEqual(['zh'])
@@ -73,7 +73,7 @@ describe.skipIf(MODE === 'record')('web e2e: native and Client locale preference
reported.length = 0
await page.reload({ waitUntil: 'load' })
acknowledgeReloadConnectionLoss(tripwire, warningsBefore)
await openSettingsFromAccountMenu(page, 'en')
await openSettings(page, 'en')
await enDialog.getByRole('button', { name: 'English', exact: true }).waitFor()
await expect.poll(() => reported).toEqual(['en'])
expect(await page.evaluate(() => document.documentElement.lang)).toBe('en')
@@ -4,7 +4,7 @@ import { join } from 'node:path'
import { chromium } from 'playwright'
import { expect, it, onTestFinished } from 'vitest'
import { launchWebScaffold } from './scaffold.ts'
import { openSettingsFromAccountMenu, newEnglishPage } from './support.ts'
import { openSettings, newEnglishPage } from './support.ts'
it('persists developer tools in the Host settings document and restores the accepted choice', async () => {
const scaffold = await launchWebScaffold()
@@ -13,7 +13,7 @@ it('persists developer tools in the Host settings document and restores the acce
onTestFinished(() => browser.close())
const page = await newEnglishPage(browser)
await page.goto(scaffold.authenticatedUrl)
await openSettingsFromAccountMenu(page, 'en')
await openSettings(page, 'en')
const toggle = page.getByRole('switch', { name: 'Developer tools' })
expect(await toggle.getAttribute('aria-checked')).toBe('true')
expect(await readFile(join(scaffold.harnessHome, 'profiles', 'scaffold', 'cordis.patch.yml'), 'utf8')).not.toMatch(/id: ui-settings(?:\r?\n|$)/)
@@ -21,7 +21,7 @@ it('persists developer tools in the Host settings document and restores the acce
await expect.poll(() => toggle.getAttribute('aria-checked')).toBe('false')
expect(await readFile(join(scaffold.harnessHome, 'profiles', 'scaffold', 'cordis.patch.yml'), 'utf8')).toContain('enabled: false')
await page.reload()
await openSettingsFromAccountMenu(page, 'en')
await openSettings(page, 'en')
await expect.poll(() => toggle.getAttribute('aria-checked')).toBe('false')
await page.getByRole('button', { name: 'Agent presets', exact: true }).click()
+2 -2
View File
@@ -14,7 +14,7 @@ import { excelDrawingFixture } from '../../../packages/client/ui-sidebar-documen
import { xlsFixture } from '../../../packages/client/ui-sidebar-documentpreview/tests/xls-fixture.ts'
import { pdfFixture, selectionPdfFixture } from '../../../packages/client/ui-sidebar-documentpreview/tests/pdf-fixture.ts'
import { assertFixtureInventory, compareOrRefreshGolden, launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold } from './scaffold.ts'
import { openSettingsFromAccountMenu, connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
import { openSettings, connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
const FIXTURE = fileURLToPath(new URL('../../../snapshots/web/lifecycle-chrome/session.v3.jsonl', import.meta.url))
const SNAPSHOT_DIR = fileURLToPath(new URL('../../../snapshots/web/document-preview', import.meta.url))
@@ -425,7 +425,7 @@ else process.exit(1);
`- Local script: ${await basicHtml.locator('#local-result').innerText()}`,
`- Network requests: ${previewNetworkRequests}`,
].join('\n'))
await openSettingsFromAccountMenu(page, 'en')
await openSettings(page, 'en')
const settings = page.getByRole('dialog', { name: 'Settings' })
await settings.getByRole('switch', { name: 'Developer tools' }).click()
await expect.poll(() => settings.getByRole('switch', { name: 'Developer tools' }).getAttribute('aria-checked')).toBe('true')
@@ -1,6 +0,0 @@
- dialog "开始你的创作":
- heading "开始你的创作" [level=2]
- button "关闭"
- paragraph: 登录后即可创建、编辑和分享你的设计项目,所有内容在本地保存。
- button "添加 API Key"
- button "登录"
@@ -0,0 +1,4 @@
- alert:
- paragraph: "Cannot download model.int8.onnx: the download service returned HTTP 503."
- paragraph: Confirm that the download address is available and the proxy can reach the download service, or retry later.
- text: "Download source: https://model-mirror.example"
+3 -3
View File
@@ -14,7 +14,7 @@ import {
acknowledgeReloadConnectionLoss, assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
launchWebScaffold, parseSeedFixture, renderSeedFixture, seedSession, watchConsole, webSnapshotMode, type WebScaffold,
} from './scaffold.ts'
import { openSettingsFromAccountMenu, newEnglishPage, saveFailureShot } from './support.ts'
import { openSettings, newEnglishPage, saveFailureShot } from './support.ts'
const SNAPSHOT_DIR = fileURLToPath(new URL('../../../snapshots/web/message-actions', import.meta.url))
// Borrowed read-only: this scenario needs any settled user+assistant pair, not
@@ -321,7 +321,7 @@ describe('web e2e: message IconActions and clocks on settled history', () => {
it.skipIf(MODE === 'record')('persists performance detail and hides statistics in Compact', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-performance-usage'))
const stats = page.locator('[data-composer-stats]')
await openSettingsFromAccountMenu(page, 'en')
await openSettings(page, 'en')
const dialog = page.getByRole('dialog', { name: 'Settings', exact: true })
const row = dialog.getByText('Performance & usage', { exact: true }).locator('../..')
await row.getByRole('button', { name: 'Detailed', exact: true }).click()
@@ -336,7 +336,7 @@ describe('web e2e: message IconActions and clocks on settled history', () => {
await compareOrRefreshGolden(join(SNAPSHOT_DIR, 'compact.expected.md'), await captureStableAria(page, '[data-composer-stats]', scaffold.workspaceCwd), MODE)
const warningStart = tripwire.warnings.length
await page.reload()
await openSettingsFromAccountMenu(page, 'en')
await openSettings(page, 'en')
await row.getByRole('button', { name: 'Compact', exact: true }).waitFor()
acknowledgeReloadConnectionLoss(tripwire, warningStart)
await row.getByRole('button', { name: 'Compact', exact: true }).click()
@@ -9,7 +9,7 @@ import {
captureStableAria, compareOrRefreshGolden, launchWebScaffold,
watchConsole, webSnapshotMode, type WebScaffold,
} from './scaffold.ts'
import { openSettingsFromAccountMenu, saveFailureShot, ZH_BROWSER_LOCALE } from './support.ts'
import { openSettings, saveFailureShot, ZH_BROWSER_LOCALE } from './support.ts'
const EXPECTED = fileURLToPath(new URL('./expected/models-settings-recovery/stored-error.expected.md', import.meta.url))
const FAILURE = 'llm-pi-ai: provider "openrouter" model "111" needs an api; '
@@ -37,7 +37,7 @@ describe('web e2e: repairs a stored provider after catalog drift', () => {
page = await browser.newPage({ viewport: { width: 1680, height: 1000 }, locale: ZH_BROWSER_LOCALE })
tripwire = watchConsole(page)
await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
await openSettingsFromAccountMenu(page, 'zh')
await openSettings(page, 'zh')
const dialog = page.getByRole('dialog', { name: '设置' })
await dialog.getByRole('button', { name: '模型', exact: true }).click()
await dialog.getByText(FAILURE, { exact: true }).waitFor()
+2 -2
View File
@@ -25,7 +25,7 @@ import {
assertFixtureInventory, captureStableAria, compareOrRefreshGolden,
launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold,
} from './scaffold.ts'
import { openSettingsFromAccountMenu, ZH_BROWSER_LOCALE, saveFailureShot } from './support.ts'
import { openSettings, ZH_BROWSER_LOCALE, saveFailureShot } from './support.ts'
const SNAPSHOT_DIR = fileURLToPath(new URL('./expected/models-settings', import.meta.url))
const EMPTY_EXPECTED = join(SNAPSHOT_DIR, 'empty.expected.md')
@@ -72,7 +72,7 @@ describe('web e2e: Models settings page configures a dormant provider', () => {
it('opens the add card over the dormant directory vocabulary', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-models-empty'))
await openSettingsFromAccountMenu(page, 'zh')
await openSettings(page, 'zh')
const dialog = page.getByRole('dialog', { name: '设置' })
await dialog.waitFor({ timeout: 10_000 })
await dialog.getByRole('button', { name: '模型', exact: true }).click()
@@ -16,11 +16,10 @@ import {
WELCOME_NOTICE_ACK_FIELD, WELCOME_NOTICE_COPY, WELCOME_NOTICE_SETTINGS_NAMESPACE,
WELCOME_NOTICE_VERSION,
} from './scaffold.ts'
import { openSettingsFromAccountMenu, ZH_BROWSER_LOCALE, connectFreshWorkspaceZh, saveFailureShot } from './support.ts'
import { openSettings, ZH_BROWSER_LOCALE, connectFreshWorkspaceZh, saveFailureShot } from './support.ts'
const SNAPSHOT_DIR = fileURLToPath(new URL('./expected/onboarding-deepseek-config', import.meta.url))
const WELCOME_EXPECTED = join(SNAPSHOT_DIR, 'welcome.expected.md')
const SIGN_IN_EXPECTED = join(SNAPSHOT_DIR, 'sign-in.expected.md')
const MISSING_EXPECTED = join(SNAPSHOT_DIR, 'missing.expected.md')
const MODELS_EXPECTED = join(SNAPSHOT_DIR, 'models.expected.md')
const DEFAULT_MODELS_EXPECTED = join(SNAPSHOT_DIR, 'default-models.expected.md')
@@ -73,13 +72,10 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
await welcome.getByRole('button', { name: WELCOME_NOTICE_COPY.zh.continueLabel }).click()
await welcome.waitFor({ state: 'detached', timeout: 15_000 })
const signIn = page.getByRole('dialog', { name: '开始你的创作' })
await signIn.waitFor({ timeout: 15_000 })
await compareOrRefreshGolden(SIGN_IN_EXPECTED,
await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd), MODE)
await signIn.getByRole('button', { name: '添加 API Key', exact: true }).click()
const credentialStep = page.getByRole('dialog', { name: '添加一个 API Key 开始使用' })
await credentialStep.waitFor({ timeout: 15_000 })
expect(await page.getByRole('dialog', { name: '开始你的创作' }).count()).toBe(0)
expect(await page.getByRole('button', { name: '账号菜单', exact: true }).count()).toBe(0)
const keyInput = credentialStep.getByLabel('API 密钥', { exact: true })
await keyInput.waitFor({ timeout: 10_000 })
const initial = await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd)
@@ -102,7 +98,7 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
// The ordinary Models surface reuses the refreshed join and exposes the
// configured write-only placeholder without a reload.
await openSettingsFromAccountMenu(page, 'zh')
await openSettings(page, 'zh')
const settings = page.getByRole('dialog', { name: '设置' })
await settings.waitFor({ timeout: 10_000 })
await settings.getByRole('button', { name: '模型', exact: true }).click()
@@ -203,7 +199,7 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
onTestFailed(() => saveFailureShot(page, 'web-e2e-onboarding-deepseek-models'))
// Opened here rather than inherited: the credential test reloads the page
// after configuring the key, so nothing carries an open dialog across.
await openSettingsFromAccountMenu(page, 'zh')
await openSettings(page, 'zh')
const settings = page.getByRole('dialog', { name: '设置' })
await settings.waitFor({ timeout: 10_000 })
await settings.getByRole('button', { name: '模型', exact: true }).click()
@@ -292,7 +288,7 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
it('keeps the fixture inventory closed', async () => {
await assertFixtureInventory(
SNAPSHOT_DIR,
['welcome.expected.md', 'sign-in.expected.md', 'missing.expected.md', 'models.expected.md', 'default-models.expected.md'],
['welcome.expected.md', 'missing.expected.md', 'models.expected.md', 'default-models.expected.md'],
)
})
})
+9 -2
View File
@@ -8,7 +8,7 @@ import {
acknowledgeReloadConnectionLoss, assertFixtureInventory, captureStableAria, compareOrRefreshGolden,
launchWebScaffold, watchConsole, webSnapshotMode, WELCOME_NOTICE_COPY, type WebScaffold,
} from './scaffold.ts'
import { openSettingsFromAccountMenu, ZH_BROWSER_LOCALE, saveFailureShot } from './support.ts'
import { openSettings, ZH_BROWSER_LOCALE, saveFailureShot } from './support.ts'
const SNAPSHOT_DIR = fileURLToPath(new URL('./expected/onboarding-native', import.meta.url))
const MODE = webSnapshotMode()
@@ -52,7 +52,14 @@ describe.skipIf(MODE === 'record').each([false, true])('web e2e: native credenti
await page.reload({ waitUntil: 'load' })
acknowledgeReloadConnectionLoss(tripwire, warningsBefore)
}
await openSettingsFromAccountMenu(page, 'zh')
const accountMenu = page.getByRole('button', { name: '账号菜单', exact: true })
if (desktop) await accountMenu.waitFor()
else {
await page.getByRole('button', { name: '设置', exact: true }).waitFor()
expect(await accountMenu.count()).toBe(0)
expect(await page.getByRole('dialog', { name: '开始你的创作' }).count()).toBe(0)
}
await openSettings(page, 'zh')
const settings = page.getByRole('dialog', { name: '设置', exact: true })
await settings.getByRole('button', { name: '模型', exact: true }).click()
await settings.getByLabel('API 密钥', { exact: true }).waitFor()
@@ -14,7 +14,7 @@ import {
acknowledgeReloadConnectionLoss, assertFixtureInventory, captureStableAria, compareOrRefreshGolden,
launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold,
} from './scaffold.ts'
import { openSettingsFromAccountMenu, ZH_BROWSER_LOCALE, saveFailureShot } from './support.ts'
import { openSettings, ZH_BROWSER_LOCALE, saveFailureShot } from './support.ts'
const SNAPSHOT_DIR = fileURLToPath(new URL('./expected/onboarding-usable-provider', import.meta.url))
const DISMISSED_EXPECTED = join(SNAPSHOT_DIR, 'dismissed.expected.md')
@@ -44,15 +44,14 @@ describe.skipIf(MODE === 'record')('web e2e: another usable provider ends first-
it('closes the setup card without discarding the add card beside it', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-onboarding-setup-card-cancel'))
const signIn = page.getByRole('dialog', { name: '开始你的创作' })
await signIn.waitFor({ timeout: 15_000 })
await signIn.getByRole('button', { name: '添加 API Key', exact: true }).click()
const credentialStep = page.getByRole('dialog', { name: CREDENTIAL_STEP })
await credentialStep.waitFor({ timeout: 15_000 })
expect(await page.getByRole('dialog', { name: '开始你的创作' }).count()).toBe(0)
expect(await page.getByRole('button', { name: '账号菜单', exact: true }).count()).toBe(0)
await credentialStep.getByRole('button', { name: '稍后配置' }).click()
await credentialStep.waitFor({ state: 'detached', timeout: 15_000 })
await openSettingsFromAccountMenu(page, 'zh')
await openSettings(page, 'zh')
const settings = page.getByRole('dialog', { name: '设置' })
await settings.waitFor({ timeout: 10_000 })
// Dismissing the onboarding step leaves Settings closed, so enter the
@@ -116,7 +115,7 @@ describe.skipIf(MODE === 'record')('web e2e: another usable provider ends first-
// The Models page agrees: DeepSeek stays a row rather than reopening its
// setup card over a user who already has somewhere to send a request.
await openSettingsFromAccountMenu(page, 'zh')
await openSettings(page, 'zh')
await settings.waitFor({ timeout: 10_000 })
await settings.getByRole('button', { name: '模型', exact: true }).click()
await settings.getByRole('button', { name: '编辑 DeepSeek (deepseek-official)' }).waitFor({ timeout: 10_000 })
+2 -2
View File
@@ -14,7 +14,7 @@ import {
SCAFFOLD_DEFAULTS_BUNDLE, assertFixtureInventory, captureStableAria, compareOrRefreshGolden,
launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold,
} from './scaffold.ts'
import { ZH_BROWSER_LOCALE, connectFreshWorkspaceZh, openSettingsFromAccountMenu, saveFailureShot } from './support.ts'
import { ZH_BROWSER_LOCALE, connectFreshWorkspaceZh, openSettings, saveFailureShot } from './support.ts'
const SNAPSHOT_DIR = fileURLToPath(new URL('./expected/plugin-manager', import.meta.url))
const MANAGER_EXPECTED = join(SNAPSHOT_DIR, 'manager.expected.md')
@@ -64,7 +64,7 @@ describe('web e2e: plugin manager', () => {
const source = language === 'en' ? '中文' : 'English'
const target = language === 'en' ? 'English' : '中文'
if (await page.getByRole('dialog', { name: settings }).count() === 0) {
await openSettingsFromAccountMenu(page, language === 'en' ? 'zh' : 'en')
await openSettings(page, language === 'en' ? 'zh' : 'en')
}
await page.getByRole('dialog', { name: settings }).getByRole('button', { name: source }).click()
await page.getByRole('menuitem', { name: target }).click()
-1
View File
@@ -316,7 +316,6 @@ async function bootPreview(origin: string, browser: Browser): Promise<void> {
const continueButton = page.getByRole('button', { name: 'Continue' })
await continueButton.waitFor({ timeout: HERO_TIMEOUT_MS })
await continueButton.click()
await page.getByRole('button', { name: 'Add API Key', exact: true }).click()
const configureLater = page.getByRole('button', { name: 'Configure later' })
await configureLater.waitFor({ timeout: 30_000 })
await configureLater.click()
+25 -27
View File
@@ -19,7 +19,7 @@ import {
acknowledgeReloadConnectionLoss, assertFixtureInventory, captureStableAria, compareOrRefreshGolden,
launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold,
} from './scaffold.ts'
import { openSettingsFromAccountMenu, ZH_BROWSER_LOCALE, saveFailureShot } from './support.ts'
import { openSettings, ZH_BROWSER_LOCALE, saveFailureShot } from './support.ts'
const SNAPSHOT_DIR = fileURLToPath(new URL('./expected/settings-chrome', import.meta.url))
const DIALOG_EXPECTED = join(SNAPSHOT_DIR, 'dialog.expected.md')
@@ -59,15 +59,13 @@ describe('web e2e: settings modal and General preferences', () => {
it('opens the settings dialog, switches sections, and closes by every path', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-settings-shell'))
const trigger = page.getByRole('button', { name: '账号菜单', exact: true })
expect(await trigger.getAttribute('aria-haspopup')).toBe('menu')
const trigger = page.getByRole('button', { name: '设置', exact: true })
expect(await trigger.getAttribute('aria-haspopup')).toBe('dialog')
expect(await trigger.getAttribute('aria-expanded')).toBe('false')
await trigger.click()
expect(await trigger.getAttribute('aria-expanded')).toBe('true')
await page.getByRole('menuitem', { name: '设置', exact: true }).click()
const dialog = page.getByRole('dialog', { name: '设置' })
await dialog.waitFor({ timeout: 10_000 })
expect(await trigger.getAttribute('aria-expanded')).toBe('false')
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: '工作区内修改' }).waitFor({ timeout: 10_000 })
@@ -186,7 +184,7 @@ describe('web e2e: settings modal and General preferences', () => {
await expect.poll(() => page.getByRole('dialog', { name: '设置' }).count(), { timeout: 5_000 }).toBe(0)
expect(await trigger.getAttribute('aria-expanded')).toBe('false')
// Close path 2: the header close button (focus lands there on open).
await openSettingsFromAccountMenu(page, 'zh')
await openSettings(page, 'zh')
await page.getByRole('dialog', { name: '设置' }).getByRole('button', { name: '关闭' }).click()
await expect.poll(() => page.getByRole('dialog', { name: '设置' }).count(), { timeout: 5_000 }).toBe(0)
expect(tripwire.pageErrors).toEqual([])
@@ -198,7 +196,7 @@ describe('web e2e: settings modal and General preferences', () => {
onTestFailed(() => saveFailureShot(overflowPage, 'web-e2e-settings-navigation'))
const console = watchConsole(overflowPage)
await overflowPage.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
await openSettingsFromAccountMenu(overflowPage, 'zh')
await openSettings(overflowPage, 'zh')
const dialog = overflowPage.getByRole('dialog', { name: '设置', exact: true })
const navigation = dialog.getByRole('navigation')
const list = navigation.locator('[class*="_navList"]')
@@ -242,7 +240,7 @@ describe('web e2e: settings modal and General preferences', () => {
expect(existing.snapshotEvents().find(event => event.type === 'permission/preset')?.data)
.toEqual({ preset: 'workspace-write' })
await openSettingsFromAccountMenu(page, 'zh')
await openSettings(page, 'zh')
const dialog = page.getByRole('dialog', { name: '设置' })
await dialog.waitFor({ timeout: 10_000 })
const selector = dialog.getByRole('button', { name: '工作区内修改' })
@@ -309,7 +307,7 @@ describe('web e2e: settings modal and General preferences', () => {
it('uses the persisted dark preference while plugins are still loading', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-settings-boot-theme'))
await page.emulateMedia({ colorScheme: 'light' })
await openSettingsFromAccountMenu(page, 'zh')
await openSettings(page, 'zh')
const initialDialog = page.getByRole('dialog', { name: '设置' })
const darkCube = initialDialog.getByRole('button', { name: '深色' })
await selectTheme(darkCube, 'dark')
@@ -355,7 +353,7 @@ describe('web e2e: settings modal and General preferences', () => {
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
acknowledgeReloadConnectionLoss(tripwire, warningStart)
await openSettingsFromAccountMenu(page, 'zh')
await openSettings(page, 'zh')
const restoredDialog = page.getByRole('dialog', { name: '设置' })
const systemCube = restoredDialog.getByRole('button', { name: '跟随系统' })
// The boot palette precedes the settings mirror's saved preference.
@@ -409,7 +407,7 @@ describe('web e2e: settings modal and General preferences', () => {
expect(light.faviconPaths).toEqual(['/favicon.svg'])
expectThemeColorSynchronized(light)
await openSettingsFromAccountMenu(page, 'zh')
await openSettings(page, 'zh')
const dialog = page.getByRole('dialog', { name: '设置' })
await dialog.waitFor({ timeout: 10_000 })
const darkCube = dialog.getByRole('button', { name: '深色' })
@@ -463,7 +461,7 @@ describe('web e2e: settings modal and General preferences', () => {
}
// `system` follows the emulated OS scheme (dark stays dark, light clears).
await openSettingsFromAccountMenu(page, 'zh')
await openSettings(page, 'zh')
const systemCube = page.getByRole('dialog', { name: '设置' }).getByRole('button', { name: '跟随系统' })
await selectTheme(systemCube, 'system')
await expect.poll(() => systemCube.getAttribute('aria-pressed'), { timeout: 5_000 }).toBe('true')
@@ -525,7 +523,7 @@ describe('web e2e: settings modal and General preferences', () => {
}
expect(await readFontSize()).toBe('14px')
expect(await readSecondaryFontSize()).toBe('13px')
await openSettingsFromAccountMenu(page, 'zh')
await openSettings(page, 'zh')
const dialog = page.getByRole('dialog', { name: '设置' })
await dialog.waitFor({ timeout: 10_000 })
// The stepper reveals its arrows on hover; the up arrow steps 14 → 15 → 16.
@@ -550,7 +548,7 @@ describe('web e2e: settings modal and General preferences', () => {
expect(await readSecondaryFontSize()).toBe('14px')
// Restore the default for the specs that follow (and the dialog golden).
await openSettingsFromAccountMenu(page, 'zh')
await openSettings(page, 'zh')
const restored = page.getByRole('dialog', { name: '设置' })
await restored.waitFor({ timeout: 10_000 })
await restored.getByText('16', { exact: true }).hover()
@@ -565,7 +563,7 @@ describe('web e2e: settings modal and General preferences', () => {
['detailed', '详细'], ['expanded', '完全展开'],
] as const)('persists the %s work-details mode across reload', async (mode, label) => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-settings-transcript-view'))
await openSettingsFromAccountMenu(page, 'zh')
await openSettings(page, 'zh')
const dialog = page.getByRole('dialog', { name: '设置' })
await dialog.waitFor({ timeout: 10_000 })
await dialog.getByText('工作过程展示', { exact: true }).waitFor({ timeout: 10_000 })
@@ -581,7 +579,7 @@ describe('web e2e: settings modal and General preferences', () => {
await page.reload({ waitUntil: 'load' })
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
acknowledgeReloadConnectionLoss(tripwire, warningStart)
await openSettingsFromAccountMenu(page, 'zh')
await openSettings(page, 'zh')
const reloaded = page.getByRole('dialog', { name: '设置' })
const restoredDetails = reloaded.getByText('工作过程展示', { exact: true }).locator('../..')
await restoredDetails.getByRole('button', { name: label, exact: true }).waitFor({ timeout: 10_000 })
@@ -597,7 +595,7 @@ describe('web e2e: settings modal and General preferences', () => {
it('persists the busy-state Enter behavior across reload and a distinct port', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-settings-enter-behavior'))
await openSettingsFromAccountMenu(page, 'zh')
await openSettings(page, 'zh')
const dialog = page.getByRole('dialog', { name: '设置' })
await dialog.waitFor({ timeout: 10_000 })
await dialog.getByRole('button', { name: '排队发送' }).click()
@@ -612,7 +610,7 @@ describe('web e2e: settings modal and General preferences', () => {
await page.reload({ waitUntil: 'load' })
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
acknowledgeReloadConnectionLoss(tripwire, warningStart)
await openSettingsFromAccountMenu(page, 'zh')
await openSettings(page, 'zh')
const reloaded = page.getByRole('dialog', { name: '设置' })
await reloaded.getByRole('button', { name: '插话发送' }).waitFor({ timeout: 10_000 })
@@ -623,7 +621,7 @@ describe('web e2e: settings modal and General preferences', () => {
expect(second.baseUrl).not.toBe(scaffold.baseUrl)
await secondPage.goto(second.authenticatedUrl, { waitUntil: 'load' })
await secondPage.waitForSelector('[class*="frame"]', { timeout: 30_000 })
await openSettingsFromAccountMenu(secondPage, 'zh')
await openSettings(secondPage, 'zh')
await secondPage.getByRole('dialog', { name: '设置' })
.getByRole('button', { name: '插话发送' }).waitFor({ timeout: 10_000 })
expect(await secondPage.evaluate(() => localStorage.getItem('dsh.conversation.busyEnter'))).toBeNull()
@@ -646,7 +644,7 @@ describe('web e2e: settings modal and General preferences', () => {
it('persists the settings language across reload and a distinct port', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-settings-language'))
await openSettingsFromAccountMenu(page, 'zh')
await openSettings(page, 'zh')
const zhDialog = page.getByRole('dialog', { name: '设置' })
await zhDialog.waitFor({ timeout: 10_000 })
// The document language follows the active locale in the assembled app, not
@@ -678,7 +676,7 @@ describe('web e2e: settings modal and General preferences', () => {
await page.reload({ waitUntil: 'load' })
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
acknowledgeReloadConnectionLoss(tripwire, warningStart)
const enTrigger = page.getByRole('button', { name: 'Account menu' })
const enTrigger = page.getByRole('button', { name: 'Settings' })
await enTrigger.waitFor({ timeout: 10_000 })
// A Chinese browser on another port still receives the explicit English
@@ -690,7 +688,7 @@ describe('web e2e: settings modal and General preferences', () => {
expect(second.baseUrl).not.toBe(scaffold.baseUrl)
await secondPage.goto(second.authenticatedUrl, { waitUntil: 'load' })
await secondPage.waitForSelector('[class*="frame"]', { timeout: 30_000 })
await openSettingsFromAccountMenu(secondPage, 'en')
await openSettings(secondPage, 'en')
await secondPage.getByRole('dialog', { name: 'Settings' })
.getByRole('button', { name: 'English' }).waitFor({ timeout: 10_000 })
expect(await secondPage.evaluate(() => localStorage.getItem('dsh.locale'))).toBeNull()
@@ -701,7 +699,7 @@ describe('web e2e: settings modal and General preferences', () => {
await second.close()
}
await openSettingsFromAccountMenu(page, 'en')
await openSettings(page, 'en')
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 })
@@ -725,7 +723,7 @@ describe('web e2e: settings modal and General preferences', () => {
await enPage.goto(fresh.authenticatedUrl, { waitUntil: 'load' })
await enPage.waitForSelector('[class*="frame"]', { timeout: 30_000 })
expect(await enPage.evaluate(() => localStorage.getItem('dsh.locale'))).toBeNull()
await openSettingsFromAccountMenu(enPage, 'en')
await openSettings(enPage, 'en')
const dialog = enPage.getByRole('dialog', { name: 'Settings' })
await dialog.waitFor({ timeout: 10_000 })
await dialog.getByRole('button', { name: 'English' }).waitFor({ timeout: 10_000 })
@@ -764,7 +762,7 @@ describe('web e2e: settings modal and General preferences', () => {
await frPage.goto(fresh.authenticatedUrl, { waitUntil: 'load' })
await frPage.waitForSelector('[class*="frame"]', { timeout: 30_000 })
expect(await frPage.evaluate(() => localStorage.getItem('dsh.locale'))).toBeNull()
await openSettingsFromAccountMenu(frPage, 'en')
await openSettings(frPage, 'en')
const dialog = frPage.getByRole('dialog', { name: 'Settings' })
await dialog.waitFor({ timeout: 10_000 })
await dialog.getByRole('button', { name: 'English' }).waitFor({ timeout: 10_000 })
@@ -798,7 +796,7 @@ describe('web e2e: settings modal and General preferences', () => {
onTestFinished(() => withoutBrowser.close())
const browserConsole = watchConsole(withoutBrowser)
await withoutBrowser.goto(fresh.authenticatedUrl, { waitUntil: 'load' })
await openSettingsFromAccountMenu(withoutBrowser, 'en')
await openSettings(withoutBrowser, 'en')
const dialog = withoutBrowser.getByRole('dialog', { name: 'Settings' })
await dialog.getByRole('button', { name: 'Detailed', exact: true }).waitFor()
expect(await dialog.getByText('Open chat links in', { exact: true }).count()).toBe(0)
+2 -2
View File
@@ -8,7 +8,7 @@ import type {} from '@deepseek-ai/dsh-api-terminal-controller'
import type { SubprocessTerminalHandle } from '@deepseek-ai/dsh-subprocess'
import { createProcessInspector, type ProcessIdentity } from '@deepseek-ai/dsh-subprocess-local/src/process-inspector.ts'
import { compareOrRefreshGolden, launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold } from './scaffold.ts'
import { openSettingsFromAccountMenu, connectFreshWorkspace, saveFailureShot } from './support.ts'
import { openSettings, connectFreshWorkspace, saveFailureShot } from './support.ts'
const expected = fileURLToPath(new URL('./expected/sidebar-terminal/running.expected.md', import.meta.url))
const shots = fileURLToPath(new URL('../../../.artifacts/screenshots/sidebar-terminal/', import.meta.url))
@@ -47,7 +47,7 @@ async function controlTransport(page: Page) {
}
async function selectTerminalTheme(page: Page, name: string): Promise<void> {
await openSettingsFromAccountMenu(page, 'en')
await openSettings(page, 'en')
const dialog = page.getByRole('dialog', { name: 'Settings' })
const [response] = await Promise.all([
page.waitForResponse(candidate => new URL(candidate.url()).pathname === '/api/settings/mutate' && candidate.request().method() === 'POST'),
+2 -2
View File
@@ -16,7 +16,7 @@ import {
compareOrRefreshGolden, fixtureUserPrompts,
launchWebScaffold, recordFixture, watchConsole, webSnapshotMode, type WebScaffold,
} from './scaffold.ts'
import { openSettingsFromAccountMenu, connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
import { openSettings, connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
const SNAPSHOT_DIR = fileURLToPath(new URL('../../../snapshots/web/steering', import.meta.url))
const FIXTURE = join(SNAPSHOT_DIR, 'session.v3.jsonl')
@@ -309,7 +309,7 @@ describe('web e2e: composer shortcut follows the swapped busy behavior', () => {
it.skipIf(MODE === 'record')('queues Cmd+Enter when plain Enter is configured to Steer', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-composer-swapped-shortcut'))
await openSettingsFromAccountMenu(page, 'en')
await openSettings(page, 'en')
const dialog = page.getByRole('dialog', { name: 'Settings' })
await dialog.getByRole('button', { name: 'Queue' }).click()
await page.getByRole('menuitem', { name: 'Steer' }).click()
+9 -4
View File
@@ -226,11 +226,16 @@ export function conversationContextKey(kind: string, id: string): string {
return `${kind.length}:${kind}${id}`
}
/** Open Settings through the sidebar account menu.
/** Open Settings through the Web gear or Desktop account menu.
* @param page - browser page with the mounted sidebar.
* @param locale - current UI language.
*/
export async function openSettingsFromAccountMenu(page: Page, locale: 'en' | 'zh'): Promise<void> {
await page.getByRole('button', { name: locale === 'zh' ? '账号菜单' : 'Account menu', exact: true }).click()
await page.getByRole('menuitem', { name: locale === 'zh' ? '设置' : 'Settings', exact: true }).click()
export async function openSettings(page: Page, locale: 'en' | 'zh'): Promise<void> {
const label = locale === 'zh' ? '设置' : 'Settings'
if (await page.evaluate(() => 'dshDesktop' in globalThis)) {
await page.getByRole('button', { name: locale === 'zh' ? '账号菜单' : 'Account menu', exact: true }).click()
await page.getByRole('menuitem', { name: label, exact: true }).click()
} else {
await page.getByRole('button', { name: label, exact: true }).click()
}
}
+3 -3
View File
@@ -20,7 +20,7 @@ import {
acknowledgeReloadConnectionLoss, assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
launchWebScaffold, recordFixture, watchConsole, webSnapshotMode, type WebScaffold,
} from './scaffold.ts'
import { openSettingsFromAccountMenu, connectFreshWorkspace, expandOwningTurnProcess, newEnglishPage, saveFailureShot, WEB_FIXTURE_TIME } from './support.ts'
import { openSettings, connectFreshWorkspace, expandOwningTurnProcess, newEnglishPage, saveFailureShot, WEB_FIXTURE_TIME } from './support.ts'
const SNAPSHOT_DIR = fileURLToPath(new URL('../../../snapshots/web/turn-tail-actions', import.meta.url))
const FIXTURE = join(SNAPSHOT_DIR, 'session.v3.jsonl')
@@ -256,7 +256,7 @@ describe('web e2e: assistant IconActions wait for the turn to end', () => {
expect(await process.getAttribute('aria-expanded')).toBe('false')
expect(await tool.isVisible()).toBe(false)
await openSettingsFromAccountMenu(page, 'en')
await openSettings(page, 'en')
const dialog = page.getByRole('dialog', { name: 'Settings' })
await dialog.getByText('Work details', { exact: true }).locator('../..')
.getByRole('button', { name: 'Compact', exact: true }).click()
@@ -279,7 +279,7 @@ describe('web e2e: assistant IconActions wait for the turn to end', () => {
expect(await tool.isVisible()).toBe(true)
await process.click()
await openSettingsFromAccountMenu(page, 'en')
await openSettings(page, 'en')
const restored = page.getByRole('dialog', { name: 'Settings' })
await restored.getByText('Work details', { exact: true }).locator('../..')
.getByRole('button', { name: label, exact: true }).click()
+61
View File
@@ -0,0 +1,61 @@
/** The real local provider publishes actionable download failures through the speech Remote to bundle details. */
import { once } from 'node:events'
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { createServer } from 'node:http'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { chromium, type Browser } from 'playwright'
import { expect, it, onTestFinished } from 'vitest'
import { captureStableAria, compareOrRefreshGolden, launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold } from './scaffold.ts'
import { newEnglishPage } from './support.ts'
const bundle = fileURLToPath(new URL('../../../packages/experimental/voice-input-bundle', import.meta.url))
const expected = fileURLToPath(new URL('./expected/voice-download.expected.md', import.meta.url))
it('shows the failed asset, actual download source and recovery advice, then retries on request', async () => {
const resources: { scaffold?: WebScaffold; browser?: Browser } = {}
const root = await mkdtemp(join(tmpdir(), 'dsh-voice-download-'))
onTestFinished(async () => { await rm(root, { recursive: true, force: true }) })
let status = 503
const requests: string[] = []
const server = createServer((request, response) => {
requests.push(request.url!)
response.writeHead(status); response.end()
})
onTestFinished(async () => {
try { await resources.browser?.close() } finally {
try { await resources.scaffold?.close() } finally {
const closed = new Promise<void>((resolve, reject) => { server.close((error) => { if (error) reject(error); else resolve() }) })
server.closeAllConnections(); await closed
}
}
})
server.listen(0, '127.0.0.1'); await once(server, 'listening')
const address = server.address()
if (!address || typeof address === 'string') throw new Error('Expected a loopback listener')
const origin = `http://127.0.0.1:${address.port}`, overlay = join(root, 'voice.patch.yml')
await writeFile(overlay, `- id: speech-to-text-sensevoice\n config:\n dataRoot: ${JSON.stringify(join(root, 'models'))}\n modelOrigin: ${origin}\n`)
const scaffold = await launchWebScaffold({ profile: { packages: [{ dir: bundle, enabled: true }] }, extraOverlayPath: overlay })
resources.scaffold = scaffold
const browser = await chromium.launch()
resources.browser = browser
const page = await newEnglishPage(browser), tripwire = watchConsole(page)
await page.goto(scaffold.authenticatedUrl)
await page.getByRole('button', { name: 'Plugins', exact: true }).click()
await page.locator('[data-plugin-package="@deepseek-ai/dsh-experimental-voice-input-bundle"]').getByRole('button').click()
await page.getByRole('button', { name: 'Download and prepare', exact: true }).click()
const alert = page.getByRole('alert').filter({ hasText: 'Cannot download model.int8.onnx' })
await alert.waitFor()
expect(await alert.textContent()).toContain('HTTP 503')
expect(await alert.textContent()).toContain(`Download source: ${origin}`)
expect(requests).toHaveLength(1)
expect(requests[0]).toContain('/resolve/2365baeacb507f821a0c8120fcee3d484dba7a07/model.int8.onnx')
await compareOrRefreshGolden(expected,
(await captureStableAria(page, '[role="alert"]', scaffold.workspaceCwd)).replaceAll(origin, 'https://model-mirror.example'), webSnapshotMode())
status = 404
await page.getByRole('button', { name: 'Retry preparation', exact: true }).click()
await expect.poll(() => alert.textContent()).toContain('HTTP 404')
expect(requests).toHaveLength(2)
expect(tripwire.pageErrors).toEqual([])
})
+2 -2
View File
@@ -556,7 +556,7 @@ describe('web e2e: workspace management (create / rename / grouping / hover affo
expect(await copied.isVisible()).toBe(true)
expect(await page.evaluate(() => navigator.clipboard.readText())).toBe(rowTitle)
// Leaving anchor and card together closes it after the grace.
await page.getByRole('button', { name: 'Account menu' }).hover()
await page.getByRole('button', { name: 'Settings' }).hover()
await expect.poll(() => card.count(), { timeout: 5_000 }).toBe(0)
expect(tripwire.pageErrors).toEqual([])
}, 60_000)
@@ -586,7 +586,7 @@ describe('web e2e: workspace management (create / rename / grouping / hover affo
await page.waitForTimeout(POINTER_HOLD_MS)
expect(await page.getByRole('menuitem', { name: 'Rename' }).count()).toBe(1)
// Pointer-leave dismissal still applies once the pointer genuinely leaves.
await page.getByRole('button', { name: 'Account menu' }).hover()
await page.getByRole('button', { name: 'Settings' }).hover()
await expect.poll(() => page.getByRole('menuitem', { name: 'Rename' }).count(), { timeout: 5_000 }).toBe(0)
expect(tripwire.pageErrors).toEqual([])
}, 60_000)
+1
View File
@@ -29,6 +29,7 @@
"tests/scaffold.ts",
"tests/voice-input.e2e.ts",
"tests/voice-setup.e2e.ts",
"tests/voice-download.e2e.ts",
"tests/prefix-proxy.ts",
"tests/auto-review-fixture.ts",
"tests/scaffold-generation.spec.ts",
+2 -2
View File
@@ -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 docs/subsystems/voice-input.md
voice-input.md: 32517d418512078cde5af221594d94171054abf4
voice-input.zh.md: 162a28162d8a171ac214dd59a2096752172c8081
voice-input.md: 1c128b268c8fbbcabc13f4ca52daaa62534a6366
voice-input.zh.md: 5ce0626f4dc769cb12a842b7ad296eb915cb94ea
+2
View File
@@ -20,6 +20,8 @@ The input facade captures a revision-bearing selection before recording. `InputA
The Host provider owns one preparation task across page and Session changes. The Client shares one `follow()` subscription across the composer, setup prompt and bundle details. Optional `SpeechSetupEstimate` metadata supplies provider-specific planning hints, separate from measured progress. `SpeechPreparationStepKind` identifies ordered resource operations; `SpeechPreparationStep` records each status and start time. The complete `SpeechPreparationState` retains these steps after cancellation or failure. The collapsed UI shows the current operation; expanding lists all steps, with byte progress or elapsed time only for the running step. Closing an observer never cancels preparation. Verified files survive retries and idle worker reclamation.
Failed preparation can include `SpeechDownloadFailure` with the asset, source origin, classified reason and optional diagnostic code or HTTP status. The Client localizes recovery advice; raw download causes remain on the Host.
The microphone occupies `conversation.input.activity`, between the model selector and Send. Clicking starts capture and expands the toolbar; Stop transcribes and inserts into the draft. The activity preserves the editor and submit action, owns local feedback, and releases expansion on unmount. Cancel, Escape or hiding the page discards capture. Waveform history displays measured microphone amplitude. Bundle details contain recognition preferences, preparation state and progress. Explicit enablement uses `plugins.bundle.activation` to guide users with missing models to setup; the list shows only the bundle description and switch.
## Design rationale
+2
View File
@@ -20,6 +20,8 @@
Host Provider 在页面和 Session 变化期间拥有同一个准备任务。Client 在输入框、安装引导弹窗、Bundle 详情之间共享一份 `follow()` 订阅。可选 `SpeechSetupEstimate` 元数据提供各 Provider 的资源预期,与实测进度分开。`SpeechPreparationStepKind` 标识有序资源操作;`SpeechPreparationStep` 记录各步骤状态与开始时间。完整 `SpeechPreparationState` 在取消或失败后保留这些步骤。折叠 UI 显示当前操作,展开后列出所有步骤,仅进行中步骤显示字节进度或等待时间。关闭观察者不会取消准备。已校验文件在重试及空闲进程回收后继续复用。
准备失败时可包含 `SpeechDownloadFailure`,提供文件、下载源、原因分类以及可选错误码或 HTTP 状态。Client 将恢复建议本地化;原始下载错误留在 Host。
麦克风占用模型选择器与发送按钮之间的 `conversation.input.activity`。点击开始录音并展开工具栏;停止后转写并插入草稿。活动栏保留编辑器与提交按钮,拥有局部反馈,并在卸载时释放展开状态。取消、Escape 或隐藏页面会丢弃录音。波形历史展示实测麦克风音量。Bundle 详情包含识别偏好、准备状态和进度。显式启用时通过 `plugins.bundle.activation` 引导缺少模型的用户前往安装;列表只显示 Bundle 描述和开关。
## 设计依据
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@deepseek-ai/dsh-root",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"license": "MIT",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-acp",
"description": "Automation-only Agent Client Protocol server for driving DeepSeek Harness agents over JSON-RPC stdio",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-api-account-controller",
"description": "Expose safe account operations over authenticated Remote",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-api-gateway",
"description": "Typert Remote Host dispatcher and Client API endpoint",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-api-job-controller",
"description": "Job Remote observation stream and the reference-counted client job-output service",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-api-remotes",
"description": "Remote BFF assembly for application-selected Host capabilities",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-api-session-controller",
"description": "Session Remote commands, cold reads, and live control transport",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"publishConfig": {
"access": "public"
},
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-api-settings-controller",
"description": "Remote owner for the configuration surfaces over the settings-domain seams",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"publishConfig": {
"access": "public"
},
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-api-terminal-controller",
"description": "Session-owned interactive terminals with shell discovery, screen recovery and typed Remote control",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"publishConfig": {
"access": "public"
},
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-api-workspace-controller",
"description": "Workspace Remote commands and reconnect-safe state transport",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-api-workspace-files",
"description": "Workspace file service and Client resource provider: bounded reads, directory listing, and live metadata over the workspaceFiles Remote namespace",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"publishConfig": {
"access": "public"
},
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-attachment-local",
"description": "Private content-addressed DSH_HOME attachment storage",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-attachment",
"description": "Durable immutable attachment storage seam for the DeepSeek Harness",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-app-boot",
"description": "Shared boot glue for the app bins: .env loading, fail-loud Loader guards, snapshot-aware config resolution, and the Loader boot sequence",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-cmdline",
"description": "Immutable command-line handoff from a dsh launcher to any app plugin that injects cmdlineArgs",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-config-editor",
"description": "Persist plugin configuration through profile patches and Loader reconciliation",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-hmr",
"description": "Coordinated module and profile configuration hot reload",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-plugin-manager",
"description": "Current-profile plugin and bundle management shared by dsh CLI, Web and agent tools",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"publishConfig": {
"access": "public"
},
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-browser-use",
"description": "Exclusive named browser-use provider registration",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-acp-app",
"description": "The dsh ACP profile bundle: automation-only JSON-RPC stdio and process lifecycle over dsh-base",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-base",
"description": "The shared dsh core as a profile bundle: the first patch layer of base-backed profiles, inserting core rows over the empty profile root",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-headless",
"description": "The dsh one-shot bundle: a direct core Agent/Session runner over dsh-base with no Host, HTTP, or browser layer",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-sdk-app",
"description": "The dsh SDK profile bundle: stdio JSON-RPC serving and process lifecycle over dsh-base",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-sdk-minimal",
"description": "The standalone minimal SDK profile bundle: JSON-RPC, one DeepSeek adapter, persistent shell, and JSONL sessions",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-web-app",
"description": "The dsh browser-surface bundle: the web patch layer over dsh-base plus the runtime glue plugin (frontend dist serving, web-surface prompt, bash runtime variables, URL line)",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-connection",
"description": "Authenticated RPC transport and generation lifecycle",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-file-upload",
"description": "Agent-scoped browser file upload, streaming intake, and staged receipt service",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-hmr",
"description": "Web client graph synchronization and rebuilt-bundle reload transport",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-locale",
"description": "Locale plugin: Host-backed preference, extensible language catalog, browser fallback, and typed built-in dictionaries",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-modules",
"description": "Client module system, dual-face: node half composes the __DSH_BOOT__ entry graph (incremental dsh.client scan, bundle route, index tap, webPlugins service); browser half is the lazy-CJS module table the vendored cordis Loader consumes as its internal seam",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-resources",
"description": "Unified client resource model: protocol-registered providers turn URL addresses into live values, consumed through the useResource global standard hook",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-store",
"description": "React-free observable and snapshot-store contracts with the shared Zustand/Immer engine",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-agent-preset",
"description": "Agent-preset surfaces: the default for later sessions, this session's seat, and the composition editor",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-approval",
"description": "Approval composer takeover over the scoped Remote Event waterfall",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-attachment",
"description": "Dynamic attachment presentation plugin for conversation input, message-image, and trajectory image slots",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"publishConfig": {
"access": "public"
},
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-brand-official",
"description": "Official DeepSeek Harness brand occupants for the Web client's sidebar slots",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-chat",
"description": "Chat Conversation target, node definitions, renderers, and details surface",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-commands",
"description": "Client command surface: global directory cache, '/' source, three command UI kinds, popupSelect registry",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-conversation",
"description": "Target-neutral Conversation assembly, shell, composer, queue, and view navigation",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-deliverables",
"description": "Changed-files card with per-file comparison tabs, delivery cards, and clickable final-response file references for Web",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"publishConfig": {
"access": "public"
},
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-directory-picker-browse",
"description": "In-app directory browsing surface: the workspace directory-flow owner rendering the host's listing and creation primitives",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"publishConfig": {
"access": "public"
},
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-directory-picker-native",
"description": "Native directory-picker surface: the renderless workspace directory-flow occupant driving the local Desktop or Host OS chooser",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-dockkit",
"description": "Docking layout kit: split-tree engine with invertible operations, and the React components that render and drive it (zero cordis)",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-goal",
"description": "Session goal surface: GoalBar docked above the composer, read from the goal session projection",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"publishConfig": {
"access": "public"
},
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-input-trigger",
"description": "Input trigger pipeline: '/' and '@' detection, candidate menu, pick routing to registered sources",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-jobs",
"description": "Session-header background-job list with on-demand streaming record panels",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-layout",
"description": "Shell plugin: three-column AppFrame with drag handles, ctx.layout viewing-state service (navigation + panels)",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"publishConfig": {
"access": "public"
},
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-message-feedback",
"description": "The Web feedback surface: per-message Like/Dislike in the assistant-message action strip and the feedback dialog behind both ratings and /feedback, backed by the messageFeedback and sessionFeedback Host Remotes",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"publishConfig": {
"access": "public"
},
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-model-selection",
"description": "Model selection over the shared model catalog, Session projection, and session.selectModel",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-open-in-app",
"description": "Web \"Open In...\" controls: the Session-header split button opening the workspace directory in an installed application, and the document preview's default-application controls for one file",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"publishConfig": {
"access": "public"
},
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-permission-presets",
"description": "Permission surfaces: a new-session default in General settings and a current-session /permission popup over the permissions projection",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-plan",
"description": "Plan mode controls, persistent transcript plan cards, and sidebar Markdown previews",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"publishConfig": {
"access": "public"
},
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-plugin-manager",
"description": "Plugin management for the dsh web client: the sidebar Plugins panel installs, enables, disables, retries, and composes installed plugin packages",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-primitives",
"description": "Pure React atoms for the dsh web UI: controls, icons, markdown, and JSON inspectors (zero cordis)",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"publishConfig": {
"access": "public"
},
@@ -1596,16 +1596,17 @@ export const IconSlidersTwoOutlineMedium = (props: IconProps) => (
const IconMicrophoneOutlineArtwork = ({ size = 16, className, strokeWidth }: WeightedIconProps) => (
<svg width={size} height={size} viewBox="0 0 16 16" className={className} fill="none" stroke="currentColor" strokeWidth={strokeWidth} aria-hidden="true">
<rect x="6" y="1" width="4" height="8" rx="2" /><path d="M3 7v1a5 5 0 0 0 10 0V7M8 13v2m-3 0h6" />
<rect x={4.5 + strokeWidth / 2} y={1 + strokeWidth / 2} width={7 - strokeWidth} height={10 - strokeWidth} rx={(7 - strokeWidth) / 2} />
<path d="M2.35 8.675C3.075 11.3 5.2 13.125 8 13.125C10.8 13.125 12.925 11.3 13.65 8.675M8 13.125V15" />
</svg>
)
/** Regular one-pixel microphone artwork. */
/** Microphone with uniform one-pixel strokes. */
export const IconMicrophoneOutlineRegular = (props: IconProps) => (
<IconMicrophoneOutlineArtwork {...props} strokeWidth={ICON_REGULAR_STROKE} />
)
/** Medium microphone artwork with a 1.3px stroke. */
/** Microphone with uniform 1.3px strokes. */
export const IconMicrophoneOutlineMedium = (props: IconProps) => (
<IconMicrophoneOutlineArtwork {...props} strokeWidth={ICON_MEDIUM_STROKE} />
)
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-reference",
"description": "Unified Web @file and @session reference source",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-renderer",
"description": "Browser UI renderer: React slot bindings, ctx.uiRenderer, and the assembled application root",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-schedule",
"description": "Read-only active Schedule catalog in the Web Session header",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-session",
"description": "Session Controller adapter for React and session-scoped slots",
"version": "0.1.6-alpha.2",
"version": "0.1.7-alpha.1",
"publishConfig": {
"access": "public"
},
@@ -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-settings-account/README.md
README.md: fdbbc4c391e7b4d6f6111de936b23f2fa101f3e3
README.zh.md: 64acaa4b975c5b37213990bb866b268c9a689d1e
README.md: f024f7ef6506c1c9317d80660aaa0c8221e6a51e
README.zh.md: 2868d46a11db60cfec57f1eee6b146316b989288
@@ -1,5 +1,5 @@
---
description: "The Account settings section displays DeepSeek login state and offers browser sign-in and cancellation; the sidebar account menu provides Platform sign-out."
description: "Desktop Account settings display DeepSeek login state and offer browser authorization and cancellation; the sidebar account menu provides Platform sign-out."
kind: "package-reference"
---
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
## Summary
The Account settings section displays DeepSeek login state and offers browser sign-in and cancellation; the sidebar account menu provides Platform sign-out.
Desktop Account settings display DeepSeek login state and offer browser authorization and cancellation; the sidebar account menu provides Platform sign-out.
## Table of Contents
@@ -20,19 +20,21 @@ The Account settings section displays DeepSeek login state and offers browser si
<a id="use-this-package"></a>
## Use this package
The client activates only inside Desktop, identified by its preload bridge. Plain Web clients keep the standard Settings launcher and API-key onboarding without account login, account settings, or an account-state subscription.
The sidebar and Account settings display the profile avatar as a circular image, with the account icon as fallback when the URL is absent or the image fails to load. The collapsed sidebar centers the avatar in a 36 × 36 px button.
The section registers through settings.section and uses the account Remote namespace. The stream survives carrier reconnects through the shared Remote supervisor. The page renders feature-owned English and Chinese copy and keeps API keys separate from account state.
Balances use Platform Web currency formatting: two decimal places and digit grouping, positive amounts truncated to cents, positive sub-cent amounts shown as <0.01, and negative amounts rounded with a minimum displayed magnitude of 0.01. Raw Host balance strings remain unchanged.
Desktop usage and top-up actions open an isolated native Platform view below a 48px return bar. Back destroys the view and preserves the Account settings page. Browser clients retain external links. Loading failures retain the return action and show a centered localized error with Retry. Retry reloads the same destination; renderer commands never receive the account token.
Desktop usage and top-up actions open an isolated native Platform view below a 48px return bar. Back destroys the view and preserves the Account settings page. Loading failures retain the return action and show a centered localized error with Retry. Retry reloads the same destination; renderer commands never receive the account token.
Contact us opens the Feishu questionnaire in the system browser on Desktop or a new browser tab on Web. It supplies the available Platform UID, build version, UI locale and physical screen resolution as prefill_* parameters, with hide_*=1 for every context field; tokens and contact details are excluded. Configure contactFormUrl on the ui-settings-account plugin to select another HTTPS form. contactSource defaults to empty until the questionnaire supports a Harness source option; OS and device fields remain unfilled, matching the Web implementation.
Contact us opens the Feishu questionnaire in the system browser. It supplies the available Platform UID, build version, UI locale and physical screen resolution as prefill_* parameters, with hide_*=1 for every context field; tokens and contact details are excluded. Configure contactFormUrl on the ui-settings-account plugin to select another HTTPS form. contactSource defaults to empty until the questionnaire supports a Harness source option; OS and device fields remain unfilled, matching the Web implementation.
The sidebar account menu uses the shared Menu surface, backdrop blur, spacing, and row typography; feature styles only size its launcher.
The account card’s More account information link opens `https://platform.deepseek.com` in the system browser on Desktop or a new tab on Web.
The account card’s More account information link opens `https://platform.deepseek.com` in the system browser.
<a id="understand-the-implementation"></a>
## Understand the implementation
@@ -44,7 +46,7 @@ The Account section appears first in Settings only while signed in; it is hidden
The [credentials subsystem](../../../docs/subsystems/credentials.md) owns storage APIs; the [architecture](../../../docs/architecture.md) explains application composition.
Account login uses a dismissible dialog before the model onboarding credential editor. The dialog uses a compact, right-aligned action row with the primary action last. The sidebar displays the profile name, falling back to the server-masked phone number or email when the name is absent; it stays blank while the profile is loading. Dialog copy and the sidebar account label are not text-selectable; the copy-link button remains available and restores its label two seconds after each successful copy. Clipboard failures show Copy failed on the link for two seconds without interrupting sign-in. Waiting shows a copyable authorization link and a loading indicator; timeout and failure require an explicit retry. Sign-in failures render in that dialog; the launcher's inline failure line reports only a failed sign-out. Closing a waiting dialog cancels its Host attempt. The sidebar can reopen the same API-key editor through the settings coordinator. Browser login preopens a tab on the click and navigates it only when that attempt receives its authorization URL; blocked popups retain the copy-link route. The original tab retains the authorization window handle and closes it on failure, timeout, or cancellation. Login feedback stays in the original tab; callbacks never navigate to a second Web UI.
Account login uses a dismissible dialog before the model onboarding credential editor. The dialog uses a compact, right-aligned action row with the primary action last. The sidebar displays the profile name, falling back to the server-masked phone number or email when the name is absent; it stays blank while the profile is loading. Dialog copy and the sidebar account label are not text-selectable; the copy-link button remains available and restores its label two seconds after each successful copy. Clipboard failures show Copy failed on the link for two seconds without interrupting sign-in. Waiting shows a copyable authorization link and a loading indicator; timeout and failure require an explicit retry. Sign-in failures render in that dialog; the launcher's inline failure line reports only a failed sign-out. Closing a waiting dialog cancels its Host attempt. The sidebar can reopen the same API-key editor through the settings coordinator.
A terminal account-state stream failure appears in the sign-in dialog or Account settings. Plugin unload suppresses late failure reports.
@@ -1,5 +1,5 @@
---
description: "设置中的账号页面显示 DeepSeek 登录状态,并提供浏览器登录和取消;侧边栏账号菜单提供 Platform 退出登录。"
description: "Desktop 设置中的账号页面显示 DeepSeek 登录状态,并提供浏览器授权和取消;侧边栏账号菜单提供 Platform 退出登录。"
kind: "package-reference"
---
@@ -9,7 +9,7 @@ kind: "package-reference"
## 概述
设置中的账号页面显示 DeepSeek 登录状态,并提供浏览器登录和取消;侧边栏账号菜单提供 Platform 退出登录。
Desktop 设置中的账号页面显示 DeepSeek 登录状态,并提供浏览器授权和取消;侧边栏账号菜单提供 Platform 退出登录。
## 目录
@@ -20,19 +20,21 @@ kind: "package-reference"
<a id="use-this-package"></a>
## 使用此包
客户端仅在具有 preload 桥接的 Desktop 内启用。普通 Web 客户端保留标准设置入口和 API Key 引导,不显示账号登录和账号设置,也不订阅账号状态。
侧边栏和账号设置以圆形图片显示账号头像;头像 URL 缺失或图片加载失败时使用账号图标兜底。折叠侧边栏将头像居中放置在 36 × 36 px 的按钮内。
该页面通过 settings.section 注册,并使用 account Remote 命名空间。共享 Remote 管理器负责连接恢复后的流重连。页面使用功能自有的中英文文案,将 API Key 与账号状态分开。
余额沿用 Platform Web 的金额格式:两位小数和千分位分组,正金额截断至分,小于一分的正金额显示为 <0.01,负金额按舍入规则处理且显示绝对值至少为 0.01。Host 返回的原始余额字符串保持不变。
Desktop 的用量和充值操作在 48px 返回栏下方打开隔离的原生 Platform 视图。返回操作销毁视图并保留 Account 设置页。浏览器客户端继续使用外链。加载失败时保留返回操作,居中显示本地化错误提示和“重试”按钮。重试重新加载同一目标页面;渲染进程命令不接收账号 token。
Desktop 的用量和充值操作在 48px 返回栏下方打开隔离的原生 Platform 视图。返回操作销毁视图并保留 Account 设置页。加载失败时保留返回操作,居中显示本地化错误提示和“重试”按钮。重试重新加载同一目标页面;渲染进程命令不接收账号 token。
“联系我们”在 Desktop 的系统浏览器或 Web 的新标签页打开飞书问卷。链接通过 prefill_* 参数填写已有的 Platform UID、构建版本、界面语言和屏幕物理分辨率,并为所有上下文字段设置 hide_*=1;不传 token 或联系方式。可在 ui-settings-account 插件配置 contactFormUrl,切换到另一个 HTTPS 问卷。问卷支持 Harness 来源选项前,contactSource 默认为空;OS 和设备字段沿用 Web 实现,保持未填写。
“联系我们”在系统浏览器中打开飞书问卷。链接通过 prefill_* 参数填写已有的 Platform UID、构建版本、界面语言和屏幕物理分辨率,并为所有上下文字段设置 hide_*=1;不传 token 或联系方式。可在 ui-settings-account 插件配置 contactFormUrl,切换到另一个 HTTPS 问卷。问卷支持 Harness 来源选项前,contactSource 默认为空;OS 和设备字段沿用 Web 实现,保持未填写。
侧边栏账号菜单使用公共 Menu 的表面、背景模糊、间距和菜单项排版;功能样式仅设置入口布局。
账号卡片的“更多账号信息”链接在 Desktop 中通过系统浏览器打开 `https://platform.deepseek.com`,在 Web 中打开新标签页。
账号卡片的“更多账号信息”链接通过系统浏览器打开 `https://platform.deepseek.com`。
<a id="understand-the-implementation"></a>
## 理解实现
@@ -44,7 +46,7 @@ Desktop 的用量和充值操作在 48px 返回栏下方打开隔离的原生 Pl
[凭证子系统](../../../docs/subsystems/credentials.zh.md)定义存储接口;[架构](../../../docs/architecture.zh.md)说明应用组合。
账号登录在模型引导的凭证编辑器之前显示可关闭的弹窗。弹窗底部采用紧凑的右对齐横排操作,主操作位于末尾。侧边栏优先显示昵称,缺少昵称时显示服务端返回的脱敏手机号或邮箱;资料加载期间保持留空。 弹窗文字和侧边栏账号状态文字不可选中,复制登录链接按钮始终可用,并在每次复制成功两秒后恢复原文案。剪贴板失败时,链接原位显示“复制失败”两秒,不中断登录。等待状态提供可复制的授权链接和加载图标;超时和失败均须用户手动重试。登录失败在该弹窗内呈现;侧边栏入口的内联失败提示仅用于退出登录失败。关闭等待弹窗会取消对应 Host 登录流程。侧边栏通过设置协调器重新打开同一个 API Key 编辑器。浏览器登录在点击时预先打开标签页,只有该次流程收到授权地址后才导航;弹窗被拦截时保留复制链接入口。原标签页保留授权窗口引用,并在失败、超时或取消时关闭它。登录反馈留在原标签页,回调不会导航到第二个 Web UI。
账号登录在模型引导的凭证编辑器之前显示可关闭的弹窗。弹窗底部采用紧凑的右对齐横排操作,主操作位于末尾。侧边栏优先显示昵称,缺少昵称时显示服务端返回的脱敏手机号或邮箱;资料加载期间保持留空。 弹窗文字和侧边栏账号状态文字不可选中,复制登录链接按钮始终可用,并在每次复制成功两秒后恢复原文案。剪贴板失败时,链接原位显示“复制失败”两秒,不中断登录。等待状态提供可复制的授权链接和加载图标;超时和失败均须用户手动重试。登录失败在该弹窗内呈现;侧边栏入口的内联失败提示仅用于退出登录失败。关闭等待弹窗会取消对应 Host 登录流程。侧边栏通过设置协调器重新打开同一个 API Key 编辑器。
账户状态流的终止错误会显示在登录对话框或账户设置中。插件卸载后不再报告延迟到达的错误。
Loaded 100 of 392 files, more files were not shown because too many files have changed in this diff. Show more