feat(web): add session model selector

This commit is contained in:
Yichen Jiang
2026-07-24 14:55:54 +08:00
parent bc7a89b81f
commit 208a44a7ec
87 changed files with 2236 additions and 87 deletions

View File

@@ -20,6 +20,7 @@
"@deepseek-ai/dsh-client-runtime": "workspace:^",
"@deepseek-ai/dsh-client-ui-conversation": "workspace:^",
"@deepseek-ai/dsh-client-ui-layout": "workspace:^",
"@deepseek-ai/dsh-client-ui-model-selector": "workspace:^",
"@deepseek-ai/dsh-client-ui-question": "workspace:^",
"@deepseek-ai/dsh-client-ui-sidebar": "workspace:^",
"@deepseek-ai/dsh-client-ui-theme": "workspace:^",

View File

@@ -37,6 +37,7 @@ const CLIENT_PACKAGES = [
'@deepseek-ai/dsh-client-ui-layout',
'@deepseek-ai/dsh-client-ui-sidebar',
'@deepseek-ai/dsh-client-ui-conversation',
'@deepseek-ai/dsh-client-ui-model-selector',
'@deepseek-ai/dsh-client-ui-question',
'@deepseek-ai/dsh-client-ui-trajectory',
] as const

View File

@@ -53,6 +53,9 @@
{
"path": "../../packages/client/ui-conversation"
},
{
"path": "../../packages/client/ui-model-selector"
},
{
"path": "../../packages/client/ui-trajectory"
},

View File

