Skip to content

The web frontend is built in two disjoint halves that meet at runtime. The shell (apps/web over @deepseek-ai/dsh-client-web) is a compiled Vite application; the plugins are lazily loaded client.js bundles. This page follows the whole chain: dsh web → served dist → index.html → shell boot → slot render of the UI.

The two build targets ​

TargetPackageBuilt byWhat it ships
Plugin bundlesevery dsh.client packagetsdown (packages/client/tsdown.client.ts)one ./client.js per package, plus map + package.json exports["./client"]
The shell@deepseek-ai/dsh-web-frontend = apps/webVite (apps/web/vite.config.ts)dist/index.html + hashed assets/ chunks

apps/web is not a standalone application — its Vite config throws if you try a bare serve/preview (rejectStandaloneServe) because only the host injects window.__DSH_BOOT__. The entry is thin:

ts
// apps/web/src/main.ts
import { AppWebEntry } from '@deepseek-ai/dsh-client-web'
const el = document.getElementById('root')
if (el === null) throw new Error('web app: missing #root')
void new AppWebEntry(el).run()

Vite's manifest.webmanifest and favicon.svg live in apps/web/public/; the docs/web-styling.md guide documents the CSS conventions the shell obeys.

Serving the built app ​

The host half of the web transport (patches in bundle/web-app/cordis.patch.yml) mounts the web rows. The webserver row (@deepseek-ai/dsh-host-webserver, default 127.0.0.1:3080) registers the /api gateway prefix and the /api/remote.mux WebSocket upgrade; the web-runtime row mounts @deepseek-ai/dsh-web-app (the bundle's glue plugin, packages/bundle/web-app/src/index.ts).

@deepseek-ai/dsh-web-app resolves the frontend dist (workspace knowledge, never user config) by require.resolve('@deepseek-ai/dsh-web-frontend/dist/index.html'), then mounts @deepseek-ai/dsh-host-frontend-static over the web server's fallback seat (registerFallback). frontend-static (packages/host/frontend-static/src/index.ts) serves with locked semantics: traversal outside the dist root → 403, absent or non-file targets → 404 (deep SPA misses no longer fall back to index.html), non-GET/HEAD → 405. The index path is gated by ctx.connection.authorizeIndex before its bytes are read (see Client runtime): a valid launch token in the URL mints the dsh-auth-* cookie and redirects (303) to clean /, a valid cookie serves the page, anything else gets the same minimal 401. Every index response flows through ctx.webServer.renderIndex — the structured injection renderer that runs the boot-manifest and boot-theme injections and appends the __DSH_BOOT_READY__ tail.

The dsh web CLI seam ​

@deepseek-ai/dsh-web-app/startup (packages/bundle/web-app/src/startup.ts) parses the --profile web flag family (--host, --port, --trusted-host, and the new --no-open) with a commander command and provides immutable values as WEB_STARTUP_SERVICE. Flag-configured rows inject that service (resolving after it exists) — e.g. the webserver row's host/port defaults and the connection row's trust fence:

ts
context: `host: 127.0.0.1, port: 3080 (webStartup overrides), trustedHosts: ctx.webRuntime.trustedHosts, openBrowser: true`

--host 0.0.0.0 is rejected loudly — exposing RCE to the network is indefensible; --port 0 lets the OS pick one. After the server binds, the web runtime samples LAN IPv4 literals once (resolveLanTrust), publishes webRuntime, and — when the Loader tree settles — prints the readiness URL line. The printed URL is the authenticated URL: dsh web: <origin>/?token=<launchToken> (plus (LAN: …) for an all-interfaces bind) — the one URL a browser can open without a cookie (see packages/bundle/web-app/src/index.ts:261-280). Unless --no-open was passed it also opens that URL in the default browser. The host additionally sets the DSH_WEB_URL shell variable (a bash variable pointing at the canonical local URL) for session prompts. Supervisors and the keyless CLI smoke RPC as soon as they observe the URL line.

Structured index taps and the readiness tail ​

Every index.html response from frontend-static goes through ctx.webServer.renderIndex, which runs the registered injection rows (IndexInjection values) in composition order:

  1. client-modules boot-injection tap — bootInjections(graph) (packages/client/modules/src/index.ts) emits structured rows: an inline <script> installing the __ModuleLoader__ queue facade in <head>, script-preload rows for application-phase bundles, blocking script-src rows for bootstrap-phase bundles, and a { kind: 'global', name: '__DSH_BOOT__', value: graph } row. The escaping discipline stays: plugin-controlled strings cannot break out of the script element.
  2. ui-theme boot-theme tap — drops a <script> right after <body> that resolves system (and the stored fontSize), writing colorScheme + data-ds-dark-theme + the CSS variable --dsh-content-font-size so first paint is correctly themed.

The renderer then appends the __DSH_BOOT_READY__ tail — a <script> that resolves a Promise.withResolvers() deferred (globalThis.__DSH_BOOT_READY__) — awaited by the boot kernel after every row has taken effect (see below). Deep-route misses that would 404 anyway are irrelevant; served pages' relative asset URLs are anchored with a <base href="/"> splice.

txt
dsh web (--host --port --trusted-host --no-open)
   └─ @deepseek-ai/dsh-web-app            resolve dist → frontend-static; print authenticated URL
        └─ @deepseek-ai/dsh-host-webserver  fallback seat
             └─ authorizeIndex (401 / token→cookie 303)
                  └─ renderIndex → structured IndexInjection rows + __DSH_BOOT_READY__ tail

The boot sequence ​

AppWebEntry.run() (packages/client/web/src/boot.ts, a plain .ts — the old boot.tsx/app-shell.ts/AppRoot.tsx are deleted) is the shell kernel. Stage order:

  1. Wait on the boot-readiness gate — await __DSH_BOOT_READY__.promise (boot.ts:54): the served index resolves it in the rendered tail, so the await returns on the next microtask; an asynchronous bootstrap resolves it after its last row.
  2. Parse window.__DSH_BOOT__ into a two-view BootManifest (module rows + plugin rows).
  3. Build ClientModuleSystem over the module rows with the platform staticModules from seed.ts — the react family, cordis, dsh-client-store, ui-slots, ui-primitives. There is no app-shell module anymore, and no ui-attachment/schema-form seed words; the shell registers only itself (@deepseek-ai/dsh-client-web) and client-modules as static rows.
  4. Render the framework-free BootPage immediately — a shell self-sufficiency rule: the page must work while plugins load.
  5. new Context(), mount the vendored Cordis Loader, inject the module system as loader.internal, create one loader entry per plugin row, prefetch immediately rows, then loader.await().
  6. A full fiber sweep (assertEntriesActive) fails loud listing which entry is pending (waiting on a missing service) or failed.
  7. Hand the mount point to the renderer — ctx.inject(['uiRenderer'], …) → scope.uiRenderer.mount(container) (boot.ts mountApp). The renderer hydrates the kernel-owned boot DOM through BootHandoff (packages/client/ui-renderer/src/client/index.ts): React's hydrateRoot preserves the data-dsh-boot markup until a useLayoutEffect flips it to the assembled application — no flash, and a clean single-pass swap.

Rows marked immediately in the boot graph are prefetched in parallel with Loader mounting (factory registration only), so the cross-package synchronous require edges can resolve before any entry materializes; per-row prefetch failures stay silent because the create-side import reloads and reports them.

The AppWebEntry seam for tests ​

AppWebEntry takes an optional BootSeams object (a Pick<ClientModuleCreateOptions, 'loadBundle'>) that lets a jsdom-enabled test replace the <script> bundle-transport hook with an in-process stand-in. Production passes no seams and uses the default same-origin combo-URL <script src> loader. Everything else in the kernel — parseBootManifest, ClientModuleSystem, BootPage, staticModules — is testable against the same __DSH_BOOT__ wire because the boot is fully decoupled from any real host until connection.start is called.

This is also why apps/web/src/main.ts can be three lines: the shell library owns the boot; the app owns finding the mount node.

How plugins add UI ​

Plugins add UI entirely through slots + modules — the shell has no knowledge of any specific feature. A plugin entry (owning _apply on a Cordis context) calls ctx.slots.register({ name: '…', … }, Component); the renderer composes its props and mounts it when the slot's outlet renders root. The root slot's sole occupant is ui-layout's AppFrame, which then renders its sidebar/conversation/details/shell.overlay children. So "adding UI" usually means picking a declared seat (e.g. a new conversation.chat.node key or a shell.overlay id) and registering — see UI modules.

The fail-loud boot page ​

BootPage (packages/client/web/src/boot-page.ts) is a pure kernel component with zero plugin dependencies — the fail-loud presentation must not depend on the system whose failure it reports. It subscribes to Loader fiber states; before settlement it renders a loading card (wordmark + spinner + per-entry labels via loader-status.ts), and on failure or an expired boot-readiness promise it lists every entry whose fiber is failed plus the error, staying on the boot page — there is no partial UI.

The success path is one handoff: when uiRenderer.mount runs, the renderer's slot tree renders root, and the assembled application replaces the boot DOM in that single hydration pass.

The Vite build in detail ​

apps/web/vite.config.ts does four notable things:

  1. Rejects standalone serving — rejectStandaloneServe throws unless the bundle is served by dsh web (which injects window.__DSH_BOOT__), so a bare dev server can never expose a boot-manifest-free shell.
  2. Hashes every workspace module — the shell bundle is the only workspace code Vite compiles: plugin packages are never bundled here (shell self-sufficiency); they arrive at runtime as ./client.js bundles through the module system.
  3. Manual vendor chunks — math (KaTeX), syntax highlight (shiki), and markdown (micromark/mdast) go into vendor (with assets/langs/ for lazy grammars, assets/fonts/ for KaTeX); the three boot grammars (typescript, shellscript, json) ride vendor so the initial load stays small.
  4. Removed the workspace source-alias list — workspace packages are now consumed as built lib products through their own package.json exports; the only alias that remains stubs node:module with a throwing browser stand-in (./src/node-module-stub.ts), and process.versions.node/process.execArgv/CORDIS_SHARED are pinned to keep the vendored loader's Node probes inert in the browser.

The built dist/ therefore contains index.html, assets/index-*.js, assets/vendor-*.js, assets/langs/*.js (lazy Shiki grammars), assets/fonts/*.{woff2,woff,ttf} (KaTeX faces), and the public/ passthroughs manifest.webmanifest + favicon.svg. Because only the index chunk re-hashes when shell source changes, returning clients keep the cached vendor chunk — the manual-chunk split is a cache discipline as much as a code organization.

Plugin bundles, by contrast, are served under the combo URL /plugins/??<pkg>/client.js,<pkg2>/client.js&rev=<hash> by the client-modules node half with cache-control: public, max-age=31536000, immutable (IMMUTABLE_CACHE); the per-plugin /plugins/<id>/client.js URL is reserved for HMR and source maps. The rev is the cache-buster: a rebuilt bundle (via pnpm run dev:web + the HMR chain) gets a fresh graph rev, so the browser never sits on a stale plugin. Revs converge through clientModules.rebuilt(id) and cascade to the page over the HMR SSE channel — see Client runtime.

Theming ​

@deepseek-ai/dsh-client-ui-theme owns a durable ui-theme settings section — preference (light | dark | system, default system) and the new fontSize preference (default 14, bounds 12–17) — plus a browser ThemeRuntime that resolves system via matchMedia('(prefers-color-scheme: dark)'). The base palette is tokenized CSS custom properties (--dsw-alias-*) in packages/client/ui-theme/src/styles/.

Before the plugin tree activates, the host injects a boot theme script into every index.html response (boot-theme.ts): it reads the durable preference + font size from the Host, resolves system in the browser, and writes document.documentElement.style.colorScheme, body[data-ds-dark-theme], and document.body.style.setProperty('--dsh-content-font-size', '<n>px') — the attrs that select the dark palette and the content font size.

Once the client is up, ui-layout's ThemePresenter (theme-presenter.ts) projects each resolved ThemeSnapshot onto the document: root color-scheme, the dark attribute from active.colorScheme (never the id), alias-token overrides as inline CSS variables on body, and one presenter-owned meta[name="theme-color"]. Third-party themes register { id, colorScheme, tokens } or stack overrideTokens(source, {token: {light,dark}}). The Appearance and FontSize rows (AppearanceRow.tsx, FontSizeRow) are settings.general.item entries (orders 10 and 11).

The token model and dark mode ​

Base variable sheets live in packages/client/ui-theme/src/styles/ (base.css, design-platform.css, scrollbar.css, shiki.css, gradient-shadow-text.css). The design system is two-palette token CSS: light and dark palettes both carry the same --dsw-alias-* token names, and body[data-ds-dark-theme] selects the dark values with no class-name rewrites. Override layers (theme packs or model-authored skinning) add a third axis on top — inline --dsw-alias-* variables on body whose {light, dark} pair is chosen by the active colorScheme. Because tokens are the only style currency, a theme that re-paints the app never touches component CSS — it registers tokens.

The boot <script> guarantees the first paint is already correctly colored and sized (no flash of light-theme before the plugin tree resolves system), while ThemeRuntime's prefers-color-scheme listener re-emits whenever the OS scheme flips while the preference is system. DSH_CLIENT_BUILD_PROFILE='official' additionally activates ui-brand-official's occupants for sidebar.brand.mark/sidebar.brand.name (fish-mark fallback otherwise).

Packages in this section ​

Package
@deepseek-ai/dsh-web-frontend (apps/web)
@deepseek-ai/dsh-client-web
@deepseek-ai/dsh-client-store
@deepseek-ai/dsh-client-ui-renderer
@deepseek-ai/dsh-client-ui-theme
@deepseek-ai/dsh-client-ui-layout
@deepseek-ai/dsh-client-ui-sidebar
@deepseek-ai/dsh-web-app (bundle/web-app)
@deepseek-ai/dsh-host-frontend-static
@deepseek-ai/dsh-host-webserver
@deepseek-ai/cordis (vendored)

Further reading ​