|
@@ -1,6 +1,6 @@
|
|
|
/**
|
|
/**
|
|
|
* `dsh` default surface — the interactive TUI coding agent. Boots the shipped
|
|
* `dsh` default surface — the interactive TUI coding agent. Boots the shipped
|
|
|
- * tui-agent config (or an explicit config argument) with the personal overlay
|
|
|
|
|
|
|
+ * tui-agent config (or the `--config` override) with the personal overlay
|
|
|
* from the Harness home (`~/.dsh`): its `.env` fills environment gaps (precedence:
|
|
* from the Harness home (`~/.dsh`): its `.env` fills environment gaps (precedence:
|
|
|
* ambient environment, then the invoking directory's `.env`, then the personal one)
|
|
* ambient environment, then the invoking directory's `.env`, then the personal one)
|
|
|
* and its `config.yaml` patches the booted tree. The workspace is the invoking
|
|
* and its `config.yaml` patches the booted tree. The workspace is the invoking
|
|
@@ -18,8 +18,7 @@ import {
|
|
|
installFailLoud,
|
|
installFailLoud,
|
|
|
loadEnv,
|
|
loadEnv,
|
|
|
loadPersonalPatches,
|
|
loadPersonalPatches,
|
|
|
- parseResumeArg,
|
|
|
|
|
- replaceResumeArg,
|
|
|
|
|
|
|
+ RESUME_SESSION_ID_KEY,
|
|
|
resolveConfigPath,
|
|
resolveConfigPath,
|
|
|
} from '@deepseek-ai/dsh-app-boot'
|
|
} from '@deepseek-ai/dsh-app-boot'
|
|
|
import { resolveDshHome } from '@deepseek-ai/dsh-paths'
|
|
import { resolveDshHome } from '@deepseek-ai/dsh-paths'
|
|
@@ -28,12 +27,6 @@ import type { TuiResumeHost } from '@deepseek-ai/dsh-tui'
|
|
|
|
|
|
|
|
const NAME = 'dsh'
|
|
const NAME = 'dsh'
|
|
|
|
|
|
|
|
-// The env var the shipped tui-agent config reads (`resumeSessionId: !!js
|
|
|
|
|
-// process.env.RESUME_SESSION_ID`) to rehydrate a persisted session. The
|
|
|
|
|
-// `--resume <id>` flag is CLI sugar that sets it before boot, so the printed
|
|
|
|
|
-// `dsh --resume <id>` exit hint runs back through this same intake.
|
|
|
|
|
-const RESUME_SESSION_ID_ENV = 'RESUME_SESSION_ID'
|
|
|
|
|
-
|
|
|
|
|
// Both the source tree (apps/cli/src) and the bundled bin (apps/cli/lib) sit
|
|
// Both the source tree (apps/cli/src) and the bundled bin (apps/cli/lib) sit
|
|
|
// one directory under apps/cli, so the shipped default config resolves with
|
|
// one directory under apps/cli, so the shipped default config resolves with
|
|
|
// the same relative hop from either artifact.
|
|
// the same relative hop from either artifact.
|
|
@@ -48,26 +41,30 @@ const SOURCE_ROOT = fileURLToPath(new URL('../../..', import.meta.url))
|
|
|
the tui-agent PTY smoke drives this path end to end, personal overlay included */
|
|
the tui-agent PTY smoke drives this path end to end, personal overlay included */
|
|
|
/**
|
|
/**
|
|
|
* Run the interactive TUI from the invoking directory.
|
|
* Run the interactive TUI from the invoking directory.
|
|
|
- * @param argv - arguments after the subcommand dispatch; a `--resume <id>` flag
|
|
|
|
|
- * resumes that persisted session, and the first non-flag argument may name a
|
|
|
|
|
- * config to boot instead of the shipped default.
|
|
|
|
|
|
|
+ * @param config - a config path to boot instead of the shipped default, or
|
|
|
|
|
+ * `undefined` for the default; already parsed from `--config`.
|
|
|
|
|
+ * @param resumeSessionId - a persisted session id to resume, or `undefined`;
|
|
|
|
|
+ * already parsed and non-empty-validated from `--resume`. It is provided on the
|
|
|
|
|
+ * boot context under {@link RESUME_SESSION_ID_KEY}, which the shipped config
|
|
|
|
|
+ * reads through `!!js` to rehydrate that session.
|
|
|
*/
|
|
*/
|
|
|
-export async function runTui(argv: string[]): Promise<void> {
|
|
|
|
|
|
|
+export async function runTui(config: string | undefined, resumeSessionId: string | undefined): Promise<void> {
|
|
|
// Refuse pipes BEFORE booting: a compose-time throw inside the Loader tree
|
|
// Refuse pipes BEFORE booting: a compose-time throw inside the Loader tree
|
|
|
// is logged per-entry rather than rethrown, so a piped launch would
|
|
// is logged per-entry rather than rethrown, so a piped launch would
|
|
|
// otherwise settle into an idle UI-less process instead of exiting nonzero.
|
|
// otherwise settle into an idle UI-less process instead of exiting nonzero.
|
|
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
|
- process.stderr.write(`${NAME}: the TUI requires stdin and stdout to be interactive TTYs\n`)
|
|
|
|
|
|
|
+ process.stderr.write(
|
|
|
|
|
+ `${NAME}: the TUI requires stdin and stdout to be interactive TTYs; use \`${NAME} -p "task"\` for pipes and automation\n`,
|
|
|
|
|
+ )
|
|
|
process.exit(1)
|
|
process.exit(1)
|
|
|
}
|
|
}
|
|
|
installFailLoud(NAME)
|
|
installFailLoud(NAME)
|
|
|
// The bin already loaded the invoking directory's .env; the personal .env
|
|
// The bin already loaded the invoking directory's .env; the personal .env
|
|
|
// only fills what is still unset (process.loadEnvFile never overrides).
|
|
// only fills what is still unset (process.loadEnvFile never overrides).
|
|
|
loadEnv(NAME, resolveDshHome())
|
|
loadEnv(NAME, resolveDshHome())
|
|
|
- // An explicit `--resume` flag beats any ambient RESUME_SESSION_ID, so set it
|
|
|
|
|
- // after loadEnv and before boot reads it through the config's `!!js`.
|
|
|
|
|
- const { resumeSessionId, rest } = parseResumeArg(argv)
|
|
|
|
|
- if (resumeSessionId !== undefined) process.env[RESUME_SESSION_ID_ENV] = resumeSessionId
|
|
|
|
|
|
|
+ // The in-place `/resume` handoff re-execs `dsh` with a normalized `--resume`
|
|
|
|
|
+ // flag, so the resumed process rehydrates through this same intake. The host
|
|
|
|
|
+ // is offered only when Node exposes `process.execve` and knows its own entry.
|
|
|
const entry = process.argv[1]
|
|
const entry = process.argv[1]
|
|
|
const execve = process.execve?.bind(process)
|
|
const execve = process.execve?.bind(process)
|
|
|
const app: { current?: Context } = {}
|
|
const app: { current?: Context } = {}
|
|
@@ -75,13 +72,15 @@ export async function runTui(argv: string[]): Promise<void> {
|
|
|
async handoff(sessionId): Promise<never> {
|
|
async handoff(sessionId): Promise<never> {
|
|
|
const current = app.current
|
|
const current = app.current
|
|
|
if (current === undefined) throw new Error(`${NAME}: app boot has not completed`)
|
|
if (current === undefined) throw new Error(`${NAME}: app boot has not completed`)
|
|
|
|
|
+ // Rebuild argv from the parsed config plus the selected id: TUI mode's
|
|
|
|
|
+ // only arguments are `--config <path>` and `--resume <id>`.
|
|
|
const nextArgv = [
|
|
const nextArgv = [
|
|
|
process.execPath,
|
|
process.execPath,
|
|
|
...process.execArgv,
|
|
...process.execArgv,
|
|
|
entry,
|
|
entry,
|
|
|
- ...replaceResumeArg(process.argv.slice(2), sessionId),
|
|
|
|
|
|
|
+ `--resume=${sessionId}`,
|
|
|
|
|
+ ...config !== undefined ? ['--config', config] : [],
|
|
|
]
|
|
]
|
|
|
- process.env[RESUME_SESSION_ID_ENV] = sessionId
|
|
|
|
|
try {
|
|
try {
|
|
|
await current.fiber.dispose()
|
|
await current.fiber.dispose()
|
|
|
execve(process.execPath, nextArgv, process.env)
|
|
execve(process.execPath, nextArgv, process.env)
|
|
@@ -94,9 +93,12 @@ export async function runTui(argv: string[]): Promise<void> {
|
|
|
}
|
|
}
|
|
|
const ctx = await boot(
|
|
const ctx = await boot(
|
|
|
NAME,
|
|
NAME,
|
|
|
- resolveConfigPath(rest[0] ?? DEFAULT_CONFIG, undefined),
|
|
|
|
|
|
|
+ resolveConfigPath(config ?? DEFAULT_CONFIG, undefined),
|
|
|
loadPersonalPatches(NAME),
|
|
loadPersonalPatches(NAME),
|
|
|
(hostCtx) => {
|
|
(hostCtx) => {
|
|
|
|
|
+ // Inject the resume id (or undefined) so the shipped config's `!!js`
|
|
|
|
|
+ // reads it as a bare identifier; then offer the in-place handoff host.
|
|
|
|
|
+ hostCtx.provide(RESUME_SESSION_ID_KEY, resumeSessionId)
|
|
|
if (resumeHost !== undefined) hostCtx.provide('tuiResumeHost', resumeHost)
|
|
if (resumeHost !== undefined) hostCtx.provide('tuiResumeHost', resumeHost)
|
|
|
},
|
|
},
|
|
|
)
|
|
)
|