@@ -14,6 +14,7 @@ const PLUGINS: readonly (WebBootEntry & { dir: string })[] = [
{ id: '@deepseek-ai/dsh-client-ui-layout', dir: 'ui-layout', url: '/plugins/ui-layout.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-runtime'] },
{ id: '@deepseek-ai/dsh-client-ui-sidebar', dir: 'ui-sidebar', url: '/plugins/ui-sidebar.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-layout'] },
{ id: '@deepseek-ai/dsh-client-ui-conversation', dir: 'ui-conversation', url: '/plugins/ui-conversation.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-layout'] },
{ id: '@deepseek-ai/dsh-client-ui-model-selector', dir: 'ui-model-selector', url: '/plugins/ui-model-selector.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-conversation'] },
{ id: '@deepseek-ai/dsh-client-ui-trajectory', dir: 'ui-trajectory', url: '/plugins/ui-trajectory.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-conversation'] },
]
@@ -77,7 +78,7 @@ function titleSurfaces(label: string): { sidebar: string; breadcrumb: string; do
return { sidebar, breadcrumb, documentTitle: document.title }
}
it('projects initial and revised durable titles through the built eight-plugin fixture app', async () => {
it('projects initial and revised durable titles through the built nine-plugin fixture app', async () => {
const root = document.querySelector<HTMLElement>('#root')
if (root === null) throw new Error('snapshot root missing')
act(() => {

View File

@@ -1,7 +1,7 @@
// Keyless boot-chain smoke over the REAL carrier: startWebServer + entry
// graph (__DSH_BOOT__ web2 shape) injection + built shell dist in a real
// chromium. First describe: graph injection + the fail-loud half. Second
// describe: the settled success pass — all nine REAL tsdown bundles load
// describe: the settled success pass — all ten REAL tsdown bundles load
// through the module system + vendored Loader chain in ?fixture mode (the
// infrastructure four ride the immediately prefetch tier, the UI rows fetch
// on demand), the three-column frame appears in one flip, and the resident
@@ -31,6 +31,7 @@ const REAL_PLUGINS: { id: string; dir: string; inject?: string[]; immediately?:
{ id: LAYOUT_ID, dir: 'ui-layout', inject: ['@deepseek-ai/dsh-client-runtime'] },
{ id: SIDEBAR_ID, dir: 'ui-sidebar', inject: [LAYOUT_ID] },
{ id: '@deepseek-ai/dsh-client-ui-conversation', dir: 'ui-conversation', inject: [LAYOUT_ID] },
{ id: '@deepseek-ai/dsh-client-ui-model-selector', dir: 'ui-model-selector', inject: ['@deepseek-ai/dsh-client-ui-conversation'] },
{ id: '@deepseek-ai/dsh-client-ui-question', dir: 'ui-question', inject: ['@deepseek-ai/dsh-client-ui-conversation'] },
{ id: '@deepseek-ai/dsh-client-ui-trajectory', dir: 'ui-trajectory', inject: ['@deepseek-ai/dsh-client-ui-conversation'] },
]
@@ -118,7 +119,7 @@ describe('web boot chain (keyless, real carrier)', () => {
})
})
describe('web boot chain success pass (keyless, nine real bundles, ?fixture)', () => {
describe('web boot chain success pass (keyless, ten real bundles, ?fixture)', () => {
let server: Awaited<ReturnType<typeof startWebServer>>
let browser: Browser
let page: Page
@@ -246,6 +247,37 @@ describe('web boot chain success pass (keyless, nine real bundles, ?fixture)', (
expect(await external.getAttribute('rel')).toBe('noopener noreferrer')
})
it('selects a model from a provider group and uses it for the next fixture request', async () => {
onTestFailed(() => saveFailureShot(page, 'smoke-model-selector'))
await page.getByRole('button', { name: 'New session', exact: true }).click()
const emptyInput = page.locator('textarea[placeholder]')
await emptyInput.fill('selector seed')
await page.getByRole('button', { name: '发送' }).click()
const selector = page.getByRole('button', { name: '选择模型,当前 DeepSeek-V4-Flash' })
await selector.waitFor({ state: 'visible', timeout: 15_000 })
expect(await selector.innerText()).toBe('DeepSeek-V4-Flash')
await selector.click()
const menu = page.getByRole('menu', { name: '模型' })
await menu.waitFor()
const triggerBox = await selector.boundingBox()
const menuBox = await menu.boundingBox()
expect(triggerBox).not.toBeNull()
expect(menuBox).not.toBeNull()
expect((menuBox?.y ?? 0) + (menuBox?.height ?? 0)).toBeLessThanOrEqual((triggerBox?.y ?? 0) + 1)
expect(await page.getByRole('group', { name: 'DeepSeek' }).getByRole('menuitemradio').allTextContents())
.toEqual(expect.arrayContaining(['DeepSeek-V4-Flash快速响应', 'DeepSeek-V4-Pro复杂任务']))
await page.getByRole('group', { name: 'OpenAI' }).getByRole('menuitemradio', { name: 'GPT-5' }).click()
await menu.waitFor({ state: 'detached' })
expect(await page.getByRole('button', { name: '选择模型,当前 GPT-5' }).innerText()).toBe('GPT-5')
const residentInput = page.locator('textarea[placeholder]')
await expect.poll(() => residentInput.isEnabled(), { timeout: 15_000 }).toBe(true)
await residentInput.fill('report model')
await page.getByRole('button', { name: '发送' }).click()
await page.getByText('当前模型openai/gpt-5', { exact: true }).waitFor({ timeout: 15_000 })
})
it('renders and completes the resident question through the composer slot', async () => {
onTestFailed(() => saveFailureShot(page, 'smoke-question-composer'))
const sessionTree = page.getByRole('tree', { name: 'Sessions' })

View File

@@ -144,10 +144,13 @@ async function detailsTrack(page: Page): Promise<number> {
return Number(cols.split(' ').pop()!.replace('px', ''))
}
// Readiness gate: `dsh web` serves ALL nine manifest plugins; until every UI
// Readiness gate: `dsh web` serves all ten production manifest plugins; 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', 'i18n', 'ui-layout', 'ui-sidebar', 'ui-conversation', 'ui-question', 'ui-trajectory']
const UI_PLUGIN_DIRS = [
'connection', 'runtime', 'ui-theme', 'i18n', 'ui-layout', 'ui-sidebar',
'ui-conversation', 'ui-model-selector', 'ui-question', 'ui-trajectory',
]
const ROUND_DONE_MARKER = 'WEB_ROUND_DONE'
const notReady = UI_PLUGIN_DIRS.filter((dir) => {
const bundle = join(REPO_ROOT, 'packages/client', dir, 'lib/client.js')