2026-07-19 21:17:57 +08:00
/**
2026-07-25 14:37:57 +08:00
* `dsh web` — thin bin over the config-tree boot: run AppCLIEntry with the
* already-parsed host/port/dev, print the URL line, wire signals. All
2026-07-29 21:59:44 +08:00
* composition lives in the shared base plus Web overlay; all boot glue lives in AppCLIEntry. Host and
2026-07-25 16:19:02 +08:00
* port are unvalidated pass-through overrides — the `dsh-host-webserver` schema
* gates them at boot.
2026-07-19 21:17:57 +08:00
*/
2026-07-25 01:19:47 +08:00
import { fileURLToPath } from 'node:url'
2026-07-28 17:20:20 +08:00
import type { Context } from 'cordis'
2026-08-01 22:04:10 +08:00
import { addHarnessSourceSection , resolveConfigPath } from '@deepseek-ai/dsh-app-boot'
2026-08-02 00:56:14 +08:00
import type { } from '@deepseek-ai/dsh-host-webserver'
2026-07-28 17:20:20 +08:00
import type { } from '@deepseek-ai/dsh-system-prompt'
2026-08-02 14:18:42 +08:00
import type { } from '@deepseek-ai/dsh-bash-env'
2026-07-25 01:19:47 +08:00
import { AppCLIEntry } from './app-cli-entry.ts'
2026-08-03 22:08:47 +08:00
import { createProcessShutdown } from './process-shutdown.ts'
2026-07-19 21:17:57 +08:00
2026-08-04 10:07:17 +08:00
// The shipped base plus the Web application's overlay.
2026-07-30 10:11:38 +08:00
const BASE_CONFIG = fileURLToPath ( new URL ( '../config/base.cordis.yml' , import . meta . url ) )
const WEB_OVERLAY = fileURLToPath ( new URL ( '../config/web.cordis.yml' , import . meta . url ) )
2026-07-28 17:20:20 +08:00
const SOURCE_ROOT = fileURLToPath ( new URL ( '../../..' , import . meta . url ) )
2026-07-28 18:04:21 +08:00
const DSH_WEB_URL = 'DSH_WEB_URL' as const
2026-07-29 11:22:48 +08:00
const DSH_WEB_MODE = 'DSH_WEB_MODE' as const
type WebMode = 'production' | 'development'
2026-07-28 18:04:21 +08:00
2026-08-02 00:56:14 +08:00
// Display-only mirror of the webserver schema's loopback host: the address the
// local URL always prints. Not a source of truth — the schema is.
const LOOPBACK_HOST = '127.0.0.1'
2026-07-28 18:04:21 +08:00
/** Model-visible orientation and acceptance boundary for sessions created through `dsh web`. */
2026-07-29 11:22:48 +08:00
function webSurfacePrompt ( webUrl : string , mode : WebMode ) : string {
const updateContract = mode === 'development'
? 'This Web process was launched with `dsh web --dev`, so its client-plugin HMR receiver is active. '
+ 'No-refresh updates occur only when `pnpm run dev:web` is also running from this same checkout to rebuild client-plugin bundles; verify that watcher before promising automatic updates. '
+ 'Client-plugin changes then reload automatically, while apps/web shell and other plain-package changes still require a rebuild and page refresh. '
: 'This Web process was launched without `--dev`, so HMR is inactive: rebuild the affected Web artifacts and verify this existing URL after a page refresh. '
+ 'If the user wants no-refresh client-plugin updates, explain that this GUI must be restarted with `dsh web --dev` and `pnpm run dev:web` must also run from this same checkout; do not present either command alone as sufficient. '
2026-07-28 18:04:21 +08:00
return ` You are interacting with the user through the DeepSeek Harness Web GUI at ${ webUrl } . `
+ 'When the user refers to "this page", "this GUI", or "this app" without naming another target, they mean this GUI. '
+ 'The browser provides no implicit DOM, route, or screenshot context. '
2026-07-29 11:22:48 +08:00
+ updateContract
+ 'Starting another server does not update this GUI. '
2026-07-28 18:04:21 +08:00
+ 'The apps/web Vite entry builds the shell but is not a standalone application because only dsh web injects window.__DSH_BOOT__. '
+ 'Do not start a replacement server unless the user asks; if one is needed, use a managed background task and verify its exact URL.'
}
2026-07-28 17:20:20 +08:00
2026-08-02 00:56:14 +08:00
/** Resolve the canonical loopback URL from the active Web server. */
function localWebUrl ( ctx : Context ) : string {
const port = ctx . get ( 'httpServer' ) ? . port
if ( port === undefined ) throw new Error ( 'dsh web: httpServer service missing while resolving Web runtime' )
return ` http:// ${ LOOPBACK_HOST } : ${ String ( port ) } `
}
2026-07-28 17:20:20 +08:00
/**
2026-08-02 00:56:14 +08:00
* Register the launcher-owned prompt and shell runtime context before the
* shared config tree mounts. The earlier injections install the prompt
* sections and managed Bash contributor when their owning services activate;
* dynamic values read the bound server only when consumed.
2026-08-02 00:49:58 +08:00
* @param ctx - Web root context with Loader installed but no config tree mounted.
2026-07-28 17:20:20 +08:00
* @param sourceRoot - absolute checkout root resolved from the launcher module.
2026-07-29 11:22:48 +08:00
* @param mode - whether this process mounted the client-plugin HMR receiver.
2026-07-28 17:20:20 +08:00
*/
2026-08-02 00:56:14 +08:00
export function prepareWebRuntimeContext ( ctx : Context , sourceRoot : string , mode : WebMode ) : void {
2026-08-02 00:49:58 +08:00
ctx . inject ( [ 'systemPrompt' ] , ( promptCtx ) = > {
addHarnessSourceSection ( promptCtx , sourceRoot )
2026-08-02 00:56:14 +08:00
promptCtx . systemPrompt . section ( {
name : 'app:web-surface' ,
order : - 98 ,
text : ( ) = > webSurfacePrompt ( localWebUrl ( promptCtx ) , mode ) ,
} )
} )
ctx . inject ( [ 'bashEnv' ] , ( runtimeCtx ) = > {
runtimeCtx . bashEnv . register ( {
name : 'web-runtime' ,
variables : {
[ DSH_WEB_URL ] : { description : 'Canonical local URL of the DeepSeek Harness Web GUI serving this session.' } ,
[ DSH_WEB_MODE ] : { description : 'Web runtime mode: production, or development when the client-plugin HMR receiver is active.' } ,
} ,
resolve : ( ) = > ( { [ DSH_WEB_URL ] : localWebUrl ( runtimeCtx ) , [ DSH_WEB_MODE ] : mode } ) ,
} )
2026-07-28 18:04:21 +08:00
} )
2026-07-28 17:20:20 +08:00
}
2026-07-23 21:56:18 +08:00
2026-07-24 19:43:59 +08:00
/**
2026-07-25 15:03:17 +08:00
* Serve the browser UI from the shipped config tree. `host`/`port` are passed
2026-07-29 21:59:44 +08:00
* through only when the flag was given; absent, the shipped Web overlay value stands.
2026-07-25 16:19:02 +08:00
* @param host - the bind host, or `undefined` to keep the config default.
2026-07-25 15:03:17 +08:00
* @param port - the listen port (`0` requests an OS-assigned port), or `undefined` to keep the config default.
2026-07-29 11:22:48 +08:00
* @param dev - mount the client HMR receiver; `pnpm run dev:web` separately rebuilds watched plugin bundles.
2026-07-25 18:05:39 +08:00
* @param workspaceRoot - parent directory for name-created workspaces, or `undefined` for the gateway's cwd fallback.
2026-07-28 15:40:02 +08:00
* @param trustedHosts - extra authorities for the /api browser-trust fence, or `undefined` for the derived LAN literals alone.
2026-07-29 15:28:38 +08:00
* @param config - an overlay of loader patches applied over the shipped web
2026-07-29 23:36:58 +08:00
* composition instead of `$DSH_HOME/config.yaml`, or `undefined` to use the
* personal overlay; already parsed from `--config`.
2026-07-24 19:43:59 +08:00
*/
2026-07-25 18:05:39 +08:00
export async function runWeb (
host : string | undefined ,
port : number | undefined ,
dev : boolean ,
workspaceRoot : string | undefined ,
2026-07-28 15:40:02 +08:00
trustedHosts : string [ ] | undefined ,
2026-07-29 15:28:38 +08:00
config? : string ,
2026-07-25 18:05:39 +08:00
) : Promise < void > {
2026-08-02 00:56:14 +08:00
const mode : WebMode = dev ? 'development' : 'production'
2026-07-25 15:03:17 +08:00
const entry = new AppCLIEntry ( {
2026-07-29 15:28:38 +08:00
configPath : BASE_CONFIG ,
overlayPath : WEB_OVERLAY ,
. . . config !== undefined && { extraOverlayPath : resolveConfigPath ( config , undefined ) } ,
2026-07-25 15:03:17 +08:00
dev ,
2026-08-02 00:56:14 +08:00
prepare : ( ctx ) = > { prepareWebRuntimeContext ( ctx , SOURCE_ROOT , mode ) } ,
2026-07-30 06:00:53 +08:00
watchPersonalConfig : true ,
2026-07-25 15:03:17 +08:00
. . . host !== undefined && { host } ,
. . . port !== undefined && { port } ,
2026-07-25 18:05:39 +08:00
. . . workspaceRoot !== undefined && { workspaceRoot } ,
2026-07-28 15:40:02 +08:00
. . . trustedHosts !== undefined && { trustedHosts } ,
2026-07-25 15:03:17 +08:00
} )
2026-07-25 01:19:47 +08:00
const { ctx , port : boundPort } = await entry . run ( )
2026-08-02 00:56:14 +08:00
const resolvedLocalWebUrl = localWebUrl ( ctx )
2026-07-19 21:17:57 +08:00
2026-08-03 22:08:47 +08:00
const shutdown = createProcessShutdown ( async ( ) = > { await ctx . fiber . dispose ( ) } )
2026-07-19 21:17:57 +08:00
2026-07-27 15:49:43 +08:00
// Install shutdown handling before publishing readiness: supervisors may
// send a signal as soon as they observe the URL line.
2026-08-03 22:08:47 +08:00
process . on ( 'SIGTERM' , ( ) = > { shutdown . interrupt ( 0 ) } )
process . on ( 'SIGINT' , ( ) = > { shutdown . interrupt ( 130 ) } )
2026-07-27 15:49:43 +08:00
2026-07-28 17:47:56 +08:00
// The entry's boot-time snapshot, not a fresh sample: the printed LAN URL
// must name an address the /api trust fence was configured with.
const lanCandidate = entry . lanAddresses [ 0 ]
2026-08-02 00:56:14 +08:00
console . log ( ` dsh web: ${ resolvedLocalWebUrl } ${ lanCandidate === undefined ? '' : ` (LAN: http:// ${ lanCandidate } : ${ boundPort } ) ` } ` )
2026-07-19 21:17:57 +08:00
}