Merge branch 'master' into worktree/dsh-arg-parser

Integrate the Commander argument adapter with master's safe session-resume
feature and dsh web --dev flag.

- args.ts: add --dev to the web parser.
- tui.ts: keep master's process.execve in-place resume handoff, but take the
  adapter's parsed (config, resume); inject the resume id through boot's
  prepare(ctx) hook via ctx.provide(RESUME_SESSION_ID_KEY, id) instead of the
  RESUME_SESSION_ID env var; rebuild the re-exec argv as `dsh --resume <id>`.
- app-boot: drop master's replaceResumeArg (no longer needed) alongside the
  already-removed parseResumeArg; add RESUME_SESSION_ID_KEY.
- the four tui-agent/cordis configs read the ctx-provided resumeSessionId via a
  typeof-guarded !!js expression, so resume needs no env var.
- web.ts: keep master's client roster and --dev watch, take parsed host/port/dev.
This commit is contained in:
Turtle
2026-07-25 12:02:28 +08:00
1479 changed files with 60394 additions and 10691 deletions
+59 -6
View File
@@ -10,30 +10,83 @@ import { mountWebPlugins, startHost } from '@deepseek-ai/dsh-host-runtime'
import { createHostWebPluginRegistry, startWebServer } from '@deepseek-ai/dsh-host-webserver'
import { ALL_INTERFACES_HOST, LOOPBACK_HOST } from './args.ts'
// --- 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.
/**
* Serve the browser UI. Host and port are already validated by the argument
* adapter (host constrained to loopback/all-interfaces, port a 065535 integer).
* 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
/**
* Serve the browser UI. Host, port, and dev are already validated by the
* argument adapter (host constrained to loopback/all-interfaces, port a
* 065535 integer).
* @param hostAddress - the bind host: {@link LOOPBACK_HOST} or {@link ALL_INTERFACES_HOST}.
* @param port - the listen port; `0` lets the OS choose a free port.
* @param dev - mount the client HMR driver and watch plugin bundles for rebuilds.
*/
export async function runWeb(hostAddress: string, port: number): Promise<void> {
export async function runWeb(hostAddress: string, port: number, dev: boolean): Promise<void> {
// A missing DEEPSEEK_API_KEY throws here (plugin load is fail-loud, uncaught by design).
const host = await startHost({
boot: {
persistenceRoot: './.sessions',
workspaceContext: { maxBytes: 65_536 },
sessionTitleLlm: true,
},
})
// 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, ...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`) },
...dev ? { watch: { intervalMs: CLIENT_BUNDLE_POLL_MS } } : {},
})
if (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)