mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
refactor(cli): exclude live-session registry surface
This commit is contained in:
@@ -2,24 +2,19 @@
|
||||
* Commander adapter for the `dsh` command-line entry: the one place argv is
|
||||
* parsed and routed to a mode. `bin.ts` switches on the returned discriminant
|
||||
* and dynamic-imports that mode's module. One program: the default (no
|
||||
* subcommand) is the TUI/headless surface with option-only flags; `meta` and
|
||||
* `web` are real subcommands. Commander owns `--help`/`--version` and parse
|
||||
* errors — it prints and exits at the point of failure (a domain failure routes through
|
||||
* subcommand) is the TUI/headless surface with option-only flags; `web` is a
|
||||
* real subcommand. Commander owns `--help`/`--version` and parse errors — it
|
||||
* prints and exits at the point of failure (a domain failure routes through
|
||||
* `command.error`), so this returns only a resolved mode.
|
||||
* @module @deepseek-ai/dsh/args
|
||||
*/
|
||||
|
||||
import { Command, CommanderError } from 'commander'
|
||||
|
||||
/**
|
||||
* Interactive TUI: the default mode. `--config` applies an overlay over the
|
||||
* shipped composition in place of the personal one, `--config-replace` boots a
|
||||
* file as the whole tree instead, and `--resume <id>` rehydrates a session.
|
||||
*/
|
||||
/** Interactive TUI: the default mode. `--config` swaps the tree; `--resume <id>` rehydrates a session. */
|
||||
interface TuiInvocation {
|
||||
mode: 'tui'
|
||||
config?: string
|
||||
configReplace?: string
|
||||
resume?: string
|
||||
}
|
||||
|
||||
@@ -29,39 +24,6 @@ interface HeadlessInvocation {
|
||||
prompt: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Interactive TUI over this harness checkout: `dsh meta`. Identical to
|
||||
* {@link TuiInvocation} except the workspace is the launcher's own source tree
|
||||
* rather than the invoking directory. No `--config`: booting a foreign tree
|
||||
* against the harness workspace is the `--config` case, not this one.
|
||||
*/
|
||||
interface MetaInvocation {
|
||||
mode: 'meta'
|
||||
resume?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Guided fresh-session entries: `dsh migrate` seeds the first turn with the
|
||||
* `dsh-migrate` skill, `dsh upgrade` with `dsh-upgrade`. Each always mints a
|
||||
* fresh session in the invoking directory and takes no options — `--resume`,
|
||||
* `--config`, and `-p` are rejected as mistyped, so there is nothing to carry.
|
||||
*/
|
||||
interface SkillSessionInvocation {
|
||||
mode: 'migrate' | 'upgrade'
|
||||
}
|
||||
|
||||
/**
|
||||
* List live sessions: `dsh list-sessions` (alias `dsh ps`). A read-only surface
|
||||
* that boots no agent tree — it reads the cross-process session registry and
|
||||
* exits. `json` selects the machine-readable form over the human table. There
|
||||
* is no workspace filter: the listing is always every live session, whatever
|
||||
* directory it runs in.
|
||||
*/
|
||||
interface ListSessionsInvocation {
|
||||
mode: 'list-sessions'
|
||||
json: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Browser UI: `dsh web`. `host`/`port` are present only when the flag was
|
||||
* passed — pass-through overrides with no CLI default and no CLI validation:
|
||||
@@ -74,8 +36,6 @@ interface ListSessionsInvocation {
|
||||
*/
|
||||
interface WebInvocation {
|
||||
mode: 'web'
|
||||
/** Overlay of loader patches applied over the shipped web composition. */
|
||||
config?: string
|
||||
host?: string
|
||||
port?: number
|
||||
dev: boolean
|
||||
@@ -85,17 +45,10 @@ interface WebInvocation {
|
||||
}
|
||||
|
||||
/** The resolved `dsh` invocation: exactly one mode. `--help`/`--version`/errors exit inside {@link parseDshArgs}. */
|
||||
export type DshInvocation =
|
||||
| TuiInvocation
|
||||
| HeadlessInvocation
|
||||
| MetaInvocation
|
||||
| SkillSessionInvocation
|
||||
| ListSessionsInvocation
|
||||
| WebInvocation
|
||||
export type DshInvocation = TuiInvocation | HeadlessInvocation | WebInvocation
|
||||
|
||||
/** Raw web-subcommand options straight from Commander. */
|
||||
interface WebOptions {
|
||||
config?: string
|
||||
host?: string
|
||||
port?: string
|
||||
dev?: boolean
|
||||
@@ -112,7 +65,6 @@ interface WebOptions {
|
||||
function resolveWeb(options: WebOptions): WebInvocation {
|
||||
return {
|
||||
mode: 'web',
|
||||
...options.config !== undefined && { config: options.config },
|
||||
...options.host !== undefined && { host: options.host },
|
||||
...options.port !== undefined && { port: Number(options.port) },
|
||||
dev: options.dev === true,
|
||||
@@ -134,31 +86,21 @@ export function parseDshArgs(argv: readonly string[], version: string): DshInvoc
|
||||
const program = new Command()
|
||||
.name('dsh')
|
||||
.version(version, '-V, --version', 'output the version number')
|
||||
.description('dsh: DeepSeek Harness — an interactive coding agent for your terminal.\nRun `dsh` with no arguments to start a session in the current directory.')
|
||||
// The default surface takes no positional task, so `dsh "task"` fails
|
||||
// commander's arity check with no hint; these examples are where a first
|
||||
// reader learns the entry points and that a one-shot task rides `-p`.
|
||||
.addHelpText('after', `
|
||||
Examples:
|
||||
dsh start an interactive session in this directory
|
||||
dsh -p "run the tests" answer one task, print the result, and exit
|
||||
dsh --resume <id> continue a past session (list ids with \`dsh ps\`)
|
||||
`)
|
||||
.description('dsh: interactive TUI (default), headless task, and browser UI')
|
||||
.exitOverride()
|
||||
// Default surface: option-only (no positional), so `web` can be a real
|
||||
// subcommand without a positional collision.
|
||||
.option('-p, --prompt <task>', 'answer this task without the interactive UI, then exit')
|
||||
.option('--resume <id>', 'continue a past session by id (list ids with `dsh ps`)')
|
||||
.option('--config <path>', 'apply this overlay of loader patches instead of the personal one')
|
||||
.option('--config-replace <path>', 'boot this file as the entire tree, ignoring the shipped and personal configuration')
|
||||
.action((options: { config?: string; configReplace?: string; prompt?: string; resume?: string }) => {
|
||||
.option('--config <path>', 'boot an alternate cordis.yml instead of the shipped tree (TUI mode)')
|
||||
.option('-p, --prompt <task>', 'run one headless turn for this task, print the result, and exit')
|
||||
.option('--resume <id>', 'resume the persisted session with this id (TUI mode)')
|
||||
.action((options: { config?: string; prompt?: string; resume?: string }) => {
|
||||
if (options.prompt !== undefined) {
|
||||
// A headless prompt owns the invocation; an empty task has nothing to
|
||||
// run, and --config/--resume are TUI inputs that must not silently
|
||||
// vanish from a headless run.
|
||||
if (options.prompt === '') program.error('error: --prompt needs a task')
|
||||
if (options.config !== undefined || options.configReplace !== undefined || options.resume !== undefined) {
|
||||
program.error('error: --prompt takes no --config, --config-replace, or --resume')
|
||||
if (options.config !== undefined || options.resume !== undefined) {
|
||||
program.error('error: --prompt takes no --config or --resume')
|
||||
}
|
||||
resolved = { mode: 'headless', prompt: options.prompt }
|
||||
return
|
||||
@@ -166,97 +108,30 @@ Examples:
|
||||
// An empty --resume= id would silently start a fresh session downstream
|
||||
// (agent-loop treats '' as no-resume), so a mistyped resume must fail loud.
|
||||
if (options.resume === '') program.error('error: --resume needs a session id')
|
||||
// The two config flags are mutually exclusive: one layers over the shipped
|
||||
// tree, the other discards it, so accepting both would silently drop one.
|
||||
if (options.config !== undefined && options.configReplace !== undefined) {
|
||||
program.error('error: --config and --config-replace are mutually exclusive')
|
||||
}
|
||||
resolved = {
|
||||
mode: 'tui',
|
||||
...options.config !== undefined && { config: options.config },
|
||||
...options.configReplace !== undefined && { configReplace: options.configReplace },
|
||||
...options.resume !== undefined && { resume: options.resume },
|
||||
}
|
||||
})
|
||||
|
||||
// Commander parses the parent (default-surface) options on either side of a
|
||||
// subcommand into `program.opts()`. For a subcommand that shares none of them,
|
||||
// a leaked `--config`/`-p`/`--resume` is a mistyped invocation that must fail
|
||||
// loud rather than silently run and drop the input.
|
||||
const rejectParentOptions = (command: string): void => {
|
||||
const parent = program.opts<{ config?: string; configReplace?: string; prompt?: string; resume?: string }>()
|
||||
if (parent.config !== undefined || parent.configReplace !== undefined
|
||||
|| parent.prompt !== undefined || parent.resume !== undefined) {
|
||||
program.error(`error: ${command} takes none of --config, -p/--prompt, or --resume`)
|
||||
}
|
||||
}
|
||||
|
||||
// Registration order is the rendered help order, so daily use comes first
|
||||
// and the harness-development surfaces (`web --dev`, `meta`) come last.
|
||||
// `migrate` and `upgrade` are guided fresh-session entries: they take no
|
||||
// options and always mint a fresh session, so nothing is left to carry. Each
|
||||
// description names the outcome, not the skill the first turn invokes.
|
||||
const guided = {
|
||||
migrate: 'import settings from another coding agent (Claude Code, Codex, opencode)',
|
||||
upgrade: 'update this dsh installation to the latest version',
|
||||
} as const
|
||||
for (const mode of ['migrate', 'upgrade'] as const) {
|
||||
program
|
||||
.command(mode)
|
||||
.description(guided[mode])
|
||||
.action(() => {
|
||||
rejectParentOptions(mode)
|
||||
resolved = { mode }
|
||||
})
|
||||
}
|
||||
|
||||
program
|
||||
.command('list-sessions')
|
||||
.alias('ps')
|
||||
.description('list sessions running right now')
|
||||
.option('--json', 'print the records as a JSON array instead of a table')
|
||||
.action((options: { json?: boolean }) => {
|
||||
rejectParentOptions('list-sessions')
|
||||
resolved = { mode: 'list-sessions', json: options.json === true }
|
||||
})
|
||||
|
||||
// Host and port name no default: the CLI passes neither through when the flag
|
||||
// is absent, so the shipped `cordis.yml` value stands and restating it here
|
||||
// would duplicate a fact this file does not own.
|
||||
const web = program.command('web').description('serve the browser UI on the configured host and port')
|
||||
const web = program.command('web').description('serve the browser UI (host/port default to the shipped config)')
|
||||
web
|
||||
.option('--config <path>', 'apply this overlay of loader patches over the shipped configuration')
|
||||
.option('--host <host>', 'bind host; pass 0.0.0.0 to reach it from another machine')
|
||||
.option('--port <port>', 'listen port; pass 0 to let the OS pick a free one')
|
||||
.option('--dev', 'developer mode: hot-reload the browser client')
|
||||
.option('--workspace-root <path>', 'parent directory for workspaces created from the browser UI')
|
||||
.option('--host <host>', 'override the config bind host (127.0.0.1 or 0.0.0.0)')
|
||||
.option('--port <port>', 'override the config listen port (0 requests an OS-assigned port)')
|
||||
.option('--dev', 'mount the client HMR driver and watch plugin bundles for rebuilds')
|
||||
.option('--workspace-root <path>', 'parent directory for name-created workspaces')
|
||||
.option('--trusted-host <authority...>', 'extra authority the /api browser-trust fence accepts (host or host:port; repeatable)')
|
||||
.action((options: WebOptions) => {
|
||||
rejectParentOptions('web')
|
||||
resolved = resolveWeb(options)
|
||||
})
|
||||
|
||||
// `--resume` is NOT redeclared here: an option a subcommand shares with its
|
||||
// parent parses into `program.opts()` and leaves the subcommand's own options
|
||||
// empty, so redeclaring it would silently drop the id. Commander therefore
|
||||
// omits it from this subcommand's option list, hence the trailing help text.
|
||||
program
|
||||
.command('meta')
|
||||
.description('work on the dsh source that runs this command, from any directory')
|
||||
.addHelpText('after', '\nAccepts --resume <id> to resume a persisted session from this checkout.\n')
|
||||
.action(() => {
|
||||
// Commander parses the parent (default-surface) options on either side of
|
||||
// the subcommand into `program.opts()`. `meta` accepts only `--resume`, so
|
||||
// a leaked `--config`/`-p` is a mistyped invocation that must fail loud
|
||||
// rather than silently be dropped.
|
||||
// the subcommand into `program.opts()`. `web` shares none of them, so a
|
||||
// leaked `--config`/`-p`/`--resume` is a mistyped invocation that must
|
||||
// fail loud rather than silently start the web server and drop it.
|
||||
const parent = program.opts<{ config?: string; prompt?: string; resume?: string }>()
|
||||
if (parent.config !== undefined || parent.prompt !== undefined) {
|
||||
program.error('error: meta takes neither --config nor -p/--prompt')
|
||||
if (parent.config !== undefined || parent.prompt !== undefined || parent.resume !== undefined) {
|
||||
program.error('error: web takes none of --config, -p/--prompt, or --resume')
|
||||
}
|
||||
// Same reason as the default surface: an empty id would start a fresh
|
||||
// session downstream instead of failing the mistyped resume.
|
||||
if (parent.resume === '') program.error('error: --resume needs a session id')
|
||||
resolved = { mode: 'meta', ...parent.resume !== undefined && { resume: parent.resume } }
|
||||
resolved = resolveWeb(options)
|
||||
})
|
||||
|
||||
try {
|
||||
|
||||
@@ -30,7 +30,7 @@ const invocation = parseDshArgs(process.argv.slice(2), readVersion())
|
||||
switch (invocation.mode) {
|
||||
case 'web': {
|
||||
const { runWeb } = await import('./web.ts')
|
||||
await runWeb(invocation.host, invocation.port, invocation.dev, invocation.workspaceRoot, invocation.trustedHosts, invocation.config)
|
||||
await runWeb(invocation.host, invocation.port, invocation.dev, invocation.workspaceRoot, invocation.trustedHosts)
|
||||
break
|
||||
}
|
||||
case 'headless': {
|
||||
@@ -40,23 +40,7 @@ switch (invocation.mode) {
|
||||
}
|
||||
case 'tui': {
|
||||
const { runTui } = await import('./tui.ts')
|
||||
await runTui(invocation.config, invocation.resume, undefined, undefined, invocation.configReplace)
|
||||
break
|
||||
}
|
||||
case 'meta': {
|
||||
const { runMeta } = await import('./tui.ts')
|
||||
await runMeta(invocation.resume)
|
||||
break
|
||||
}
|
||||
case 'list-sessions': {
|
||||
const { runListSessions } = await import('./list-sessions.ts')
|
||||
await runListSessions(invocation.json)
|
||||
break
|
||||
}
|
||||
case 'migrate':
|
||||
case 'upgrade': {
|
||||
const { runSkillSession } = await import('./tui.ts')
|
||||
await runSkillSession(`dsh-${invocation.mode}`)
|
||||
await runTui(invocation.config, invocation.resume)
|
||||
break
|
||||
}
|
||||
default:
|
||||
|
||||
@@ -14,7 +14,6 @@ import type { MuxFrame } from '@deepseek-ai/dsh-host-apiproxy/api'
|
||||
import type { RpcRequest, RpcResponse } from '@deepseek-ai/dsh-host-apiproxy/api/rpc'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import { AppCLIEntry } from './app-cli-entry.ts'
|
||||
import { registerLiveSessions } from './register-session.ts'
|
||||
|
||||
/** Outcome of one headless turn: aggregated final text plus the turn-end reason kind. */
|
||||
interface TurnOutcome {
|
||||
@@ -76,13 +75,11 @@ async function consumeUntilTurnEnd(frames: AsyncIterable<RpcRequest<MuxFrame>>,
|
||||
export async function runHeadless(task: string): Promise<void> {
|
||||
// A missing DEEPSEEK_API_KEY throws here (plugin load is fail-loud, uncaught by design).
|
||||
const entry = new AppCLIEntry({
|
||||
configPath: fileURLToPath(new URL('../base.cordis.yml', import.meta.url)),
|
||||
overlayPath: fileURLToPath(new URL('../web.cordis.yml', import.meta.url)),
|
||||
configPath: fileURLToPath(new URL('../cordis.yml', import.meta.url)),
|
||||
dev: false,
|
||||
port: 0,
|
||||
})
|
||||
const { ctx, port } = await entry.run()
|
||||
await registerLiveSessions(ctx)
|
||||
const dispose = async (): Promise<void> => { await ctx.fiber.dispose() }
|
||||
// The headless session is web-observable while it runs (same composition).
|
||||
process.stderr.write(`dsh: observing at http://127.0.0.1:${String(port)}\n`)
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
/**
|
||||
* `dsh list-sessions` (alias `dsh ps`) — list the sessions running right now.
|
||||
*
|
||||
* A read-only surface: it mounts the session registry alone and never boots an
|
||||
* agent tree, so listing stays fast and cannot start model work as a side
|
||||
* effect. Liveness comes from the registry, which prunes records whose process
|
||||
* is gone, and every displayed field including the title comes from the record,
|
||||
* so no session log is opened and no backend format is assumed.
|
||||
* @module @deepseek-ai/dsh/list-sessions
|
||||
*/
|
||||
|
||||
import { Context } from 'cordis'
|
||||
import { type SessionRegistryRecord } from '@deepseek-ai/dsh-session-registry'
|
||||
import SessionRegistryFile from '@deepseek-ai/dsh-session-registry-file'
|
||||
import { registryRoot } from './register-session.ts'
|
||||
|
||||
/** Column header text, also the minimum width of each column. */
|
||||
const HEADERS = ['SESSION', 'PID', 'UPTIME', 'WORKSPACE', 'TITLE'] as const
|
||||
|
||||
/** Shown when a session has no title yet. */
|
||||
const NO_TITLE = '—'
|
||||
|
||||
/**
|
||||
* Render milliseconds of uptime as a compact human duration.
|
||||
* @param ms - elapsed milliseconds since the session registered.
|
||||
* @returns a short duration such as `12s`, `4m`, or `2h14m`.
|
||||
*/
|
||||
export function formatUptime(ms: number): string {
|
||||
const seconds = Math.max(0, Math.floor(ms / 1000))
|
||||
if (seconds < 60) return `${String(seconds)}s`
|
||||
const minutes = Math.floor(seconds / 60)
|
||||
if (minutes < 60) return `${String(minutes)}m`
|
||||
const hours = Math.floor(minutes / 60)
|
||||
const remainder = minutes % 60
|
||||
if (hours < 24) return remainder === 0 ? `${String(hours)}h` : `${String(hours)}h${String(remainder)}m`
|
||||
const days = Math.floor(hours / 24)
|
||||
const leftoverHours = hours % 24
|
||||
return leftoverHours === 0 ? `${String(days)}d` : `${String(days)}d${String(leftoverHours)}h`
|
||||
}
|
||||
|
||||
/** One fully-resolved listing row, in column order. */
|
||||
type Row = readonly [string, string, string, string, string]
|
||||
|
||||
/**
|
||||
* Build the display rows for a listing, newest session first.
|
||||
* @param records - the live records to render.
|
||||
* @param now - the current epoch milliseconds uptime is measured against.
|
||||
* @returns one row per record, each already stringified per column.
|
||||
*/
|
||||
export function buildRows(records: readonly SessionRegistryRecord[], now: number): Row[] {
|
||||
return [...records]
|
||||
.sort((left, right) => right.startedAt - left.startedAt)
|
||||
.map(record => [
|
||||
record.sessionId,
|
||||
String(record.pid),
|
||||
formatUptime(now - record.startedAt),
|
||||
record.cwd,
|
||||
record.title ?? NO_TITLE,
|
||||
] as const)
|
||||
}
|
||||
|
||||
/**
|
||||
* Render rows as a left-aligned table with a header line.
|
||||
*
|
||||
* The last column is never padded, so a long title cannot add trailing
|
||||
* whitespace to every line.
|
||||
* @param rows - the rows to render, already stringified.
|
||||
* @returns the complete table text, newline-terminated.
|
||||
*/
|
||||
export function renderTable(rows: readonly Row[]): string {
|
||||
const widths = HEADERS.map((header, column) =>
|
||||
Math.max(header.length, ...rows.map(row => row[column]?.length ?? 0)))
|
||||
const line = (cells: readonly string[]): string =>
|
||||
cells.map((cell, column) => column === cells.length - 1 ? cell : cell.padEnd(widths[column] ?? 0)).join(' ').trimEnd()
|
||||
return [line(HEADERS), ...rows.map(row => line(row))].join('\n') + '\n'
|
||||
}
|
||||
|
||||
/**
|
||||
* List live sessions and exit. Prints a table by default, or a JSON array with
|
||||
* `--json`; an empty listing is a success, not an error.
|
||||
* @param json - emit the machine-readable JSON array instead of the table.
|
||||
*/
|
||||
export async function runListSessions(json: boolean): Promise<void> {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionRegistryFile, { root: registryRoot() })
|
||||
const records = await ctx.sessionRegistry.list()
|
||||
await ctx.fiber.dispose()
|
||||
|
||||
if (json) {
|
||||
const rows = [...records]
|
||||
.sort((left, right) => right.startedAt - left.startedAt)
|
||||
.map(record => ({ ...record, uptimeMs: Date.now() - record.startedAt, title: record.title ?? null }))
|
||||
process.stdout.write(`${JSON.stringify(rows, undefined, 2)}\n`)
|
||||
return
|
||||
}
|
||||
if (records.length === 0) {
|
||||
process.stdout.write('no dsh sessions running\n')
|
||||
return
|
||||
}
|
||||
process.stdout.write(renderTable(buildRows(records, Date.now())))
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
/**
|
||||
* Mounts the cross-process live-session registry that `dsh list-sessions` reads, plus the
|
||||
* publisher that keeps it in step with this process's sessions.
|
||||
*
|
||||
* Both plugins mount on the booted app's own context, so records share that
|
||||
* fiber's lifetime: an ordinary exit disposes the fiber and deregisters, while a
|
||||
* killed process leaves records the next reader prunes by pid. Only top-level
|
||||
* surfaces a user launches mount this — in-process subagents have no process of
|
||||
* their own, and out-of-process subagent backends spawn `dsh-jsonrpc-agent`
|
||||
* rather than this CLI, so neither reaches this path.
|
||||
* @module @deepseek-ai/dsh/register-session
|
||||
*/
|
||||
|
||||
import { join } from 'node:path'
|
||||
import type { Context } from 'cordis'
|
||||
import { resolveDshHome } from '@deepseek-ai/dsh-paths'
|
||||
import SessionRegistryFile from '@deepseek-ai/dsh-session-registry-file'
|
||||
import * as sessionRegistryLive from '@deepseek-ai/dsh-session-registry-live'
|
||||
|
||||
/** Registry root under the Harness home, shared by every surface and by `dsh list-sessions`. */
|
||||
export const registryRoot = (): string => join(resolveDshHome(), 'run')
|
||||
|
||||
/**
|
||||
* Publish this process's sessions for the lifetime of `ctx`.
|
||||
*
|
||||
* Publication follows session lifecycle rather than a launcher-known id, so one
|
||||
* path serves every surface identically — the TUI's single session and a
|
||||
* server's on-demand ones alike — and titles reach the listing as they are
|
||||
* logged.
|
||||
*
|
||||
* Mounting is best-effort: a registry failure must not take down a working agent
|
||||
* session, because the registry is an observability aid rather than part of the
|
||||
* agent's contract. Failures warn through the context logger.
|
||||
* @param ctx - the booted app context whose lifetime the records share.
|
||||
*/
|
||||
export async function registerLiveSessions(ctx: Context): Promise<void> {
|
||||
try {
|
||||
const scope = ctx.isolate('sessionRegistry')
|
||||
await scope.plugin(SessionRegistryFile, { root: registryRoot() })
|
||||
await scope.plugin(sessionRegistryLive)
|
||||
} catch (error) {
|
||||
ctx.logger('dsh').warn('session registry unavailable; `dsh list-sessions` will not list these sessions: %s', String(error))
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,6 @@ import { resolveDshHome, resolveSessionsRoot } from '@deepseek-ai/dsh-paths'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import { CONFIGURED_AGENT_IDENTITIES_KEY } from '@deepseek-ai/dsh-agent-loop'
|
||||
import type { Context } from 'cordis'
|
||||
import { registerLiveSessions } from './register-session.ts'
|
||||
import {
|
||||
INITIAL_SKILL_KEY,
|
||||
MAIN_SESSION_ID_KEY,
|
||||
@@ -71,8 +70,7 @@ const SOURCE_ROOT = fileURLToPath(new URL('../../..', import.meta.url))
|
||||
* shared session-store root, `sessions` under the Harness home. Shared-store
|
||||
* policy is the launcher's alone — the app bundle treats the slot as opaque and
|
||||
* keeps a project-local fallback, so only `dsh` decides that sessions are
|
||||
* shared across working directories (making `/resume` and `list-sessions` span
|
||||
* every workspace).
|
||||
* shared across working directories (making `/resume` span every workspace).
|
||||
* @returns the absolute session-store root this launcher shares.
|
||||
*/
|
||||
export function launcherSessionsRoot(): string {
|
||||
@@ -238,8 +236,8 @@ export async function runTui(
|
||||
hostCtx.provide(MAIN_SESSION_ID_KEY, identity)
|
||||
hostCtx.provide(TUI_GOODBYE_MESSAGE_KEY, goodbye)
|
||||
// Shared-store policy is the launcher's: sessions live in one root under
|
||||
// the Harness home across every cwd, so /resume and list-sessions see
|
||||
// every workspace. The bundle treats the slot as opaque.
|
||||
// the Harness home across every cwd, so /resume sees every workspace.
|
||||
// The bundle treats the slot as opaque.
|
||||
hostCtx.provide(SESSIONS_ROOT_KEY, launcherSessionsRoot())
|
||||
// The agent-loop row reads this to bind `main`, and the tui row reads the
|
||||
// same id, so a personal overlay repointing the model route cannot drop
|
||||
@@ -258,8 +256,5 @@ export async function runTui(
|
||||
)
|
||||
app.current = ctx
|
||||
addHarnessSourceSection(ctx, SOURCE_ROOT)
|
||||
// Publication follows the store; meta mode already chdir'd, so each session
|
||||
// reports its own cwd.
|
||||
await registerLiveSessions(ctx)
|
||||
}
|
||||
/* v8 ignore stop */
|
||||
|
||||
@@ -7,13 +7,9 @@
|
||||
*/
|
||||
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { resolveConfigPath } from '@deepseek-ai/dsh-app-boot'
|
||||
import { AppCLIEntry } from './app-cli-entry.ts'
|
||||
import { registerLiveSessions } from './register-session.ts'
|
||||
|
||||
// The shared core every `dsh` surface mounts, plus this surface's overlay over it.
|
||||
const BASE_CONFIG = fileURLToPath(new URL('../base.cordis.yml', import.meta.url))
|
||||
const WEB_OVERLAY = fileURLToPath(new URL('../web.cordis.yml', import.meta.url))
|
||||
const CONFIG_PATH = fileURLToPath(new URL('../cordis.yml', import.meta.url))
|
||||
|
||||
// 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.
|
||||
@@ -27,8 +23,6 @@ const LOOPBACK_HOST = '127.0.0.1'
|
||||
* @param dev - mount the client HMR driver and watch plugin bundles for rebuilds.
|
||||
* @param workspaceRoot - parent directory for name-created workspaces, or `undefined` for the gateway's cwd fallback.
|
||||
* @param trustedHosts - extra authorities for the /api browser-trust fence, or `undefined` for the derived LAN literals alone.
|
||||
* @param config - an overlay of loader patches applied over the shipped web
|
||||
* composition, or `undefined` for none; already parsed from `--config`.
|
||||
*/
|
||||
export async function runWeb(
|
||||
host: string | undefined,
|
||||
@@ -36,12 +30,9 @@ export async function runWeb(
|
||||
dev: boolean,
|
||||
workspaceRoot: string | undefined,
|
||||
trustedHosts: string[] | undefined,
|
||||
config?: string,
|
||||
): Promise<void> {
|
||||
const entry = new AppCLIEntry({
|
||||
configPath: BASE_CONFIG,
|
||||
overlayPath: WEB_OVERLAY,
|
||||
...config !== undefined && { extraOverlayPath: resolveConfigPath(config, undefined) },
|
||||
configPath: CONFIG_PATH,
|
||||
dev,
|
||||
...host !== undefined && { host },
|
||||
...port !== undefined && { port },
|
||||
@@ -49,7 +40,6 @@ export async function runWeb(
|
||||
...trustedHosts !== undefined && { trustedHosts },
|
||||
})
|
||||
const { ctx, port: boundPort } = await entry.run()
|
||||
await registerLiveSessions(ctx)
|
||||
|
||||
let exiting = false
|
||||
const shutdown = (code: number): void => {
|
||||
|
||||
Reference in New Issue
Block a user