mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
refactor(cli): bail early in the arg adapter instead of returning errors as data
Address review and cut ceremony: the adapter no longer models help/version/ errors as DshInvocation members. Commander owns those under exitOverride — it prints usage or the diagnostic and one try/catch in parseDshArgs turns the thrown CommanderError into process.exit with the intended code. bin.ts drops its help/version/error cases; the union is the three real modes. Domain checks bail via command.error(print + exit 1): --prompt rejects an empty task or a stray config/--resume, empty --resume= fails loud, and --host/--port are validated. A repeated --resume or a flag captured as a value is Commander's standard behavior, left alone (a bad id fails loud downstream). dsh --help discloses web via addHelpText. Net: args.ts 185 -> 112 lines. Also fixes review nits: built-bin e2e resolves on `close`; the /resume handoff uses `dsh --resume=<id> -- <config>` so a config named `web` stays a positional; and stale prose (cordis.yml comment, app-boot module doc + duplicate JSDoc, ui/README, two feature notes, an agent-loop test name) tracks the shipped state. Removes tui-demo's now-dead plugin-include dep and vendor/loader + app-boot tsconfig references.
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-24-dsh-commander-argument-adapter.md: c038f4facdc62039b8a31be5d660648fd81aebd2
|
||||
2026-07-24-dsh-commander-argument-adapter.zh.md: 285917af7c4b0da769ae7595bb1a6e5966adafd9
|
||||
2026-07-24-dsh-commander-argument-adapter.md: 0f6b18848eacc5d5771e500bf226cc6f680f8d3f
|
||||
2026-07-24-dsh-commander-argument-adapter.zh.md: ae523d0e37d09ce1f85b317bb0850194045474cb
|
||||
|
||||
@@ -10,15 +10,15 @@ The `dsh` CLI entry (`apps/cli`) parsed argv in three hand-rolled idioms that di
|
||||
|
||||
## Decision
|
||||
|
||||
Argv is parsed once, in `apps/cli/src/args.ts`, through a Commander adapter (the same parser the SDK bins — `create-sdk`, `dsh-scripts` — already standardize on). `parseDshArgs(argv, version)` resolves the invocation into a discriminated `DshInvocation` union: `{ mode: 'tui', config?, resume? }`, `{ mode: 'headless', prompt }`, `{ mode: 'web', host, port, dev }`, `{ mode: 'help' | 'version', text }`, or `{ mode: 'error', message }`. Commander runs under `exitOverride()` with output captured, so it never writes or exits on its own — `--help`, `--version`, and every parse error come back as data.
|
||||
Argv is parsed once, in `apps/cli/src/args.ts`, through a Commander adapter (the same parser the SDK bins — `create-sdk`, `dsh-scripts` — already standardize on). `parseDshArgs(argv, version)` returns a discriminated `DshInvocation` union of the three real modes: `{ mode: 'tui', config?, resume? }`, `{ mode: 'headless', prompt }`, or `{ mode: 'web', host, port, dev }`. It does **not** model help/version/errors as data: Commander owns those, printing usage or the diagnostic and exiting at the point of failure. `exitOverride()` turns each into a thrown `CommanderError` carrying the intended code (0 for help/version, 1 for a parse or domain error), which one `try/catch` in `parseDshArgs` turns into `process.exit`.
|
||||
|
||||
`bin.ts` calls the adapter once and switches on `mode` (closed union, `satisfies never` default), dynamic-importing only the chosen mode's module. Each mode module now consumes already-parsed values: `runTui(config, resume)`, `runHeadless(task)`, `runWeb(host, port, dev)` — none re-reads argv. `web` is a **reserved first token**: `parseDshArgs` dispatches a leading `web` to its own Commander parser and everything else to the default TUI/headless parser, so root flags and `web` flags never share a grammar — `dsh web -p x` fails loud (`web` has no `-p`) and `dsh -p x web` is just a headless prompt whose second positional is dropped, with no cross-command leakage to guard against. Each parser reads Commander's `opts()`/`processedArgs` after `parse()` rather than through action closures. `--host` is a `.choices([LOOPBACK_HOST, ALL_INTERFACES_HOST])` and `--port` an `argParser` that range-checks 0–65535, moving both from the inline `runWeb` checks into the parser; `--dev` mounts the client HMR driver and bundle watch. Two post-parse checks preserve the "never silently start fresh" invariant: an empty `--resume=` id and an empty `-p` task each become a `mode: 'error'`, because agent-loop treats an empty resume id as no-resume and an empty prompt has nothing to run. A repeated `--resume` is Commander's natural last-wins (the old bespoke scanner rejected it; last-wins is the standard CLI behavior and needs no special case). `--version` reads this app's `package.json`.
|
||||
`bin.ts` calls the adapter once and switches on `mode` (closed union, `satisfies never` default), dynamic-importing only the chosen mode's module; only a valid, non-help invocation reaches the switch, so it has no help/version/error cases. Each mode module consumes already-parsed values: `runTui(config, resume)`, `runHeadless(task)`, `runWeb(host, port, dev)` — none re-reads argv. `web` is a **reserved first token**: `parseDshArgs` dispatches a leading `web` to its own Commander parser and everything else to the default TUI/headless parser, so root flags and `web` flags never share a grammar — `dsh web -p x` fails loud (`web` has no `-p`). Each parser reads Commander's `opts()`/`processedArgs` after `parse()`, then bails via `command.error(...)` (print + exit 1) on the domain checks Commander cannot express: `--prompt` selects headless and rejects an empty task or a stray config/`--resume` rather than silently dropping a TUI input; an empty `--resume=` id fails loud (agent-loop treats `''` as no-resume); `--host` must be loopback/all-interfaces and `--port` an integer in 0–65535, moving both from the inline `runWeb` checks into the parser. `--dev` mounts the client HMR driver and bundle watch. A repeated `--resume`, or a following flag captured as a `--resume`/`--prompt` value, is Commander's standard behavior (last-wins / next-token) and is left alone; a bad id fails loud downstream when the session cannot load. `dsh --help` discloses the `web` mode through an `addHelpText` line (a real `web` subcommand would hijack the `[config]` positional). `--version` reads this app's `package.json`.
|
||||
|
||||
`parseResumeArg` is deleted from `dsh-app-boot` (its export, its README row, and its unit block); the pre-release stance permits the removal. `dsh-app-boot` keeps its boot/env/config/personal-overlay helpers — only the argv scanner leaves.
|
||||
|
||||
## Resume without an environment variable
|
||||
|
||||
Merging the concurrent safe-session-resume feature onto this parser retired the `RESUME_SESSION_ID` environment variable, which had been the only bridge from `--resume` into the shipped config's `resumeSessionId: !!js process.env.RESUME_SESSION_ID`. `runTui` now injects the already-parsed id through `boot`'s `prepare(ctx)` hook — `ctx.provide(RESUME_SESSION_ID_KEY, id)` (a new `dsh-app-boot` export, value `'resumeSessionId'`) — and the four tui-agent/cordis configs read it as a bare identifier: `resumeSessionId: !!js "typeof resumeSessionId === 'string' ? resumeSessionId : undefined"`. The expression is quoted because YAML otherwise parses the `?`/`:` as a mapping; the `typeof` guard tolerates a bin that never provides the slot. The `/resume` in-place handoff (`process.execve`) rebuilds its re-exec argv directly as `dsh [config] --resume <id>` from the parsed values, so `replaceResumeArg` (which the merge brought in) is dropped alongside `parseResumeArg`.
|
||||
Merging the concurrent safe-session-resume feature onto this parser retired the `RESUME_SESSION_ID` environment variable, which had been the only bridge from `--resume` into the shipped config's `resumeSessionId: !!js process.env.RESUME_SESSION_ID`. `runTui` now injects the already-parsed id through `boot`'s `prepare(ctx)` hook — `ctx.provide(RESUME_SESSION_ID_KEY, id)` (a new `dsh-app-boot` export, value `'resumeSessionId'`) — and the four tui-agent/cordis configs read it as a bare identifier: `resumeSessionId: !!js "typeof resumeSessionId === 'string' ? resumeSessionId : undefined"`. The expression is quoted because YAML otherwise parses the `?`/`:` as a mapping; the `typeof` guard tolerates a launcher that never provides the slot. The `/resume` in-place handoff (`process.execve`) rebuilds its re-exec argv directly from the parsed values as `dsh --resume=<id> [-- <config>]` — the `--` keeps a config named `web` or starting with `-` a positional — so `replaceResumeArg` (which the merge brought in) is dropped alongside `parseResumeArg`.
|
||||
|
||||
## One terminal front door: `dsh`
|
||||
|
||||
@@ -44,7 +44,7 @@ The argument surface stays inside `apps/cli`, the assembly tier, not a `packages
|
||||
|
||||
## Testing
|
||||
|
||||
`apps/cli/tests/args.spec.ts` (new; `apps/*/tests` added to the vitest include and `apps/cli/tests` to `tsconfig.host.json`) covers the adapter at the level that matters: mode routing by shape (including `web --dev`), the fail-loud checks (empty resume/prompt, bad host/port, unknown option), and `--help`/`--version` surfacing as data. Both PTY smoke groups in `examples/tui-agent/tests/tui-keyless-smoke.e2e.ts` now drive the real `apps/cli/src/bin.ts`: the `tui-agent` group boots the config as a positional, and the `dsh CLI` group covers default boot, personal overlay, invalid config, the `--resume` config intake, the `process.execve` in-place resume handoff, and the source-path prompt. `examples/cordis-agent/tests/keyless-smoke.e2e.ts` likewise launches through `dsh`. `packages/ui/app-boot/tests/app-boot.spec.ts` drops its `parseResumeArg`/`replaceResumeArg` blocks; the TUI unit and snapshot fixtures use the `dsh --resume {session}` resume command.
|
||||
`apps/cli/tests/args.spec.ts` (new; `apps/*/tests` added to the vitest include and `apps/cli/tests` to `tsconfig.host.json`) covers the adapter at the level that matters: mode routing by shape (including `web --dev`), and the exit-code behavior for the fail-loud checks (empty resume/prompt, bad host/port, `--prompt` mixed with a config, unknown option) and `--help`/`--version`, captured through a `process.exit` spy. Both PTY smoke groups in `examples/tui-agent/tests/tui-keyless-smoke.e2e.ts` now drive the real `apps/cli/src/bin.ts`: the `tui-agent` group boots the config as a positional, and the `dsh CLI` group covers default boot, personal overlay, invalid config, the `--resume` config intake, the `process.execve` in-place resume handoff, and the source-path prompt. `examples/cordis-agent/tests/keyless-smoke.e2e.ts` likewise launches through `dsh`. `packages/ui/app-boot/tests/app-boot.spec.ts` drops its `parseResumeArg`/`replaceResumeArg` blocks; the TUI unit and snapshot fixtures use the `dsh --resume {session}` resume command.
|
||||
|
||||
## Consequences
|
||||
|
||||
|
||||
@@ -10,15 +10,15 @@ Status: implemented
|
||||
|
||||
## 决策
|
||||
|
||||
argv 只在 `apps/cli/src/args.ts` 中解析一次,通过一个 Commander 适配器(即 SDK bin,如 `create-sdk`、`dsh-scripts`,已经统一采用的那个解析器)。`parseDshArgs(argv, version)` 将调用解析为一个判别式 `DshInvocation` 联合类型:`{ mode: 'tui', config?, resume? }`、`{ mode: 'headless', prompt }`、`{ mode: 'web', host, port, dev }`、`{ mode: 'help' | 'version', text }` 或 `{ mode: 'error', message }`。Commander 在 `exitOverride()` 下运行并捕获输出,因此它自身从不写出或退出:`--help`、`--version` 和每个解析错误都以数据形式返回。
|
||||
argv 只在 `apps/cli/src/args.ts` 中解析一次,并使用 Commander 适配器(SDK bin `create-sdk`、`dsh-scripts` 已经统一采用的同一解析器)。`parseDshArgs(argv, version)` 返回仅包含三种实际模式的判别式 `DshInvocation` 联合类型:`{ mode: 'tui', config?, resume? }`、`{ mode: 'headless', prompt }` 或 `{ mode: 'web', host, port, dev }`。它**不会**将帮助、版本信息或错误建模为数据:这些情况由 Commander 处理,在触发处打印用法或诊断信息并退出。`exitOverride()` 会将每种情况转为抛出的 `CommanderError`,并携带预期退出码(帮助或版本为 0,解析错误或领域错误为 1);唯一一处 `try/catch` 位于 `parseDshArgs` 中,捕获错误后调用 `process.exit`。
|
||||
|
||||
`bin.ts` 调用一次适配器,并对 `mode` 做分支切换(封闭联合类型,默认分支为 `satisfies never`),只动态导入所选模式对应的模块。每个模式模块现在只消费已解析好的值:`runTui(config, resume)`、`runHeadless(task)`、`runWeb(host, port, dev)`,都不会再次读取 argv。`web` 是一个**保留的首个 token**:`parseDshArgs` 将开头的 `web` 分发给它自己的 Commander 解析器,其余一切分发给默认的 TUI/headless 解析器,因此根级标志与 `web` 标志从不共用同一套语法——`dsh web -p x` 会显式报错(`web` 没有 `-p`),而 `dsh -p x web` 只是一个 headless prompt,其第二个位置参数被丢弃,无需防范任何跨命令泄漏。每个解析器都在 `parse()` 之后读取 Commander 的 `opts()`/`processedArgs`,而不是通过 action 闭包。`--host` 是一个 `.choices([LOOPBACK_HOST, ALL_INTERFACES_HOST])`,`--port` 是一个对 0–65535 做范围检查的 `argParser`,二者都从内联的 `runWeb` 检查移入了解析器;`--dev` 会挂载客户端 HMR(热模块替换)驱动,并启用构建产物监视。两处解析后的检查保留了「绝不静默重新开始」不变式:空的 `--resume=` id 和空的 `-p` 任务各自变为 `mode: 'error'`,因为 agent-loop 把空的 resume id 视为不恢复,而空的 prompt 没有任何内容可运行。重复出现的 `--resume` 采用 Commander 天然的后者胜出(旧的定制扫描器会拒绝它;后者胜出是标准的 CLI 行为,无需特殊处理)。`--version` 读取本应用的 `package.json`。
|
||||
`bin.ts` 只调用适配器一次,并对 `mode` 做分支切换(封闭联合类型,默认分支为 `satisfies never`),仅动态导入所选模式对应的模块;只有合法的非帮助请求才会进入这段分支逻辑,因此其中没有帮助、版本或错误分支。每个模式模块只消费已解析好的值:`runTui(config, resume)`、`runHeadless(task)`、`runWeb(host, port, dev)`,都不会再次读取 argv。`web` 是一个**保留的首个 token**:`parseDshArgs` 将开头的 `web` 分发给它自己的 Commander 解析器,其余一切分发给默认的 TUI/headless 解析器,因此根级标志与 `web` 标志从不共用同一套语法;`dsh web -p x` 会显式报错(`web` 没有 `-p`)。每个解析器都读取 Commander 的 `opts()`/`processedArgs`(在 `parse()` 之后),随后对 Commander 无法表达的领域校验调用 `command.error(...)` 立即终止(打印信息并以退出码 1 退出):`--prompt` 选择 headless 模式,并在任务为空或存在多余的配置位置参数或 `--resume` 时拒绝调用,而不会静默丢弃 TUI 输入;空的 `--resume=` id 会显式失败(agent-loop 把 `''` 视为不恢复);`--host` 必须是回环地址或全接口地址,`--port` 必须是 0–65535 范围内的整数,这两项校验都从 `runWeb` 的内联检查移入解析器。`--dev` 会挂载客户端 HMR(热模块替换)驱动,并启用构建产物监视。重复提供 `--resume`,或后续标志被捕获为 `--resume` 或 `--prompt` 的值,都是 Commander 的标准行为(最后一次取值生效/将下一 token 作为值),本适配器不作干预;无效 id 会在下游无法加载会话时显式失败。`dsh --help` 会展示 `web` 模式,具体通过一行 `addHelpText` 文本实现(真正的 `web` 子命令会劫持 `[config]` 位置参数)。`--version` 读取本应用的 `package.json`。
|
||||
|
||||
`parseResumeArg` 从 `dsh-app-boot` 中删除(包括其导出、README 中的对应行以及单元测试块);预发布阶段的立场允许这次删除。`dsh-app-boot` 保留其 boot/env/config/个人覆盖辅助函数,只有 argv 扫描器被移除。
|
||||
|
||||
## 无需环境变量即可恢复
|
||||
|
||||
将与本解析器并行开发的安全会话恢复功能合入时,系统移除了 `RESUME_SESSION_ID` 环境变量。此前,它是将 `--resume` 的值传给随产品提供的配置字段 `resumeSessionId: !!js process.env.RESUME_SESSION_ID` 的唯一通道。`runTui` 现在通过 `boot` 的 `prepare(ctx)` 钩子注入已解析的 id:`ctx.provide(RESUME_SESSION_ID_KEY, id)`(`dsh-app-boot` 的新导出,值为 `'resumeSessionId'`);tui-agent 和 cordis-agent 的四份配置将该值作为裸标识符读取:`resumeSessionId: !!js "typeof resumeSessionId === 'string' ? resumeSessionId : undefined"`。这个表达式需要加引号,否则 YAML 会把 `?` 和 `:` 解析为映射;`typeof` 守卫使从未提供该槽位的 bin 也能正常运行。`/resume` 原地交接(`process.execve`)直接根据解析后的值将重新执行的 argv 构造成 `dsh [config] --resume <id>`,因此合并时引入的 `replaceResumeArg` 与 `parseResumeArg` 一并删除。
|
||||
将与本解析器并行开发的安全会话恢复功能合入时,系统移除了 `RESUME_SESSION_ID` 环境变量。此前,它是将 `--resume` 的值传给随产品提供的配置字段 `resumeSessionId: !!js process.env.RESUME_SESSION_ID` 的唯一通道。`runTui` 现在通过 `boot` 的 `prepare(ctx)` 钩子注入已解析的 id:`ctx.provide(RESUME_SESSION_ID_KEY, id)`(`dsh-app-boot` 的新导出,值为 `'resumeSessionId'`);tui-agent 和 cordis-agent 的四份配置将该值作为裸标识符读取:`resumeSessionId: !!js "typeof resumeSessionId === 'string' ? resumeSessionId : undefined"`。这个表达式需要加引号,否则 YAML 会把 `?` 和 `:` 解析为映射;`typeof` 守卫使从未提供该槽位的启动器也能正常运行。`/resume` 原地交接(`process.execve`)直接根据解析后的值将重新执行的 argv 构造成 `dsh --resume=<id> [-- <config>]`;其中 `--` 可确保名称为 `web` 或以 `-` 开头的配置仍被视为位置参数。因此,合并时引入的 `replaceResumeArg` 与 `parseResumeArg` 一并删除。
|
||||
|
||||
## 唯一的终端入口:`dsh`
|
||||
|
||||
@@ -44,7 +44,7 @@ argv 只在 `apps/cli/src/args.ts` 中解析一次,通过一个 Commander 适
|
||||
|
||||
## 测试
|
||||
|
||||
`apps/cli/tests/args.spec.ts`(新增;`apps/*/tests` 加入 vitest include,`apps/cli/tests` 加入 `tsconfig.host.json`)覆盖适配器的关键行为:根据参数形态选择模式(包括 `web --dev`)、显式报错场景(空的恢复会话 id、空提示词、非法主机、非法端口和未知选项),以及将 `--help` 和 `--version` 作为数据返回。`examples/tui-agent/tests/tui-keyless-smoke.e2e.ts` 中的两组 PTY 冒烟测试现在都驱动真实的 `apps/cli/src/bin.ts`:`tui-agent` 组将配置作为位置参数启动,`dsh CLI` 组覆盖默认启动、个人覆盖、无效配置、配置对 `--resume` 的接收、通过 `process.execve` 原地恢复交接,以及包含源码路径的系统提示词。`examples/cordis-agent/tests/keyless-smoke.e2e.ts` 同样通过 `dsh` 启动。`packages/ui/app-boot/tests/app-boot.spec.ts` 移除其 `parseResumeArg` 和 `replaceResumeArg` 测试块;TUI 单元测试和快照 fixture(测试前置数据)使用 `dsh --resume {session}` 恢复命令。
|
||||
`apps/cli/tests/args.spec.ts`(新增;`apps/*/tests` 加入 vitest include,`apps/cli/tests` 加入 `tsconfig.host.json`)覆盖适配器的关键行为:根据参数形态进行模式路由(包括 `web --dev`),并验证以下情况各自的退出码行为:显式报错检查(恢复 id 或提示词为空、host 或 port 无效、`--prompt` 与配置混用、未知选项)以及 `--help` 和 `--version`;这些退出码通过 `process.exit` spy 捕获。`examples/tui-agent/tests/tui-keyless-smoke.e2e.ts` 中的两组 PTY 冒烟测试现在都驱动真实的 `apps/cli/src/bin.ts`:`tui-agent` 组将配置作为位置参数启动,`dsh CLI` 组覆盖默认启动、个人覆盖、无效配置、配置对 `--resume` 的接收、通过 `process.execve` 原地恢复交接,以及包含源码路径的系统提示词。`examples/cordis-agent/tests/keyless-smoke.e2e.ts` 同样通过 `dsh` 启动。`packages/ui/app-boot/tests/app-boot.spec.ts` 移除其 `parseResumeArg` 和 `replaceResumeArg` 测试块;TUI 单元测试和快照 fixture(测试前置数据)使用 `dsh --resume {session}` 恢复命令。
|
||||
|
||||
## 影响
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-21-dsh-system-prompt-source-path.md: b54d01488fd7c0b49e06200c93af2b056c9fd00b
|
||||
2026-07-21-dsh-system-prompt-source-path.zh.md: 208e3dce072f63c280999e15276dce62ff4e5c43
|
||||
2026-07-21-dsh-system-prompt-source-path.md: 4cb89e8124840bba6633235d195e95957245137c
|
||||
2026-07-21-dsh-system-prompt-source-path.zh.md: 90c23bed4a3f95155e323c63a68fe2da09543ea6
|
||||
|
||||
@@ -16,7 +16,7 @@ The testable logic lives in `dsh-app-boot`, not in `apps/cli`, because `apps/*`
|
||||
|
||||
## Scope
|
||||
|
||||
Only the `dsh` CLI adds this. The demo bins (`dsh-tui-demo`, `dsh-acp-demo`) boot their committed trees verbatim and gain no source section: they are not the self-modification surface, and their checkout root is not a fact the model needs.
|
||||
Only the `dsh` CLI adds this. The demo bins (`dsh-cli-demo`, `dsh-acp-demo`) boot their committed trees verbatim and gain no source section: they are not the self-modification surface, and their checkout root is not a fact the model needs.
|
||||
|
||||
## HMR
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ Status: implemented
|
||||
|
||||
## Scope
|
||||
|
||||
只有 `dsh` CLI 会加入这一段。demo bin(`dsh-tui-demo`、`dsh-acp-demo`)原样引导它们已提交的插件树,不会获得 source 段:它们不是自我修改的接口,其检出根目录也不是模型需要知道的事实。
|
||||
只有 `dsh` CLI 会加入这一段。demo bin(`dsh-cli-demo`、`dsh-acp-demo`)原样引导它们已提交的插件树,不会获得 source 段:它们不是自我修改的接口,其检出根目录也不是模型需要知道的事实。
|
||||
|
||||
## HMR
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-21-tui-no-banner.md: f5f4b1b847740e741ec3e33a6116e7497e955bd1
|
||||
2026-07-21-tui-no-banner.zh.md: 956fe03e2c0b09ea7378ffd53ffbe8d712d1e152
|
||||
2026-07-21-tui-no-banner.md: a6e0956f289cfc810da766fd0cae94b97baf5280
|
||||
2026-07-21-tui-no-banner.zh.md: acc5614727cf67881832af1685be557d675696e7
|
||||
|
||||
@@ -13,7 +13,7 @@ The TUI opened with a boxed product banner ("DEEPSEEK HARNESS" + model/session d
|
||||
## Decision
|
||||
|
||||
- `HeaderComponent`, the sweep animation, and its lifecycle wiring are deleted. The TUI mounts straight into the transcript; startup renders nothing above the separator.
|
||||
- The model name moves into the footer status line's left segment (`<model> <cwd> ↑tokens ↓tokens`), so the session's driving model stays visible at all times, not just at boot. The session id is no longer displayed — it lives in the session log and `./.sessions` filenames, and `RESUME_SESSION_ID` consumers retrieve it there.
|
||||
- The model name moves into the footer status line's left segment (`<model> <cwd> ↑tokens ↓tokens`), so the session's driving model stays visible at all times, not just at boot. The session id is no longer displayed — it lives in the session log and `./.sessions` filenames, where `dsh --resume <id>` and the `/resume` selector retrieve it.
|
||||
- `welcome`, when configured, renders as the transcript's first line (a muted notice) inside `rebuildTranscript`, so palette swaps preserve it. Unset renders nothing. Fixtures keep their configured welcomes; the PTY smoke's boot marker becomes the footer's model name, the only mounted-TUI text guaranteed to render regardless of cwd length.
|
||||
|
||||
This supersedes the [banner sweep Agent Note](2026-07-21-tui-banner-sweep.md) entirely: both the sweep and the banner it animated are gone.
|
||||
|
||||
@@ -13,7 +13,7 @@ TUI 启动时展示一个带框的产品横幅("DEEPSEEK HARNESS" + 模型/会
|
||||
## Decision
|
||||
|
||||
- 删除 `HeaderComponent`、扫入动画及其生命周期接线。TUI 直接挂载进 transcript;启动时分隔线之上不渲染任何东西。
|
||||
- 模型名移入页脚状态行的左段(`<model> <cwd> ↑tokens ↓tokens`),会话使用的模型因此始终可见,而不只是启动时。会话 id 不再显示——它存在于会话日志和 `./.sessions` 文件名中,`RESUME_SESSION_ID` 的使用者从那里获取。
|
||||
- 模型名移入页脚状态行的左段(`<model> <cwd> ↑tokens ↓tokens`),会话使用的模型因此始终可见,而不只是启动时。会话 id 不再显示——它存在于会话日志和 `./.sessions` 文件名中,`dsh --resume <id>` 和 `/resume` 选择器会从中获取该 id。
|
||||
- 配置了 `welcome` 时,它作为 transcript 的第一行(一条弱化的通知)在 `rebuildTranscript` 内渲染,因此调色板切换会保留它。未设置则什么也不渲染。fixture 保留各自配置的欢迎语;PTY 冒烟测试的启动标记改为页脚的模型名——无论 cwd 多长都保证渲染的唯一挂载后文本。
|
||||
|
||||
本 note 完全取代[横幅扫入 Agent Note](2026-07-21-tui-banner-sweep.md):扫入动画和它所动画的横幅都已移除。
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
/**
|
||||
* 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; each mode module then consumes the
|
||||
* already-parsed values instead of re-reading argv. Output is suppressed and
|
||||
* `exitOverride` is set so Commander never writes or exits on its own — every
|
||||
* outcome (including `--help`/`--version` and parse errors) is returned to the
|
||||
* caller as data. The `web` subcommand is a reserved first token dispatched to
|
||||
* its own parser, so root flags and `web` flags never share a grammar.
|
||||
* and dynamic-imports that mode's module. 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.
|
||||
* The `web` subcommand is a reserved first token dispatched to its own parser.
|
||||
* @module @deepseek-ai/dsh/args
|
||||
*/
|
||||
|
||||
import { Command, CommanderError, InvalidArgumentError, Option } from 'commander'
|
||||
import { Command, CommanderError } from 'commander'
|
||||
|
||||
/** The loopback host `dsh web` binds by default. */
|
||||
export const LOOPBACK_HOST = '127.0.0.1'
|
||||
@@ -31,10 +29,7 @@ interface HeadlessInvocation {
|
||||
prompt: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Browser UI: `dsh web`. Host constrained to {@link LOOPBACK_HOST}/{@link ALL_INTERFACES_HOST};
|
||||
* port already coerced and range-checked; `dev` mounts the client HMR driver and bundle watch.
|
||||
*/
|
||||
/** Browser UI: `dsh web`. Host is loopback/all-interfaces, port a 0–65535 integer, `dev` mounts the HMR driver. */
|
||||
interface WebInvocation {
|
||||
mode: 'web'
|
||||
host: string
|
||||
@@ -42,120 +37,76 @@ interface WebInvocation {
|
||||
dev: boolean
|
||||
}
|
||||
|
||||
/** `--help` or `--version` requested: `bin.ts` prints `text` to stdout and exits 0. */
|
||||
interface InfoInvocation {
|
||||
mode: 'help' | 'version'
|
||||
text: string
|
||||
}
|
||||
/** The resolved `dsh` invocation: exactly one mode. `--help`/`--version`/errors exit inside {@link parseDshArgs}. */
|
||||
export type DshInvocation = TuiInvocation | HeadlessInvocation | WebInvocation
|
||||
|
||||
/** A parse error (unknown option, missing/invalid argument): `bin.ts` prints `message` to stderr and exits 1. */
|
||||
interface ErrorInvocation {
|
||||
mode: 'error'
|
||||
message: string
|
||||
}
|
||||
|
||||
/** The resolved `dsh` invocation: exactly one mode, all values parsed and validated. */
|
||||
export type DshInvocation =
|
||||
| TuiInvocation
|
||||
| HeadlessInvocation
|
||||
| WebInvocation
|
||||
| InfoInvocation
|
||||
| ErrorInvocation
|
||||
|
||||
/** Coerce `--port` to an integer in 0–65535; a bad value fails loud as a parse error. */
|
||||
function parsePort(raw: string): number {
|
||||
const port = Number(raw)
|
||||
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
||||
throw new InvalidArgumentError(`invalid --port ${raw}`)
|
||||
}
|
||||
return port
|
||||
}
|
||||
|
||||
/**
|
||||
* A configured `Command` under `exitOverride` with output captured into `sink`,
|
||||
* so `--help`, `--version`, and parse errors surface as thrown `CommanderError`s
|
||||
* (see {@link settle}) rather than writing to a stream or exiting.
|
||||
*/
|
||||
function program(name: string, version: string, sink: string[]): Command {
|
||||
return new Command()
|
||||
.name(name)
|
||||
.version(version, '-V, --version', 'output the version number')
|
||||
.exitOverride()
|
||||
.configureOutput({
|
||||
writeOut: chunk => void sink.push(chunk),
|
||||
writeErr: chunk => void sink.push(chunk),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Run `command.parse` and map its thrown `CommanderError` to an info/error
|
||||
* invocation, or `undefined` when the parse succeeded (the caller then reads the
|
||||
* parsed options).
|
||||
*/
|
||||
function settle(command: Command, argv: readonly string[], sink: string[]): InfoInvocation | ErrorInvocation | undefined {
|
||||
try {
|
||||
command.parse(argv, { from: 'user' })
|
||||
return undefined
|
||||
} catch (error) {
|
||||
/* v8 ignore next -- Commander only throws CommanderError from parse under exitOverride */
|
||||
if (!(error instanceof CommanderError)) throw error
|
||||
if (error.code === 'commander.helpDisplayed') return { mode: 'help', text: sink.join('') }
|
||||
if (error.code === 'commander.version') return { mode: 'version', text: sink.join('') }
|
||||
return { mode: 'error', message: error.message }
|
||||
}
|
||||
/** A `Command` under `exitOverride`, so {@link parseDshArgs} owns the exit, named for its usage line. */
|
||||
function program(name: string, version: string): Command {
|
||||
return new Command().name(name).version(version, '-V, --version', 'output the version number').exitOverride()
|
||||
}
|
||||
|
||||
/** Parse `dsh web` arguments (everything after the `web` token). */
|
||||
function parseWeb(argv: readonly string[], version: string): DshInvocation {
|
||||
const sink: string[] = []
|
||||
const web = program('dsh web', version, sink)
|
||||
function parseWeb(argv: readonly string[], version: string): WebInvocation {
|
||||
const web = program('dsh web', version)
|
||||
.description('serve the browser UI')
|
||||
.addOption(new Option('--host <host>', 'bind host').choices([LOOPBACK_HOST, ALL_INTERFACES_HOST]).default(LOOPBACK_HOST))
|
||||
.addOption(new Option('--port <port>', 'listen port').default(DEFAULT_WEB_PORT).argParser(parsePort))
|
||||
.option('--host <host>', `bind host (${LOOPBACK_HOST} or ${ALL_INTERFACES_HOST})`, LOOPBACK_HOST)
|
||||
.option('--port <port>', 'listen port', String(DEFAULT_WEB_PORT))
|
||||
.option('--dev', 'mount the client HMR driver and watch plugin bundles for rebuilds')
|
||||
const settled = settle(web, argv, sink)
|
||||
if (settled !== undefined) return settled
|
||||
const { host, port, dev } = web.opts<{ host: string; port: number; dev?: boolean }>()
|
||||
return { mode: 'web', host, port, dev: dev ?? false }
|
||||
web.parse(argv, { from: 'user' })
|
||||
const { host, port, dev } = web.opts<{ host: string; port: string; dev?: boolean }>()
|
||||
if (host !== LOOPBACK_HOST && host !== ALL_INTERFACES_HOST) {
|
||||
web.error(`error: --host must be ${LOOPBACK_HOST} or ${ALL_INTERFACES_HOST}`)
|
||||
}
|
||||
const portNumber = Number(port)
|
||||
if (!/^\d+$/.test(port) || !Number.isInteger(portNumber) || portNumber > 65535) {
|
||||
web.error('error: --port must be an integer in 0-65535')
|
||||
}
|
||||
return { mode: 'web', host, port: portNumber, dev: dev === true }
|
||||
}
|
||||
|
||||
/** Parse the default (TUI / headless) arguments: `[config]`, `-p/--prompt`, `--resume`. */
|
||||
function parseRoot(argv: readonly string[], version: string): DshInvocation {
|
||||
const sink: string[] = []
|
||||
const root = program('dsh', version, sink)
|
||||
const root = program('dsh', version)
|
||||
.description('dsh: interactive TUI, headless task, and browser UI')
|
||||
.argument('[config]', 'config to boot instead of the shipped default (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)')
|
||||
const settled = settle(root, argv, sink)
|
||||
if (settled !== undefined) return settled
|
||||
// Disclose the web mode in `dsh --help`; a real `web` subcommand would
|
||||
// hijack the `[config]` positional. `parseDshArgs` intercepts `web` first.
|
||||
.addHelpText('after', '\nCommands:\n web serve the browser UI (run `dsh web --help`)')
|
||||
root.parse(argv, { from: 'user' })
|
||||
const { prompt, resume } = root.opts<{ prompt?: string; resume?: string }>()
|
||||
const config = root.processedArgs[0] as string | undefined
|
||||
|
||||
if (prompt !== undefined) {
|
||||
// A headless prompt owns the invocation; an empty task has nothing to run.
|
||||
if (prompt === '') return { mode: 'error', message: "error: option '-p, --prompt <task>' must not be empty" }
|
||||
// A headless prompt owns the invocation; an empty task has nothing to run,
|
||||
// and a config or --resume alongside it is a TUI input that must not
|
||||
// silently vanish from the run.
|
||||
if (prompt === '') root.error('error: --prompt needs a task')
|
||||
if (config !== undefined || resume !== undefined) root.error('error: --prompt takes no config or --resume')
|
||||
return { mode: 'headless', prompt }
|
||||
}
|
||||
// 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 (resume === '') return { mode: 'error', message: "error: option '--resume <id>' must not be empty" }
|
||||
return {
|
||||
mode: 'tui',
|
||||
...config !== undefined ? { config } : {},
|
||||
...resume !== undefined ? { resume } : {},
|
||||
}
|
||||
if (resume === '') root.error('error: --resume needs a session id')
|
||||
return { mode: 'tui', ...config !== undefined && { config }, ...resume !== undefined && { resume } }
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the raw argv into a single {@link DshInvocation}. Never writes to a
|
||||
* stream and never exits; `--help`/`--version` and every parse error come back
|
||||
* as data for `bin.ts` to act on. A leading `web` token dispatches to the web
|
||||
* parser; everything else is the default TUI/headless grammar.
|
||||
* Resolve the raw argv into a {@link DshInvocation}, or print and exit for
|
||||
* `--help`/`--version`/a parse error. A leading `web` token dispatches to the
|
||||
* web parser; everything else is the default TUI/headless grammar.
|
||||
* @param argv - the arguments after the node binary and script (`process.argv.slice(2)`).
|
||||
* @param version - the version string `--version` prints; read from this app's package.json.
|
||||
* @returns the resolved invocation, discriminated by `mode`.
|
||||
* @returns the resolved invocation (only reached on a valid, non-help invocation).
|
||||
*/
|
||||
export function parseDshArgs(argv: readonly string[], version: string): DshInvocation {
|
||||
return argv[0] === 'web' ? parseWeb(argv.slice(1), version) : parseRoot(argv, version)
|
||||
try {
|
||||
return argv[0] === 'web' ? parseWeb(argv.slice(1), version) : parseRoot(argv, version)
|
||||
} catch (error) {
|
||||
// Commander printed help/version/the error under `exitOverride`; exit with
|
||||
// the code it chose (0 for help/version, 1 for a parse or domain error).
|
||||
/* v8 ignore next -- Commander only throws CommanderError from parse/error under exitOverride */
|
||||
return process.exit(error instanceof CommanderError ? error.exitCode : 1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
* dsh — command-line entry. Parses argv once through the Commander adapter and
|
||||
* switches on the resolved mode; dynamic imports keep unrelated modes out of
|
||||
* each dispatch path. `web` and headless prompts run their own module;
|
||||
* everything else opens the TUI. `--help`/`--version` print and exit 0; a parse
|
||||
* error prints to stderr and exits 1.
|
||||
* everything else opens the TUI. The adapter itself prints and exits for
|
||||
* `--help`/`--version`/a parse error, so only a valid mode reaches the switch.
|
||||
* @module @deepseek-ai/dsh/bin
|
||||
*/
|
||||
|
||||
@@ -45,13 +45,6 @@ switch (invocation.mode) {
|
||||
await runTui(invocation.config, invocation.resume)
|
||||
break
|
||||
}
|
||||
case 'help':
|
||||
case 'version':
|
||||
process.stdout.write(invocation.text)
|
||||
process.exit(0)
|
||||
case 'error':
|
||||
process.stderr.write(`${invocation.message}\n`)
|
||||
process.exit(1)
|
||||
default:
|
||||
invocation satisfies never
|
||||
throw new Error(`dsh: unhandled invocation mode ${JSON.stringify(invocation)}`)
|
||||
|
||||
@@ -71,7 +71,6 @@ async function consumeUntilTurnEnd(frames: AsyncIterable<RpcRequest<MuxFrame>>,
|
||||
* @param task - the prompt text for the single turn.
|
||||
*/
|
||||
export async function runHeadless(task: string): Promise<void> {
|
||||
|
||||
// A missing DEEPSEEK_API_KEY throws here (plugin load is fail-loud, uncaught by design).
|
||||
const host = await startHost({
|
||||
boot: {
|
||||
|
||||
@@ -74,13 +74,13 @@ export async function runTui(config: string | undefined, resumeSessionId: string
|
||||
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 the optional config positional and `--resume <id>`.
|
||||
// The `--` guard keeps a config named like a flag or `web` a positional.
|
||||
const nextArgv = [
|
||||
process.execPath,
|
||||
...process.execArgv,
|
||||
entry,
|
||||
...config !== undefined ? [config] : [],
|
||||
'--resume',
|
||||
sessionId,
|
||||
`--resume=${sessionId}`,
|
||||
...config !== undefined ? ['--', config] : [],
|
||||
]
|
||||
try {
|
||||
await current.fiber.dispose()
|
||||
|
||||
@@ -1,8 +1,28 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ALL_INTERFACES_HOST, LOOPBACK_HOST, parseDshArgs } from '../src/args.ts'
|
||||
|
||||
const parse = (argv: string[]) => parseDshArgs(argv, '1.2.3')
|
||||
|
||||
/**
|
||||
* `parseDshArgs` calls `process.exit` for `--help`/`--version`/errors and lets
|
||||
* Commander print to the real streams; capture the exit code and mute output.
|
||||
*/
|
||||
function exitCode(argv: string[]): number {
|
||||
const exit = vi.spyOn(process, 'exit').mockImplementation(() => { throw new Error('exit') })
|
||||
vi.spyOn(process.stdout, 'write').mockReturnValue(true)
|
||||
vi.spyOn(process.stderr, 'write').mockReturnValue(true)
|
||||
try {
|
||||
parse(argv)
|
||||
throw new Error(`expected ${JSON.stringify(argv)} to exit`)
|
||||
} catch {
|
||||
return exit.mock.calls.at(-1)?.[0] as number
|
||||
} finally {
|
||||
vi.restoreAllMocks()
|
||||
}
|
||||
}
|
||||
|
||||
afterEach(() => { vi.restoreAllMocks() })
|
||||
|
||||
describe('parseDshArgs', () => {
|
||||
it('routes each mode by its shape: default TUI, -p headless, web subcommand', () => {
|
||||
expect(parse([])).toEqual({ mode: 'tui' })
|
||||
@@ -10,25 +30,24 @@ describe('parseDshArgs', () => {
|
||||
expect(parse(['--resume', 'sess', 'app.yml'])).toEqual({ mode: 'tui', config: 'app.yml', resume: 'sess' })
|
||||
expect(parse(['-p', 'do the thing'])).toEqual({ mode: 'headless', prompt: 'do the thing' })
|
||||
expect(parse(['web'])).toEqual({ mode: 'web', host: LOOPBACK_HOST, port: 3080, dev: false })
|
||||
expect(parse(['web', '--host', ALL_INTERFACES_HOST, '--port', '8080']))
|
||||
.toEqual({ mode: 'web', host: ALL_INTERFACES_HOST, port: 8080, dev: false })
|
||||
expect(parse(['web', '--dev'])).toEqual({ mode: 'web', host: LOOPBACK_HOST, port: 3080, dev: true })
|
||||
expect(parse(['web', '--host', ALL_INTERFACES_HOST, '--port', '8080', '--dev']))
|
||||
.toEqual({ mode: 'web', host: ALL_INTERFACES_HOST, port: 8080, dev: true })
|
||||
})
|
||||
|
||||
it('fails loud instead of silently starting fresh or serving on bad input', () => {
|
||||
// An empty resume/prompt would otherwise be swallowed (agent-loop treats an
|
||||
// empty resume id as no-resume); a bad host/port must not reach the listener.
|
||||
expect(parse(['--resume=']).mode).toBe('error')
|
||||
expect(parse(['-p', '']).mode).toBe('error')
|
||||
expect(parse(['web', '--host', '10.0.0.1']).mode).toBe('error')
|
||||
expect(parse(['web', '--port', 'abc']).mode).toBe('error')
|
||||
expect(parse(['--bogus']).mode).toBe('error')
|
||||
it('exits nonzero instead of silently starting fresh, serving, or dropping inputs', () => {
|
||||
// Empty resume/prompt would be swallowed downstream; bad host/port must not
|
||||
// reach the listener; --prompt mixed with TUI inputs must not lose them.
|
||||
expect(exitCode(['--resume='])).toBe(1)
|
||||
expect(exitCode(['-p', ''])).toBe(1)
|
||||
expect(exitCode(['web', '--host', '10.0.0.1'])).toBe(1)
|
||||
expect(exitCode(['web', '--port', 'abc'])).toBe(1)
|
||||
expect(exitCode(['web', '--port='])).toBe(1)
|
||||
expect(exitCode(['config.yml', '-p', 'x'])).toBe(1)
|
||||
expect(exitCode(['--bogus'])).toBe(1)
|
||||
})
|
||||
|
||||
it('surfaces --help and --version as printable data, not a process exit', () => {
|
||||
const help = parse(['--help'])
|
||||
expect(help).toMatchObject({ mode: 'help' })
|
||||
if (help.mode === 'help') expect(help.text).toContain('Usage: dsh')
|
||||
expect(parse(['--version'])).toEqual({ mode: 'version', text: '1.2.3\n' })
|
||||
it('exits 0 for --help (disclosing web) and --version', () => {
|
||||
expect(exitCode(['--help'])).toBe(0)
|
||||
expect(exitCode(['--version'])).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -36,7 +36,8 @@ function runBuiltBin(): Promise<{ stdout: string; code: number; stderr: string }
|
||||
child.kill('SIGKILL')
|
||||
reject(new Error(`dsh built bin did not exit within 25s. stdout:\n${stdout}\nstderr:\n${stderr}`))
|
||||
}, 25_000)
|
||||
child.on('exit', (code) => { clearTimeout(timer); resolve({ stdout, code: code ?? -1, stderr }) })
|
||||
// Resolve on `close` (all stdio drained), not `exit`, so captured output is complete.
|
||||
child.on('close', (code) => { clearTimeout(timer); resolve({ stdout, code: code ?? -1, stderr }) })
|
||||
child.on('error', (err) => { clearTimeout(timer); reject(err) })
|
||||
child.stdin.end()
|
||||
})
|
||||
|
||||
@@ -34,8 +34,8 @@
|
||||
model: deepseek-v4-pro
|
||||
# `dsh --resume <id>` provides the session id on the boot context (the ids
|
||||
# live under ./.sessions); with no flag the identifier is undefined and a
|
||||
# fresh session starts each run. The demo bin never provides it, so the
|
||||
# typeof guard reads undefined there rather than throwing.
|
||||
# fresh session starts each run. The typeof guard tolerates a launcher that
|
||||
# never provides the slot, reading undefined rather than throwing.
|
||||
resumeSessionId: !!js "typeof resumeSessionId === 'string' ? resumeSessionId : undefined"
|
||||
persistenceRoot: './.sessions'
|
||||
# Printed on exit and listed by `/resume`; `{session}` fills the live id.
|
||||
|
||||
@@ -359,7 +359,7 @@ describe('config-driven session id', () => {
|
||||
await ctx2.fiber.dispose()
|
||||
})
|
||||
|
||||
it('config-driven resumeSessionId continues a persisted session (env-var resume)', async () => {
|
||||
it('config-driven resumeSessionId continues a persisted session', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-cfg-resume-'))
|
||||
dirs.push(root)
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
],
|
||||
"license": "BSD-3-Clause",
|
||||
"peerDependencies": {
|
||||
"@cordisjs/plugin-include": "^1.0.4",
|
||||
"@cordisjs/plugin-loader": "^1.0.0-rc.5",
|
||||
"@deepseek-ai/dsh-agent": "^0.0.1",
|
||||
"@deepseek-ai/dsh-agent-loop": "^0.0.1",
|
||||
@@ -51,7 +50,6 @@
|
||||
"schemastery": "^3.17.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cordisjs/plugin-include": "workspace:^",
|
||||
"@cordisjs/plugin-loader": "workspace:^",
|
||||
"@deepseek-ai/dsh-agent": "workspace:^",
|
||||
"@deepseek-ai/dsh-agent-loop": "workspace:^",
|
||||
|
||||
@@ -14,12 +14,6 @@
|
||||
{
|
||||
"path": "../../../vendor/schemastery"
|
||||
},
|
||||
{
|
||||
"path": "../../../vendor/loader"
|
||||
},
|
||||
{
|
||||
"path": "../../ui/app-boot"
|
||||
},
|
||||
{
|
||||
"path": "../../core/agent"
|
||||
},
|
||||
|
||||
@@ -18,4 +18,4 @@ A UI integration is a client-driver plugin, not a loop change: it consumes the e
|
||||
|
||||
`user-approval`, `user-interaction`, and `tool-ask-user` live here because asking a human is a UI-backed product affordance, not part of the providerless core spine. `user-approval` owns the one-shot `ctx.approval` decision mechanism and its policy tier; answerers remain with their UI channel owners. `user-interaction` remains provider-neutral (`ctx.userInteraction`), while `tool-ask-user` is its model-facing consumer and the app/bridge packages provide concrete providers.
|
||||
|
||||
The runnable app bundles that bake these bridges into boot bins — the TUI app, ACP server app, and JSON-RPC SDK-runtime bin — live in [`examples/`](../examples/README.md) (`tui-demo`, `acp-demo`, `jsonrpc-demo`), each composed over the [`agent-spine-demo`](../examples/agent-spine-demo/README.md) bundle. `ui/` keeps the reusable bridge/channel plugins and the `app-boot` glue; each front door owns its stdout policy, and a leaf `cordis.yml` supplies backends and optional tools.
|
||||
The runnable app bundles that compose these bridges — the TUI app, ACP server app, and JSON-RPC SDK-runtime bin — live in [`examples/`](../examples/README.md) (`tui-demo`, `acp-demo`, `jsonrpc-demo`), each composed over the [`agent-spine-demo`](../examples/agent-spine-demo/README.md) bundle. `acp-demo` and `jsonrpc-demo` own boot bins; the `tui-demo` bundle is booted by the product [`dsh`](../../apps/cli/README.md) CLI. `ui/` keeps the reusable bridge/channel plugins and the `app-boot` glue; each front door owns its stdout policy, and a leaf `cordis.yml` supplies backends and optional tools.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Shared boot glue for the app bins (`dsh-tui-demo`, `dsh-cli-demo`, `dsh-acp-demo`): load the gitignored
|
||||
* Shared boot glue for the app bins (`dsh`, `dsh-cli-demo`, `dsh-acp-demo`): load the gitignored
|
||||
* `.env`, install the fail-loud Loader guards, resolve the config path (snapshot-aware), load the
|
||||
* optional personal overlay patches from the Harness home (`~/.dsh`), and drive the cordis Loader
|
||||
* against a leaf `cordis.yml` until the whole tree has settled.
|
||||
@@ -156,7 +156,6 @@ export function assertEntriesLoaded(ctx: Context, binName: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Context key a bin sets through {@link boot}'s `prepare` hook to hand a resume
|
||||
* session id to the booted config: `ctx.provide(RESUME_SESSION_ID_KEY, id)`
|
||||
|
||||
5
pnpm-lock.yaml
generated
5
pnpm-lock.yaml
generated
@@ -1539,9 +1539,6 @@ importers:
|
||||
|
||||
packages/examples/tui-demo:
|
||||
devDependencies:
|
||||
'@cordisjs/plugin-include':
|
||||
specifier: workspace:^
|
||||
version: link:../../../vendor/include
|
||||
'@cordisjs/plugin-loader':
|
||||
specifier: workspace:^
|
||||
version: link:../../../vendor/loader
|
||||
@@ -1604,7 +1601,7 @@ importers:
|
||||
version: link:../../context/workspace-context
|
||||
cordis:
|
||||
specifier: ^4.0.0-rc.7
|
||||
version: 4.0.0-rc.7(@cordisjs/plugin-include@vendor+include)(@cordisjs/plugin-loader@vendor+loader)
|
||||
version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@vendor+loader)
|
||||
schemastery:
|
||||
specifier: ^3.17.0
|
||||
version: 3.18.0
|
||||
|
||||
Reference in New Issue
Block a user