Merge remote-tracking branch 'origin/master' into worktree/web-multimodal-image-input

# Conflicts:
#	apps/cli/src/web.ts
#	apps/web/tests/smoke-fixture.e2e.ts
#	docs/architecture.i18n.yaml
#	packages/client/connection/src/client/fixture.ts
#	packages/client/runtime/src/client/sessions/conversation.ts
#	packages/client/runtime/src/client/sessions/session.ts
#	packages/client/ui-conversation/package.json
#	packages/client/ui-conversation/src/client/contract/slots.ts
#	packages/client/ui-conversation/src/client/index.ts
#	packages/client/ui-conversation/src/client/service.ts
#	packages/client/ui-conversation/src/client/skeleton/ConversationRoot.tsx
#	packages/client/ui-conversation/tests/apply-inject.spec.tsx
#	packages/client/ui-conversation/tests/service-orchestration.spec.ts
#	packages/host/runtime/src/api-proxy.ts
#	packages/host/runtime/src/boot.ts
#	packages/host/webserver/tests/webserver.spec.ts
#	pnpm-lock.yaml
This commit is contained in:
Yichen Jiang
2026-07-24 16:29:03 +08:00
503 changed files with 19038 additions and 3597 deletions

View File

@@ -10,7 +10,7 @@ The TUI surface:
- tells the agent where its own source lives: after boot it adds a prompt section naming this harness checkout, resolved from the launcher's real path so it holds under a PATH symlink and an arbitrary cwd, so the self-referential `cordis` toolset can read and modify it;
- applies the personal overlay from `~/.dsh` (see [app-boot's Personal config](../../packages/ui/app-boot/README.md#personal-config)): `.env` fills environment gaps (ambient > project `.env` > personal `.env`), `config.yaml` patches the booted tree.
The Web surface treats its invoking directory as the default project and loads applicable `AGENTS.md`/`CLAUDE.md` instructions into each agent-loop request prefix with a 65,536-byte render budget.
The Web surface treats its invoking directory as the default project, loads applicable `AGENTS.md`/`CLAUDE.md` instructions into each agent-loop request prefix with a 65,536-byte render budget, and opts into first-message model titles. The headless surface retains deterministic fallback titles without making the auxiliary title-model request.
## Install (developer machine)
@@ -20,4 +20,4 @@ Symlink the source-running launcher onto your PATH; it resolves the checkout thr
ln -sf "$(pwd)/bin/dsh" ~/.local/bin/dsh
```
`pnpm run demo:tui` runs the same entry from the repo root. The built form (`lib/bin.js`, via `pnpm run build`) needs `node --expose-internals` for the shipped config's HMR entry, exactly like the demo bins.
`pnpm run demo:tui` runs the same entry from the repo root. The built form (`lib/bin.js`, via `pnpm run build`) boots the same config under plain Node.

View File

@@ -14,6 +14,16 @@
"license": "BSD-3-Clause",
"dependencies": {
"@deepseek-ai/dsh-app-boot": "workspace:^",
"@deepseek-ai/dsh-client-connection": "workspace:^",
"@deepseek-ai/dsh-client-hmr": "workspace:^",
"@deepseek-ai/dsh-client-i18n": "workspace:^",
"@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-question": "workspace:^",
"@deepseek-ai/dsh-client-ui-sidebar": "workspace:^",
"@deepseek-ai/dsh-client-ui-theme": "workspace:^",
"@deepseek-ai/dsh-client-ui-trajectory": "workspace:^",
"@deepseek-ai/dsh-frontend": "workspace:^",
"@deepseek-ai/dsh-host-apiproxy": "workspace:^",
"@deepseek-ai/dsh-host-runtime": "workspace:^",

View File

@@ -14,6 +14,34 @@ const LOOPBACK_HOST = '127.0.0.1'
const ALL_INTERFACES_HOST = '0.0.0.0'
const REQUEST_ENVELOPE_HEADROOM_BYTES = 1024 * 1024
// --- Client composition (composition decisions live in the composing app) ---
// The composition layer owns one decision: which plugin packages mount (the
// roster). Dependency edges and the boot prefetch tier live in each package's
// dshClient declaration.
/**
* Dev-only plugin: the client HMR driver. Whether it composes in is a
* deployment decision — the dev graph includes its row, the prod graph does
* not mount it at all.
*/
const CLIENT_HMR_ID = '@deepseek-ai/dsh-client-hmr'
/** Bundle stat-poll interval for --dev (held here so the startup log states the real value). */
const CLIENT_BUNDLE_POLL_MS = 500
/** The client plugin roster (flat; per-row boot behavior comes from manifests). */
const CLIENT_PACKAGES = [
'@deepseek-ai/dsh-client-connection',
'@deepseek-ai/dsh-client-runtime',
'@deepseek-ai/dsh-client-ui-theme',
'@deepseek-ai/dsh-client-i18n',
'@deepseek-ai/dsh-client-ui-layout',
'@deepseek-ai/dsh-client-ui-sidebar',
'@deepseek-ai/dsh-client-ui-conversation',
'@deepseek-ai/dsh-client-ui-question',
'@deepseek-ai/dsh-client-ui-trajectory',
] as const
export async function runWeb(argv: string[]): Promise<void> {
const { values } = parseArgs({
args: argv,
@@ -21,6 +49,7 @@ export async function runWeb(argv: string[]): Promise<void> {
host: { type: 'string', default: LOOPBACK_HOST },
port: { type: 'string', default: '3080' },
'max-request-body-bytes': { type: 'string' },
dev: { type: 'boolean', default: false },
},
allowPositionals: false,
})
@@ -40,8 +69,11 @@ export async function runWeb(argv: string[]): Promise<void> {
? undefined
: Number(values['max-request-body-bytes'])
if (configuredMaxRequestBodyBytes !== undefined
&& (!Number.isInteger(configuredMaxRequestBodyBytes) || configuredMaxRequestBodyBytes < 1)) {
process.stderr.write(`dsh web: invalid --max-request-body-bytes ${values['max-request-body-bytes']}\n`)
&& (!Number.isInteger(configuredMaxRequestBodyBytes)
|| configuredMaxRequestBodyBytes < 1)) {
process.stderr.write(
`dsh web: invalid --max-request-body-bytes ${values['max-request-body-bytes']}\n`,
)
process.exit(1)
}
@@ -50,22 +82,46 @@ export async function runWeb(argv: string[]): Promise<void> {
boot: {
persistenceRoot: './.sessions',
workspaceContext: { maxBytes: 65_536 },
sessionTitleLlm: true,
},
})
const attachments = host.ctx.get('attachments')
if (attachments === undefined) throw new Error('dsh web: attachment service unavailable')
const maxRequestBodyBytes = configuredMaxRequestBodyBytes
?? Math.ceil(attachments.imageLimits.maxMessageImageBytes * 4 / 3) + REQUEST_ENVELOPE_HEADROOM_BYTES
?? Math.ceil(attachments.imageLimits.maxMessageImageBytes * 4 / 3)
+ REQUEST_ENVELOPE_HEADROOM_BYTES
// Web UI plugin chain: in-memory Loader tree over the eight UI packages,
// then the registry that feeds __DSH_BOOT__ and /plugins/<id>/client.js.
const mounted = await mountWebPlugins(host.ctx)
// Client plugin chain: in-memory Loader tree over the composed roster, then
// the registry that feeds the __DSH_BOOT__ entry graph and
// /plugins/<id>/client.js. All row content comes from dshClient discovery
// over the mounted roster (dev adds the HMR driver row and turns on the
// bundle watch that drives rebuilt frames).
const roster = [...CLIENT_PACKAGES, ...values.dev ? [CLIENT_HMR_ID] : []]
const mounted = await mountWebPlugins(host.ctx, roster, import.meta.url)
const webPlugins = createHostWebPluginRegistry({
ctx: host.ctx,
loader: mounted.loader,
resolvePkgJson: mounted.resolvePkgJson,
onError: (err: Error) => { process.stderr.write(`dsh web: plugin rescan: ${String(err)}\n`) },
...values.dev ? { watch: { intervalMs: CLIENT_BUNDLE_POLL_MS } } : {},
})
if (values.dev) {
// Dev visibility (the registry is a library and never prints): list what
// the bundle watch covers, then log every observed rebuild. This is a
// second onRebuilt subscription — the SSE relay inside the webserver is
// unaffected (multicast).
const revs = new Map(webPlugins.graph().entries.map(row => [row.id, row.rev]))
const bundlePaths = [...revs.keys()]
.map(id => webPlugins.clientPath(id))
.filter((path): path is string => path !== undefined)
console.log(
`dsh web: watching ${String(bundlePaths.length)} plugin bundles (${String(CLIENT_BUNDLE_POLL_MS)}ms poll):\n ${bundlePaths.join('\n ')}`,
)
webPlugins.onRebuilt((id, rev) => {
console.log(`dsh web: plugin rebuilt: ${id} rev ${revs.get(id) ?? '?'} -> ${rev}`)
revs.set(id, rev)
})
}
// Published so the webserver invariant companion can audit manifest/bundle
// consistency; nothing else reads this key.
host.ctx.reflect.provide('webPlugins', webPlugins)
@@ -97,7 +153,14 @@ export async function runWeb(argv: string[]): Promise<void> {
let server: Awaited<ReturnType<typeof startWebServer>>
try {
server = await startWebServer(
{ host: hostAddress, port, distIndex, apiHandler: host.handler, maxRequestBodyBytes, webPlugins },
{
host: hostAddress,
port,
distIndex,
apiHandler: host.handler,
maxRequestBodyBytes,
webPlugins,
},
(err: Error) => {
process.stderr.write(`dsh web: ${String(err)}\n`)
void shutdown(1)

View File

@@ -8,12 +8,56 @@
"src"
],
"references": [
{ "path": "../../vendor/cordis" },
{ "path": "../../packages/host/apiproxy" },
{ "path": "../../packages/host/runtime" },
{ "path": "../../packages/host/webserver" },
{ "path": "../../packages/core/session" },
{ "path": "../../packages/ui/app-boot" },
{ "path": "../../packages/util/paths" }
{
"path": "../../vendor/cordis"
},
{
"path": "../../packages/host/apiproxy"
},
{
"path": "../../packages/host/runtime"
},
{
"path": "../../packages/host/webserver"
},
{
"path": "../../packages/core/session"
},
{
"path": "../../packages/ui/app-boot"
},
{
"path": "../../packages/util/paths"
},
{
"path": "../../packages/client/connection"
},
{
"path": "../../packages/client/hmr"
},
{
"path": "../../packages/client/runtime"
},
{
"path": "../../packages/client/ui-theme"
},
{
"path": "../../packages/client/i18n"
},
{
"path": "../../packages/client/ui-layout"
},
{
"path": "../../packages/client/ui-sidebar"
},
{
"path": "../../packages/client/ui-conversation"
},
{
"path": "../../packages/client/ui-trajectory"
},
{
"path": "../../packages/client/ui-question"
}
]
}

View File

@@ -20,7 +20,7 @@
"react-dom": "^18.2.0"
},
"devDependencies": {
"@deepseek-ai/dsh-client-runtime": "workspace:^",
"@deepseek-ai/dsh-client-modules": "workspace:^",
"@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
"@deepseek-ai/dsh-client-web-react": "workspace:^",

View File

@@ -0,0 +1,16 @@
/**
* Browser stand-in for `node:module`, mapped by the vite alias in
* vite.config.ts (design §2.4). The vendored Loader's internal.ts imports
* `createRequire` at module scope but only calls it inside
* `ModuleLoader.fromInternal()`, whose version probe is compiled to the
* `"0.0.0"` define in the browser build — so this throw is a fail-loud
* tripwire for any path that would genuinely need Node's module machinery.
*/
/** Throwing stand-in for node:module's createRequire (never reached in the browser boot). */
export const createRequire = (): never => {
throw new Error('node:module is not available in the browser')
}
/** Erased type peer for the vendored loader's type-only LoadHookContext import. */
export type LoadHookContext = never

View File

@@ -0,0 +1,114 @@
// @vitest-environment jsdom
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { act, cleanup, fireEvent, screen, waitFor, within } from '@testing-library/react'
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
import type { WebBootEntry } from '@deepseek-ai/dsh-client-modules'
import { bootWebShell } from '@deepseek-ai/dsh-client-web'
const PLUGINS: readonly (WebBootEntry & { dir: string })[] = [
{ id: '@deepseek-ai/dsh-client-connection', dir: 'connection', url: '/plugins/connection.js', rev: 'fx', inject: [], immediately: true },
{ id: '@deepseek-ai/dsh-client-runtime', dir: 'runtime', url: '/plugins/runtime.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-connection'], immediately: true },
{ id: '@deepseek-ai/dsh-client-ui-theme', dir: 'ui-theme', url: '/plugins/ui-theme.js', rev: 'fx', inject: [], immediately: true },
{ id: '@deepseek-ai/dsh-client-i18n', dir: 'i18n', url: '/plugins/i18n.js', rev: 'fx', inject: [], immediately: true },
{ 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-trajectory', dir: 'ui-trajectory', url: '/plugins/ui-trajectory.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-conversation'] },
]
const bundles = new Map(PLUGINS.map(plugin => [
plugin.url,
readFileSync(join(process.cwd(), 'packages/client', plugin.dir, 'lib/client.js'), 'utf8'),
]))
interface FixtureTiming {
appendTitle(id: string, title: string): void
}
interface FixtureWindow extends Window {
__DSH_BOOT__?: { rev: string; entries: WebBootEntry[] }
__ModuleLoader__?: unknown
}
class ResizeObserverStub {
observe(): void {}
disconnect(): void {}
unobserve(): void {}
}
const win = window as FixtureWindow
let unmount: (() => void) | undefined
beforeEach(() => {
localStorage.clear()
history.replaceState(null, '', '/?fixture')
document.title = 'DeepSeek Harness'
const root = document.createElement('div')
root.id = 'root'
document.body.appendChild(root)
vi.stubGlobal('ResizeObserver', ResizeObserverStub)
vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) =>
setTimeout(() => { callback(0) }, 0) as unknown as number)
vi.stubGlobal('cancelAnimationFrame', (id: number) => { clearTimeout(id) })
win.__DSH_BOOT__ = { rev: 'fx', entries: PLUGINS.map(({ dir: _dir, ...plugin }) => plugin) }
})
afterEach(() => {
act(() => { unmount?.() })
unmount = undefined
cleanup()
delete win.__DSH_BOOT__
delete win.__ModuleLoader__
delete (globalThis as Record<string, unknown>).__fxTiming
document.body.innerHTML = ''
document.head.querySelectorAll('style[data-plugin]').forEach((style) => { style.remove() })
document.title = ''
history.replaceState(null, '', '/')
vi.unstubAllGlobals()
})
/** Read only the stable, user-facing title surfaces from the assembled app. */
function titleSurfaces(label: string): { sidebar: string; breadcrumb: string; documentTitle: string } {
const tree = screen.getByRole('tree', { name: 'Sessions' })
const sidebar = within(tree).getByText(label).textContent ?? ''
const breadcrumb = within(screen.getByRole('navigation', { name: '会话层级' }))
.getByRole('button', { name: label }).textContent ?? ''
return { sidebar, breadcrumb, documentTitle: document.title }
}
it('projects initial and revised durable titles through the built eight-plugin fixture app', async () => {
const root = document.querySelector<HTMLElement>('#root')
if (root === null) throw new Error('snapshot root missing')
act(() => {
unmount = bootWebShell(root, {
fetchBundle: (url) => {
const code = bundles.get(url)
return code === undefined ? Promise.reject(new Error(`missing built bundle ${url}`)) : Promise.resolve(code)
},
executeBundle: (code) => { (0, eval)(code) },
})
})
const projectLabel = await screen.findByText('fixture', {}, { timeout: 10_000 })
const projectRow = projectLabel.closest<HTMLElement>('[role="treeitem"]')
if (projectRow === null) throw new Error('fixture project row missing')
fireEvent.click(projectRow)
const initialLabel = 'Fixture 历史会话'
const initialRowLabel = await screen.findByText(initialLabel)
const initialRow = initialRowLabel.closest<HTMLElement>('[role="treeitem"]')
if (initialRow === null) throw new Error('fixture session row missing')
fireEvent.click(initialRow)
await waitFor(() => { expect(document.title).toBe(`${initialLabel} — DeepSeek Harness`) })
const initial = titleSurfaces(initialLabel)
const revisedLabel = 'Fixture 修订标题'
const timing = (globalThis as Record<string, unknown>).__fxTiming as FixtureTiming
act(() => { timing.appendTitle('fx-alpha', revisedLabel) })
await waitFor(() => { expect(document.title).toBe(`${revisedLabel} — DeepSeek Harness`) })
const revised = titleSurfaces(revisedLabel)
await expect(`${JSON.stringify({ initial, revised }, null, 2)}\n`)
.toMatchFileSnapshot('./snapshots/session-title.json')
})

View File

@@ -1,39 +1,68 @@
// Keyless boot-chain smoke over the REAL carrier: startWebServer + web-plugins
// registry surface + __DSH_BOOT__ injection + built shell dist in a real
// chromium. First describe: manifest injection + static serving. Second
// describe: the full eight-plugin production chain loads through the DI chain
// in ?fixture mode, exercises the sidebar rail, and covers durable history,
// pasted, and dropped images without a model key.
// 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
// 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
// question completes through the real UI stack. The full model round lands
// in smoke-real under the W5 real-host standard.
import { existsSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import type { Browser, Page } from 'playwright'
import { chromium } from 'playwright'
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
import { startWebServer } from '@deepseek-ai/dsh-host-webserver'
import type { WebPluginBootEntry } from '@deepseek-ai/dsh-host-webserver'
import type { WebBootEntry, WebBootGraph } from '@deepseek-ai/dsh-host-webserver'
import { DIST_INDEX, probeFreePort, requireDist, saveFailureShot } from './support.ts'
const bundlePath = (dir: string): string =>
fileURLToPath(new URL(`../../../packages/client/${dir}/lib/client.js`, import.meta.url))
/** id ↔ bundle table for the success pass (the production Web plugin chain). */
const REAL_PLUGINS: { id: string; dir: string; inject: string[]; immediately?: boolean }[] = [
{ id: '@deepseek-ai/dsh-client-connection', dir: 'connection', inject: [], immediately: true },
const LAYOUT_ID = '@deepseek-ai/dsh-client-ui-layout'
const SIDEBAR_ID = '@deepseek-ai/dsh-client-ui-sidebar'
const FIXTURE_PNG = 'iVBORw0KGgoAAAANSUhEUgAAAKAAAABaCAYAAAA/xl1SAAAAvklEQVR42u3SMQ0AAAjAMIyhELM4AAe8PD1qYFlk9cCXEAEDYkAwIAYEA2JAMCAGBANiQDAgBgQDYkAwIAYEA2JAMCAGBANiQDAgBgQDYkAwIAYEA2JAMCAGxIBCYEAMCAbEgGBADAgGxIBgQAwIBsSAYEAMCAbEgGBADAgGxIBgQAwIBsSAYEAMCAbEgGBADAgGxIAYEAyIAcGAGBAMiAHBgBgQDIgBwYAYEAyIAcGAGBAMiAHBgBgQDIgB4bYWLb6pnOb1xAAAAABJRU5ErkJggg=='
/** id ↔ bundle table for the success pass (the complete Web UI assembly). */
const REAL_PLUGINS: { id: string; dir: string; inject?: string[]; immediately?: boolean }[] = [
{ id: '@deepseek-ai/dsh-client-connection', dir: 'connection', immediately: true },
{ id: '@deepseek-ai/dsh-client-runtime', dir: 'runtime', inject: ['@deepseek-ai/dsh-client-connection'], immediately: true },
{ id: '@deepseek-ai/dsh-client-ui-theme', dir: 'ui-theme', inject: [], immediately: true },
{ id: '@deepseek-ai/dsh-client-i18n', dir: 'i18n', inject: [], immediately: true },
{ id: '@deepseek-ai/dsh-client-ui-layout', dir: 'ui-layout', inject: ['@deepseek-ai/dsh-client-runtime'] },
{ id: '@deepseek-ai/dsh-client-ui-sidebar', dir: 'ui-sidebar', inject: ['@deepseek-ai/dsh-client-ui-layout'] },
{ id: '@deepseek-ai/dsh-client-ui-conversation', dir: 'ui-conversation', inject: ['@deepseek-ai/dsh-client-ui-layout'] },
{ id: '@deepseek-ai/dsh-client-ui-theme', dir: 'ui-theme', immediately: true },
{ id: '@deepseek-ai/dsh-client-i18n', dir: 'i18n', immediately: true },
{ 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-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'] },
]
/** Manifest served by the fake registry: one live bundle row, one missing row. */
const ROWS: WebPluginBootEntry[] = [
{ id: '@deepseek-ai/dsh-client-ui-layout', url: '/plugins/@deepseek-ai/dsh-client-ui-layout/client.js', inject: [] },
{ id: '@probe/absent', url: '/plugins/@probe/absent/client.js', inject: [] },
]
const LAYOUT_BUNDLE = bundlePath('ui-layout')
const BUNDLE_PATHS = new Map(REAL_PLUGINS.map(p => [p.id, bundlePath(p.dir)]))
const row = (id: string, extra?: Partial<WebBootEntry>): WebBootEntry =>
({ id, url: `/plugins/${id}/client.js?rev=e2e`, rev: 'e2e', ...extra })
const graphRows: WebBootEntry[] = REAL_PLUGINS.map(p => row(p.id, {
...(p.inject !== undefined ? { inject: p.inject } : {}),
...(p.immediately === true ? { immediately: true } : {}),
}))
/** Graph for the fail-loud half: the immediately tier, one live UI row, one missing row. */
const FAIL_GRAPH: WebBootGraph = {
rev: 'e2e-fail',
entries: [...graphRows.filter(r => r.immediately === true), row(LAYOUT_ID), row('@probe/absent')],
}
/** Graph for the success pass: the complete assembly. */
const OK_GRAPH: WebBootGraph = { rev: 'e2e-ok', entries: graphRows }
/** Registry stub over a fixed graph (the real HostWebPluginRegistry is webserver-side production code). */
function fixedRegistry(graph: WebBootGraph, byId: ReadonlyMap<string, string>) {
return {
graph: () => graph,
clientPath: (id: string) => byId.get(id),
onRebuilt: () => () => undefined,
}
}
describe('web boot chain (keyless, real carrier)', () => {
let server: Awaited<ReturnType<typeof startWebServer>>
@@ -51,10 +80,7 @@ describe('web boot chain (keyless, real carrier)', () => {
distIndex: DIST_INDEX,
apiHandler,
maxRequestBodyBytes: 32 * 1024 * 1024,
webPlugins: {
snapshot: () => ROWS,
clientPath: id => (id === ROWS[0]!.id ? LAYOUT_BUNDLE : undefined),
},
webPlugins: fixedRegistry(FAIL_GRAPH, BUNDLE_PATHS),
}, (err) => { pageErrors.push(`server: ${String(err)}`) })
browser = await chromium.launch()
page = await browser.newPage()
@@ -67,16 +93,25 @@ describe('web boot chain (keyless, real carrier)', () => {
await server?.close()
})
it('GET / injects the manifest verbatim', async () => {
it('GET / injects the entry graph verbatim', async () => {
onTestFailed(() => saveFailureShot(page, 'smoke-boot-manifest'))
const boot = await page.evaluate(() => (window as { __DSH_BOOT__?: unknown }).__DSH_BOOT__)
expect(boot).toEqual({ plugins: ROWS })
expect(boot).toEqual(FAIL_GRAPH)
})
it('serves a real bundle through the plugins endpoint', async () => {
const res = await page.request.get(`${new URL(page.url()).origin}${ROWS[0]!.url}`)
const res = await page.request.get(`${new URL(page.url()).origin}/plugins/${LAYOUT_ID}/client.js`)
expect(res.status()).toBe(200)
expect(await res.text()).toContain('window.DSHClientProxy.loadPlugin')
expect(await res.text()).toContain('window.__ModuleLoader__.load')
})
it('boots to the loading page and fail-louds the absent entry', async () => {
onTestFailed(() => saveFailureShot(page, 'smoke-boot-fail-loud'))
await page.waitForSelector('text=HARNESS', { timeout: 10_000 })
await page.waitForSelector('text=Failed to load plugins', { timeout: 10_000 })
await page.waitForSelector('text=@probe/absent', { timeout: 2000 })
// The real UI must not have flipped in: the gate opens only on settled.
expect(await page.locator('[class*="frame"]').count()).toBe(0)
})
it('applies the token sheets before any plugin CSS', async () => {
@@ -85,8 +120,7 @@ describe('web boot chain (keyless, real carrier)', () => {
})
})
describe('web boot chain success pass (keyless, production plugin chain, ?fixture)', () => {
const missing = REAL_PLUGINS.filter(p => !existsSync(bundlePath(p.dir)))
describe('web boot chain success pass (keyless, nine real bundles, ?fixture)', () => {
let server: Awaited<ReturnType<typeof startWebServer>>
let browser: Browser
let page: Page
@@ -94,14 +128,9 @@ describe('web boot chain success pass (keyless, production plugin chain, ?fixtur
beforeAll(async () => {
requireDist()
const missing = REAL_PLUGINS.filter(p => !existsSync(bundlePath(p.dir)))
if (missing.length > 0) throw new Error(`client bundles not built (pnpm --filter <pkg> bundle): ${missing.map(m => m.dir).join(', ')}`)
const port = await probeFreePort()
const rows: WebPluginBootEntry[] = REAL_PLUGINS.map((p) => {
const row: WebPluginBootEntry = { id: p.id, url: `/plugins/${p.id}/client.js`, inject: p.inject }
if (p.immediately === true) row.immediately = true
return row
})
const byId = new Map(REAL_PLUGINS.map(p => [p.id, bundlePath(p.dir)]))
// ?fixture never opens HTTP streams; /api is a tripwire like the first describe.
const apiHandler = { fetch: () => Promise.resolve(new Response('fixture mode must not call /api', { status: 500 })) }
server = await startWebServer({
@@ -110,7 +139,7 @@ describe('web boot chain success pass (keyless, production plugin chain, ?fixtur
distIndex: DIST_INDEX,
apiHandler,
maxRequestBodyBytes: 32 * 1024 * 1024,
webPlugins: { snapshot: () => rows, clientPath: id => byId.get(id) },
webPlugins: fixedRegistry(OK_GRAPH, BUNDLE_PATHS),
}, (err) => { pageErrors.push(`server: ${String(err)}`) })
browser = await chromium.launch()
page = await browser.newPage()
@@ -135,8 +164,8 @@ describe('web boot chain success pass (keyless, production plugin chain, ?fixtur
it('every plugin CSS landed with its ownership tag', async () => {
const owners = await page.evaluate(() =>
[...document.querySelectorAll('style[data-plugin]')].map(s => (s as HTMLElement).dataset['plugin']))
expect(owners).toContain('@deepseek-ai/dsh-client-ui-layout')
expect(owners).toContain('@deepseek-ai/dsh-client-ui-sidebar')
expect(owners).toContain(LAYOUT_ID)
expect(owners).toContain(SIDEBAR_ID)
})
it('collapsed sidebar animates to a 56px rail with the four controls', async () => {
@@ -149,25 +178,27 @@ describe('web boot chain success pass (keyless, production plugin chain, ?fixtur
const settledTrack = async (px: string): Promise<void> => {
await expect.poll(firstTrack, { timeout: 2000 }).toBe(px)
}
// The brand wordmark is decorative svg (aria-hidden) — presence tracks the wide chrome.
const brand = () => page.locator('[class*="brand"]').count()
await page.getByRole('button', { name: 'Collapse sidebar' }).click()
// Mid-collapse the wide chrome is still mounted, fading — not swapped out.
expect(await page.locator('text=HARNESS').count()).toBe(1)
expect(await brand()).toBe(1)
await settledTrack('56px')
await expect.poll(() => page.locator('text=HARNESS').count(), { timeout: 2000 }).toBe(0)
for (const name of ['Expand sidebar', 'New session', 'New workspace', 'Search sessions', 'Settings']) {
await expect.poll(brand, { timeout: 2000 }).toBe(0)
for (const name of ['Open sidebar', 'New session', 'New workspace', 'Search sessions', 'Settings']) {
await expect(page.getByRole('button', { name }).isVisible(), name).resolves.toBe(true)
}
await page.getByRole('button', { name: 'Expand sidebar' }).click()
await settledTrack('300px')
await page.getByRole('button', { name: 'Open sidebar' }).click()
await settledTrack('280px')
await expect(page.getByRole('button', { name: 'Collapse sidebar' }).isVisible()).resolves.toBe(true)
// Rail search: collapse again, the search control expands and lands in the box.
await page.getByRole('button', { name: 'Collapse sidebar' }).click()
await settledTrack('56px')
await page.getByRole('button', { name: 'Search sessions' }).click()
await settledTrack('300px')
const focused = await page.evaluate(() =>
(document.activeElement as HTMLInputElement | null)?.placeholder ?? '')
expect(focused).toContain('Search')
await settledTrack('280px')
// Focus is deferred past the slide (EXPAND_SLIDE_MS) — poll for it.
await expect.poll(() => page.evaluate(() =>
(document.activeElement as HTMLInputElement | null)?.placeholder ?? ''), { timeout: 2000 }).toContain('Search')
})
it('renders file tool rows and expands fixture reasoning from either click target', async () => {
@@ -218,15 +249,36 @@ describe('web boot chain success pass (keyless, production plugin chain, ?fixtur
await preview.waitFor({ state: 'detached' })
})
it('keeps Markdown semantic while a fixture reply streams and finalizes', async () => {
onTestFailed(() => saveFailureShot(page, 'smoke-markdown-stream'))
await page.getByRole('button', { name: 'New session', exact: true }).click()
const input = page.locator('textarea[placeholder]')
await input.waitFor({ timeout: 15_000 })
await input.fill('render markdown')
await page.getByRole('button', { name: '发送' }).click()
const streaming = page.locator('[data-streaming="true"]')
await streaming.getByRole('heading', { name: 'Markdown fixture' }).waitFor({ timeout: 15_000 })
await streaming.waitFor({ state: 'detached', timeout: 15_000 })
const finalHeading = page.getByRole('heading', { name: 'Markdown fixture' })
expect(await finalHeading.evaluate(element => element.tagName)).toBe('H1')
expect(await page.locator('pre code').filter({ hasText: 'const markdown = true' }).count()).toBe(1)
const external = page.getByRole('link', { name: 'DeepSeek' })
expect(await external.getAttribute('target')).toBe('_blank')
expect(await external.getAttribute('rel')).toBe('noopener noreferrer')
})
it('pastes and drops images into the composer, then sends them as durable history', async () => {
onTestFailed(() => saveFailureShot(page, 'smoke-fixture-image-paste'))
await page.getByRole('button', { name: '停止' }).click()
const textarea = page.locator('textarea')
await textarea.waitFor({ state: 'visible' })
if (await page.getByRole('button', { name: '停止' }).count() > 0) {
await page.getByRole('button', { name: '停止' }).click()
}
await expect.poll(() => textarea.isEnabled()).toBe(true)
await textarea.evaluate((element) => {
const binary = atob('iVBORw0KGgoAAAANSUhEUgAAAKAAAABaCAYAAAA/xl1SAAAAvklEQVR42u3SMQ0AAAjAMIyhELM4AAe8PD1qYFlk9cCXEAEDYkAwIAYEA2JAMCAGBANiQDAgBgQDYkAwIAYEA2JAMCAGBANiQDAgBgQDYkAwIAYEA2JAMCAGxIBCYEAMCAbEgGBADAgGxIBgQAwIBsSAYEAMCAbEgGBADAgGxIBgQAwIBsSAYEAMCAbEgGBADAgGxIAYEAyIAcGAGBAMiAHBgBgQDIgBwYAYEAyIAcGAGBAMiAHBgBgQDIgB4bYWLb6pnOb1xAAAAABJRU5ErkJggg==')
const bytes = Uint8Array.from(binary, character => character.charCodeAt(0))
await textarea.evaluate((element, png) => {
const bytes = Uint8Array.from(atob(png), character => character.charCodeAt(0))
const transfer = new DataTransfer()
transfer.items.add(new File([bytes], 'clipboard.png', { type: 'image/png' }))
element.dispatchEvent(new ClipboardEvent('paste', {
@@ -234,12 +286,11 @@ describe('web boot chain success pass (keyless, production plugin chain, ?fixtur
cancelable: true,
clipboardData: transfer,
}))
})
}, FIXTURE_PNG)
const rail = page.getByLabel('待发送图片')
await rail.waitFor({ state: 'visible' })
const draftImage = rail.getByTitle('双击查看原图')
await draftImage.dblclick()
await rail.getByTitle('双击查看原图').dblclick()
const preview = page.getByRole('dialog', { name: '原图预览' })
await preview.waitFor({ state: 'visible' })
await page.keyboard.press('Escape')
@@ -247,38 +298,70 @@ describe('web boot chain success pass (keyless, production plugin chain, ?fixtur
await page.getByRole('button', { name: '发送' }).click()
await rail.waitFor({ state: 'detached' })
await expect.poll(() => page.getByTitle('双击查看原图').count()).toBeGreaterThanOrEqual(3)
await expect.poll(() => page.getByTitle('双击查看原图').count()).toBeGreaterThanOrEqual(1)
await page.getByRole('button', { name: '停止' }).click()
await expect.poll(() => textarea.isEnabled()).toBe(true)
await textarea.evaluate((element) => {
const binary = atob('iVBORw0KGgoAAAANSUhEUgAAAKAAAABaCAYAAAA/xl1SAAAAvklEQVR42u3SMQ0AAAjAMIyhELM4AAe8PD1qYFlk9cCXEAEDYkAwIAYEA2JAMCAGBANiQDAgBgQDYkAwIAYEA2JAMCAGBANiQDAgBgQDYkAwIAYEA2JAMCAGxIBCYEAMCAbEgGBADAgGxIBgQAwIBsSAYEAMCAbEgGBADAgGxIBgQAwIBsSAYEAMCAbEgGBADAgGxIAYEAyIAcGAGBAMiAHBgBgQDIgBwYAYEAyIAcGAGBAMiAHBgBgQDIgB4bYWLb6pnOb1xAAAAABJRU5ErkJggg==')
const bytes = Uint8Array.from(binary, character => character.charCodeAt(0))
await textarea.evaluate((element, png) => {
const bytes = Uint8Array.from(atob(png), character => character.charCodeAt(0))
const transfer = new DataTransfer()
transfer.items.add(new File([bytes], 'dropped.png', { type: 'image/png' }))
element.closest('[class*="card"]')?.dispatchEvent(new DragEvent('dragenter', {
const card = element.closest('[class*="card"]')
card?.dispatchEvent(new DragEvent('dragenter', {
bubbles: true,
cancelable: true,
dataTransfer: transfer,
}))
})
await page.getByRole('status').filter({ hasText: '松开以添加图片' }).waitFor({ state: 'visible' })
await textarea.evaluate((element) => {
const binary = atob('iVBORw0KGgoAAAANSUhEUgAAAKAAAABaCAYAAAA/xl1SAAAAvklEQVR42u3SMQ0AAAjAMIyhELM4AAe8PD1qYFlk9cCXEAEDYkAwIAYEA2JAMCAGBANiQDAgBgQDYkAwIAYEA2JAMCAGBANiQDAgBgQDYkAwIAYEA2JAMCAGxIBCYEAMCAbEgGBADAgGxIBgQAwIBsSAYEAMCAbEgGBADAgGxIBgQAwIBsSAYEAMCAbEgGBADAgGxIAYEAyIAcGAGBAMiAHBgBgQDIgBwYAYEAyIAcGAGBAMiAHBgBgQDIgB4bYWLb6pnOb1xAAAAABJRU5ErkJggg==')
const bytes = Uint8Array.from(binary, character => character.charCodeAt(0))
const transfer = new DataTransfer()
transfer.items.add(new File([bytes], 'dropped.png', { type: 'image/png' }))
element.closest('[class*="card"]')?.dispatchEvent(new DragEvent('drop', {
card?.dispatchEvent(new DragEvent('drop', {
bubbles: true,
cancelable: true,
dataTransfer: transfer,
}))
})
}, FIXTURE_PNG)
await rail.waitFor({ state: 'visible' })
await rail.getByAltText('dropped.png').waitFor({ state: 'visible' })
await page.getByRole('button', { name: '发送' }).click()
await rail.waitFor({ state: 'detached' })
await expect.poll(() => page.getByTitle('双击查看原图').count()).toBeGreaterThanOrEqual(4)
await expect.poll(() => page.getByTitle('双击查看原图').count()).toBeGreaterThanOrEqual(2)
})
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' })
const projectRow = sessionTree.getByRole('treeitem').filter({ hasText: '3 sessions' })
if (await projectRow.getAttribute('aria-expanded') === 'false') await projectRow.click()
await sessionTree.getByText('Fixture 历史会话', { exact: true }).click()
const composer = page.locator('[data-question-key]')
await composer.waitFor({ timeout: 15_000 })
expect({
question: await composer.getByRole('heading').innerText(),
progress: await composer.getByText('1 / 3', { exact: true }).innerText(),
options: await composer.getByRole('radio').allTextContents(),
custom: await composer.getByRole('button', { name: '其他,请填写自定义答案' }).innerText(),
}).toMatchInlineSnapshot(`
{
"custom": "其他,请填写自定义答案",
"options": [
"1工程落地型推荐更看重能直接做 runtime、tool executor、sandbox、trace 和线上问题排查。",
"2研究潜力型更看重 Agent 理解、训练评测思路和长期成长空间。",
"3均衡型同时要求工程能力和 Agent 认知,但可能筛选门槛更高。",
],
"progress": "1 / 3",
"question": "你现在更想招哪类 Agent/Harness 候选人?",
}
`)
await composer.getByRole('radio', { name: '工程落地型' }).click()
await composer.getByText('2 / 3', { exact: true }).waitFor()
await composer.getByRole('button', { name: '跳过本题', exact: true }).click()
await composer.getByRole('checkbox', { name: '系统设计' }).click()
await composer.getByRole('checkbox', { name: 'Agent 产品判断' }).click()
await composer.getByRole('checkbox', { name: 'Agent 产品判断' }).press('Enter')
await composer.waitFor({ state: 'detached' })
const restoredInput = page.locator('textarea[placeholder]')
await restoredInput.waitFor()
expect(await restoredInput.getAttribute('placeholder')).toBe('回复生成中,可停止后再输入')
})
it('stayed clean: no page errors across the whole load chain', () => {

View File

@@ -77,6 +77,55 @@ async function rpc<T>(baseUrl: string, method: string, payload: unknown): Promis
return body.result.value
}
interface HistoryPage {
events: { event: { type: string; data: unknown } }[]
hasMore: boolean
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null
}
function providerTitle(page: HistoryPage): string | undefined {
for (let index = page.events.length - 1; index >= 0; index--) {
const event = page.events[index]!.event
if (event.type !== 'session/title' || !isRecord(event.data)) continue
const source = event.data.source
if (typeof event.data.title === 'string' && isRecord(source) && source.kind === 'provider') {
return event.data.title
}
}
return undefined
}
function hasAssistantMarker(page: HistoryPage, marker: string): boolean {
return page.events.some(({ event }) => {
if (event.type !== 'assistant/message' || !isRecord(event.data) || !Array.isArray(event.data.content)) return false
return event.data.content.some(block =>
isRecord(block) && block.type === 'text' && typeof block.text === 'string' && block.text.includes(marker))
})
}
async function history(baseUrl: string, sessionId: string): Promise<HistoryPage> {
return rpc<HistoryPage>(baseUrl, 'session.history', { sessionId, maxMessages: 10 })
}
async function waitForProviderTitle(baseUrl: string, sessionId: string): Promise<string> {
let observed: string | undefined
await expect.poll(async () => {
observed = providerTitle(await history(baseUrl, sessionId))
return observed
}, { timeout: 90_000 }).toEqual(expect.any(String))
if (observed === undefined) throw new Error('provider-backed session title was not observed')
return observed
}
async function waitForAssistantMarker(baseUrl: string, sessionId: string, marker: string): Promise<void> {
await expect.poll(async () => hasAssistantMarker(await history(baseUrl, sessionId), marker), {
timeout: 120_000,
}).toBe(true)
}
/** W5 screenshot: evidence for the figma comparison, not a failure artifact. */
async function screen(page: Page, name: string): Promise<void> {
await page.screenshot({ path: join(REPO_ROOT, '.artifacts', `w5-${name}.png`) })
@@ -95,10 +144,11 @@ async function detailsTrack(page: Page): Promise<number> {
return Number(cols.split(' ').pop()!.replace('px', ''))
}
// Readiness gate: `dsh web` serves ALL eight manifest plugins; until every UI
// Readiness gate: `dsh web` serves ALL nine 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-trajectory']
const UI_PLUGIN_DIRS = ['connection', 'runtime', 'ui-theme', 'i18n', 'ui-layout', 'ui-sidebar', 'ui-conversation', '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')
return !existsSync(bundle) || !readFileSync(bundle, 'utf8').includes('exports.apply')
@@ -280,7 +330,8 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY || notReady.length > 0)('web smoke
const input = page.locator('textarea').first()
await input.waitFor({ timeout: 10_000 })
await screen(page, '02-empty-state')
await input.fill('请简单介绍事件溯源,两句话即可,最后以「介绍完毕」结尾')
const prompt = `Please answer this request carefully: explain event sourcing in two sentences, ending with exactly ${ROUND_DONE_MARKER}.`
await input.fill(prompt)
await input.press('Enter')
// startSession chain: session mounts, composer moves to the bottom.
// Regression pin (P0, 585671106): this send used to white-screen the tree
@@ -288,7 +339,32 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY || notReady.length > 0)('web smoke
// near-empty here means that class of bug is back.
await page.waitForFunction(() => document.body.innerText.length > 50, undefined, { timeout: 15_000 })
expect(pageErrors).toEqual([])
await page.waitForFunction(() => document.body.innerText.includes('介绍完毕'), undefined, { timeout: 120_000 })
await page.waitForFunction(
() => document.title !== 'DeepSeek Harness' && document.title.endsWith(' — DeepSeek Harness'),
undefined,
{ timeout: 15_000 },
)
await expect.poll(async () => (await rpc<{ items: { sessionId: string }[] }>(baseUrl, 'session.list', {})).items.length, {
timeout: 15_000,
}).toBe(1)
const sessions = await rpc<{ items: { sessionId: string }[] }>(baseUrl, 'session.list', {})
const sessionId = sessions.items[0]?.sessionId
if (sessionId === undefined) throw new Error('created Web session was not listed')
const durableTitle = await waitForProviderTitle(baseUrl, sessionId)
await page.waitForFunction(
expected => document.title === `${expected} — DeepSeek Harness`,
durableTitle,
{ timeout: 15_000 },
)
const sessionTree = page.getByRole('tree', { name: 'Sessions' })
const projectRow = sessionTree.getByRole('treeitem').first()
if (await projectRow.getAttribute('aria-expanded') === 'false') await projectRow.click()
await Promise.all([
sessionTree.getByText(durableTitle, { exact: true }).waitFor({ timeout: 10_000 }),
page.getByRole('navigation').getByText(durableTitle, { exact: true }).waitFor({ timeout: 10_000 }),
])
await waitForAssistantMarker(baseUrl, sessionId, ROUND_DONE_MARKER)
await page.locator('p').filter({ hasText: ROUND_DONE_MARKER }).waitFor({ timeout: 10_000 })
await screen(page, '04-round-complete')
}, 150_000)
@@ -308,10 +384,10 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY || notReady.length > 0)('web smoke
await input.fill('请用 bash 工具运行命令 echo w5marker 然后告诉我结果')
await input.press('Enter')
// Wait for the tool ROW, not response text (the reply echoes any marker).
// bash renders through the third-party sample registration (data-sample) —
// that IS the differential-rendering acceptance; the generic path renders
// data-variant rows with the handler on the data-clickable inner row.
const toolRow = page.locator('[data-sample], [data-variant] [data-clickable]').first()
// Bash renders through the third-party sample registration. Match that
// exact row: other clickable variants (for example Think disclosure)
// may precede the tool call in document order.
const toolRow = page.locator('[data-sample="bash-global"]')
await toolRow.waitFor({ timeout: 120_000 })
await screen(page, '08-bash-round')
expect(await detailsTrack(page)).toBe(0)
@@ -363,7 +439,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY || notReady.length > 0)('web smoke
onTestFailed(() => saveFailureShot(page, 'w5-reload'))
await page.reload({ waitUntil: 'load' })
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
await page.waitForFunction(() => document.body.innerText.includes('介绍完毕'), undefined, { timeout: 30_000 })
await page.locator('p').filter({ hasText: ROUND_DONE_MARKER }).waitFor({ timeout: 30_000 })
await screen(page, '12-reload-recovery')
})

View File

@@ -0,0 +1,12 @@
{
"initial": {
"sidebar": "Fixture 历史会话",
"breadcrumb": "Fixture 历史会话",
"documentTitle": "Fixture 历史会话 — DeepSeek Harness"
},
"revised": {
"sidebar": "Fixture 修订标题",
"breadcrumb": "Fixture 修订标题",
"documentTitle": "Fixture 修订标题 — DeepSeek Harness"
}
}

View File

@@ -9,14 +9,23 @@
"DOM",
"DOM.Iterable"
],
"types": ["node"]
"types": [
"node"
]
},
"include": [
"src",
"tests"
],
"references": [
{ "path": "../../packages/client/web" },
{ "path": "../../packages/host/webserver" }
{
"path": "../../packages/client/web"
},
{
"path": "../../packages/host/webserver"
},
{
"path": "../../packages/client/modules"
}
]
}

View File

@@ -10,17 +10,28 @@ export default defineConfig({
// Workspace packages resolve to SOURCE: package.json exports point at lib
// for Node/type consumers, but the browser bundle must compile src directly
// so CSS rides vite's pipeline instead of the CSS-externalized lib bundle.
// Only the shell's static surface is aliased — UI plugin packages are NOT
// bundled here; they arrive as dynamic bundles through the client loader.
// Order matters — subpath aliases must win over bare-name prefixes.
// Only the shell's normal-package surface is aliased — plugin packages are
// NEVER bundled here (web2 shell self-sufficiency); they arrive as runtime
// bundles through the client module system. Order matters — subpath
// aliases must win over bare-name prefixes.
alias: [
// Browserization of the vendored cordis Loader: its only node-only
// import; the two process probes are mapped by `define` below.
{ find: /^node:module$/, replacement: src('./src/node-module-stub.ts') },
{ find: /^@deepseek-ai\/dsh-client-web$/, replacement: src('../../packages/client/web/src/boot.tsx') },
{ find: /^@deepseek-ai\/dsh-client-web-react\/store$/, replacement: src('../../packages/client/web-react/src/store/index.ts') },
{ find: /^@deepseek-ai\/dsh-client-web-react$/, replacement: src('../../packages/client/web-react/src/index.ts') },
{ find: /^@deepseek-ai\/dsh-client-ui-slots$/, replacement: src('../../packages/client/ui-slots/src/index.ts') },
{ find: /^@deepseek-ai\/dsh-client-ui-primitives$/, replacement: src('../../packages/client/ui-primitives/src/index.ts') },
{ find: /^@deepseek-ai\/dsh-client-runtime\/loader$/, replacement: src('../../packages/client/runtime/src/client/loader/index.ts') },
{ find: /^@deepseek-ai\/dsh-client-runtime$/, replacement: src('../../packages/client/runtime/src/index.ts') },
{ find: /^@deepseek-ai\/dsh-client-modules$/, replacement: src('../../packages/client/modules/src/index.ts') },
],
},
define: {
// vendored loader internal.ts: fromInternal() probes the Node major —
// "0.0.0" takes neither branch, returning undefined (exactly the empty
// internal slot the shell boot fills with the client module loader).
'process.versions.node': '"0.0.0"',
'process.execArgv': '[]',
// vendored loader index.ts: envData falls to its default branch.
'process.env.CORDIS_SHARED': 'undefined',
},
})